날짜와 시간 & 형식화

해당 포스팅의 내용은 Java의 정석 2권 - Chapter 10 날짜와 시간 & 형식화에 있는 내용을 요약한 것입니다. 해당 책으로 복습하며 정리한 내용이고 문제가 된다면 바로 해당 포스팅을 삭제하도록 하겠습니다.

날짜와 시간 & 형식화

타임존 포함 ISO 8601 문자열의 표현

날짜/시간 및 타임존을 다루는 국제적인 규약은 다양하다. RFC 822, 1036, 1123, 2822, 3339, ISO 8601 등이 있다. 여기서는 ISO 8601RFC 3339와 관련된 표기법을 알아본다.

1
2
3
4
5
6
7
8
// 로컬 시간을 의미하는 ISO 8601 문자열
2017-11-06T15:00:00.000

// UTC(GMT) 시간을 의미하는 ISO 8601 문자열
2017-11-06T06:00:00.000Z

// 로컬 시간을 의미하면서 UTC(GMT) 대비 +09:00 임을 의미하는 ISO 8601 문자열
2017-11-06T15:00:00.000+09:00
  • 2017-11-06T15:00:00.000ISO 8601의 기본 형식이다. 해당 시간이 로컬 시간 임을 의미한다.
  • 2017-11-06T06:00:00.000Z와 같이 뒤에 Z 식별자를 추가하면 해당 시간이 UTC(GMT) 시간 임을 의미한다.
  • 2017-11-06T15:00:00.000+09:00와 같이 뒤에 Z 대신 +HH:mm 식별자를 추가하면 해당 시간이 로컬 시간이면서 **UTC(GMT)**와 09:00 만큼 차이가 남을 의미한다. 이 형식의 장점은 인간이 손쉽게 추가적인 계산 없이 로컬 시간을 인지하면서 추가적으로 타임존 정보까지 제공하기 때문에 가장 인간친화적이라고 할 수 있다.

날짜와 시간

Date는 날짜와 시간을 다룰 목적으로 JDK 1.0부터 제공되어온 클래스이다. Date 클래스는 기능이 부족했기 때문에, Calendar라는 새로운 클래스를 그 다음 버젼인 JDK 1.1부터 제공하기 시작했다. Calendar는 Date보다는 훨씬 나았지만 몇 가지 단점들이 있었고, JDK 1.8부터 java.time 패키지로 기존의 단점들을 개선한 새로운 클래스들이 추가되었다.

Date 클래스는 java.util 패키지에 속해있다.

Date와 Calendar간의 변환

Calendar가 새로 추가되면서 Date는 대부분의 메서드가 ‘deprecated’되었으므로 잘 사용되지 않는다. 그럼에도 불구하고 여전히 Date를 필요로 하는 메서드들이 존재하기 때문에 Calendar를 Date로 또는 그 반대로 변환할 일이 생긴다.

1
2
3
4
5
6
7
8
9
1. Calendar를 Date로 변환
Calendar cal = Calendar.getInstance();
Date d1 = new Date(cal.getTimeInMillis()); // Date(long date)
Date d2 = cal.getTime();

2. Date를 Calendar로 변환
Date d = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(d);

Calendar.getInstance()를 통해서 얻은 인스턴스는 기본적으로 현재 시스템의 날짜와 시간에 대한 정보를 담고 있다. (GregorianCalendar, BuddhistCalendar)

형식화 클래스

자바의 형식화 클래스는 java.text 패키지에 포함되어 있으며 숫자, 날짜, 텍스트 데이터를 일정한 형식에 맞게 표현할 수 있는 방법을 객체지향적으로 설계하여 표준화하였다. 형식화 클래스는 형식화에 사용될 패턴을 정의하는데, 데이터를 정의된 패턴에 맞춰 형식화할 수 있을 뿐만 아니라 역으로 형식화된 데이터에서 원래의 데이터를 얻어낼 수도 있다. 즉, 형식화된 데이터의 패턴만 정의해주면 복잡한 문자열에서도 substring()을 사용하지 않고도 쉽게 원하는 값을 얻어낼 수 있다는 것이다.

DecimalFormat

형식화 클래스 중에서 숫자를 형식화 하는데 사용되는 것이 DecimalFormat이다. DecimalFormat을 이용하면 숫자 데이터를 정수, 부동소수점, 금액 등의 다양한 형식으로 표현할 수 있으며, 반대로 일정한 형식의 테스트 데이터를 숫자로 쉽게 변환하는 것도 가능하다.

1
2
3
double number = 1234567.89;
DecimalFormat df = new DecimalFormat("#.#E0");
String result = df.format(number);

Number 클래스는 Integer, Double과 같은 숫자를 저장하는 래퍼 클래스의 조상이며, doubleValue()는 Number에 저장된 값을 double형의 값으로 변환하여 반환한다. 이 외에도 intValue(), floatValue()등의 메서드가 Number클래스에 정의되어 있다.

SimpleDateFormat

Date와 Calendar만으로는 날짜 데이터를 원하는 형태로 다양하게 출력하는 것은 불편하고 복잡하다. 그러나 SimpleDateFormat을 사용하면 이러한 문제들이 간단하게 해결된다.

DateFormat은 추상클래스로 SimpleDateFormat의 조상이다. DateFormat는 추상클래스이므로 인스턴스를 생성하기 위해서는 getDateInstance()와 같은 static 메서드를 이용해야 한다. getDateInstance()에 의해서 반환되는 것은 DateFormat을 상속받아 완전하게 구현한 SimpleDateFormat 인스턴스이다.

1
2
3
4
5
Date today = new Date();
SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd");

// 오늘 날짜를 yyyy-MM-dd 형태로 변환하여 반환한다.
String result = df.format(today);

Date 인스턴스만 format 메서드에 사용될 수 있다.

1
2
DateFormat df = new SimpleDateFormat("yyyy년 MM월 dd일");
Date d = df.parse("2018년 6월 6일");

parse(String source)를 사용하여 날짜 데이터의 출력형식을 변환하는 방법을 보여주는 예제이다. Integer의 parseInt()가 문자열을 정수로 변환하는 것처럼 SimpleDateFormat의 parse(String source)는 문자열(source)을 날짜(Date인스턴스)로 변환해주기 때문에 매우 유용하게 쓰일 수 있다.

ChoiceFormat

ChoiceFormat은 특정 범위에 속하는 값을 문자열로 변환해준다. 연속적 또는 불연속적인 범위의 값들을 처리하는 데 있어서 if문이나 switch문은 적절하지 못한 경우가 많다. 이럴때 ChoiceFormat을 잘 사용하면 복잡하게 처리될 수밖에 없었던 코드를 간단하고 직관적으로 만들 수 있다.

MessageFormat

MessageFormat은 데이터를 정해진 양식에 맞게 출력할 수 있도록 도와준다. 데이터가 들어갈 자리를 마련해 놓은 양식을 미리 작성하고 프로그램을 이용해서 다수의 데이터를 같은 양식으로 출력할 때 사용하면 좋다. 하나의 데이터를 다양한 양식으로 출력할 때 사용한다.

그리고 SimpleDateFormat의 parse처럼 MessageFormat의 parse를 이용하면 지정된 양식에서 필요한 데이터만을 손쉽게 추출해 낼 수도 있다.

1
2
3
4
String msg = "Name: {0} \nTel: {1} \nnAge:{2} \nBirthday:{3}";
Object[] arguments = {"이름", "01-234-5678", "27", "04-27"};

String result = MessageFormat.format(msg, arguments);

MessageFormat에 사용될 양식인 문자열 msg를 작성할 때 ‘{숫자}’로 표시된 부분이 데이터가 출력될 자리이다.

데이터를 양식에 넣어서 출려하는것 뿐만 아니라, parse(String source)를 이용해서 출력된 데이터로부터 필요한 데이터만을 뽑아낼 수 있다.

Java.time 패키지

java의 탄생부터 지금까지 날짜와 시간을 다루는데 사용해왔던, Date와 Calendar가 가지고 있던 단점들을 해소하기 위해 JDK1.8부터 ‘java.time 패키지’가 추가되었다. 이 패키지는 다음과 같이 4개의 하위 패키지를 가지고 있다.

  • java.time : 날짜와 시간을 다루는데 필요한 핵심 클래스들을 제공
    • java.time.chrono : 표준(ISO)이 아닌 달력 시스템을 위한 클래스들을 제공
    • java.time.format : 날짜와 시간을 파싱하고, 형식화하기 위한 클래스들을 제공
    • java.time.temporal : 날짜와 시간의 필드(field)와 단위(unit)를 위한 클래스들을 제공
    • java.time.zone : 시간대(time-zone)와 관련된 클래스들을 제공

위의 패키지들에 속한 클래스들의 가장 큰 특징은 String 클래스처럼 **불변(immutable)**이라는 것이다. 그래서 날짜나 시간을 변경하는 메서드들은 기존의 객체를 변경하는 대신 항상 변경된 새로운 객체를 반환한다. 기존 Calendar 클래스는 변경 가능하므로, 멀티 쓰레드 환경에서 안전하지 못하다.

멀티 쓰레드 환경에서는 동시에 여러 쓰레드가 같은 개겣에 접근할 수 있기 때문에, 변경 가능한 객체는 데이터가 잘못될 가능성이 있으며, 이를 쓰레드에 안전(thread-safe)하지 않다고 한다.

java.time 패키지의 핵심 클래스

날짜와 시간을 하나로 표현하는 Calendar 클래스와 달리, java.time 패키지에서는 날짜와 시간을 별도의 클래스로 분리해 놓았다. 시간을 표현할 때는 LocalTime 클래스를 사용하고, 날짜를 표현할 때는 LocalDate 클래스를 사용한다. 그리고 날짜와 시간이 모두 필요할 때는 LocalDateTime 클래스를 사용하면 된다.

LocalDate + LocalTime -> LocalDateTime
날짜 시간 날짜 & 시간

여기에 시간대(time-zone)까지 다뤄야 한다면, ZonedDateTime 클래스를 사용한다.

LocalDateTime + 시간대 -> ZonedDateTime

Calendar는 ZonedDateTime처럼, 날짜와 시간 그리고 시간대까지 모두 가지고 있다. Date와 유사한 클래스로는 Instant가 있는데, 이 클래스는 날짜와 시간을 초 단위(정확히는 나노초)로 표현한다. 날짜와 시간을 초단위로 표현한 값을 타임스탬프(time-stamp) 라고 부르는데, 이 값은 날짜와 시간을 하나의 정수로 표현할 수 있으므로 날짜와 시간의 차이를 계산하거나 순서를 비교하는데 유리해서 데이터베이스에 많이 사용한다.

객체 생성하기 - now(), of()

java.time 패키지에 속한 클래스의 객체를 생성하는 가장 기본적인 방법은 now()와 of()를 사용하는 것이다.

1
2
3
4
LocalDate date = LocalDate.now(); // 2018-06-06
LocalTime time = LocalTime.now(); // 02:34:50.223
LocalDateTime dateTime = LocalDateTime.now(); // 2018-06-06T02:34:50.223
ZonedDateTime dateTimeInKr = ZonedDateTime.now(); // 2018-06-06T02:34:50.223+09:00[Asia/Seoul]

LocalDate와 LocalTime

LocalDateLocalTime은 java.time 패키지의 가장 기본이 되는 클래스이며, 나머지 클래스들은 이들의 확장이므로 이 두 클래스만 잘 이해하고 나면 나머지는 아주 쉬워진다.

객체를 생성하는 방법은 현재의 날짜와 시간을 LocalDate와 LocalTime으로 각각 반환하는 now()와 지정된 날짜와 시간으로 LocalDate와 LocalTime 객체를 생성하는 of()가 있다.

  • 특정 필드의 값 가져오기 - get(), getXXX()
  • 필드의 값 변경하기 - with(), plus(), minus()
  • 날짜와 시간의 비교 - isAfter(), isBefore(), isEqual()

Instant

Instant는 에포크 타임(EPOCH TIME, 1970-01-01 00:00:00 UTC)부터 경과된 시간을 나노초 단위로 표현한다. 사람이 보기에는 불편하지만, 단일 진법으로 다루기 때문에 계산에는 편리하다. 사람이 사용하는 날짜와 시간에는 여러 진법이 섞여있어서 계산하기 어렵다.

1
2
3
Instant now = Instant.now();
Instant now2 = Instant.ofEpochSecond(now.getEpochSecond());
Instant now3 = Instant.ofEpochSecond(now.getEpochSecond(), now.getNano());

Instant를 생성할 때는 위와 같이 now()와 ofEpochSecond()를 사용한다. 그리고 필드에 저장된 값을 가져올 때는 다음과 같이 한다.

1
2
long epochSec = now.getEpochSecond();
int nano = now.getNano();

위의 코드처럼, Instant는 시간을 초 단위와 나노초 단위로 나누어 저장한다. 오라클 데이터베이스의 타임스탬프(timestamp)처럼 밀리초 단위의 EPOCH TIME을 필요로 하는 경우를 위해 toEpochMilli()가 정의되어 있다.

1
long toEpochMilli()

Instant는 항상 UTC(+00:00)를 기준으로 하기 때문에, LocalTime과 차이가 있을 수 있다. 예를 들어 한국은 시간대가 ‘+09:00’이므로 Instant와 LocalTime간에는 9시간의 차이가 있다. 시간대를 고려해야하는 경우 OffsetDateTime을 사용하는 것이 더 나은 선택일 수 있다.

UTC는 ‘Coordinated Universal Time’의 약어로 ‘세계 협정시’이라고 하며, 1972년 1월 1일부터 시행된 국제 표준시이다. 이전에 사용되던 GMT(Greenwich Mean Time)와 UTC는 거의 같지만, UTC가 좀 더 정확하다.

LocalDateTime과 ZonedDateTime

LocalDateTime에 시간대(time-zone)를 추가하면, ZonedDateTime이 된다. 기존에는 TimeZone클래스로 시간대를 다뤘지만 새로운 시간 패키지에서는 ZoneId라는 클래스를 사용한다. ZoneId는 일광 절약시간(DST, Daylight Saving Time)을 자동적으로 처리해주므로 더 편리하다.

LocalDate에 시간 정보를 추가하는 atTime()을 쓰면 LocalDateTime을 얻을 수 있는 것처럼, LocalDateTime에 atZone()으로 시간대 정보를 추가하면, ZonedDateTime을 얻을 수 있다.

1
2
3
ZoneId zid = ZoneId.of("Asia/Seoul");
zonedDateTime zdt = dateTime.atZone(zid);
Syste.out.println(zdt); // 2018-06-06T14:23:50.235+09:00[Asia/Seoul]

만일 현재 특정 시간대의 시간, 예를 들어 뉴욕을 알고 싶다면 다음과 같이 하면 된다.

1
2
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);

ZoneOffSet

UTC로부터 얼마만큼 떨어져 있는지를 ZoneOffSet으로 표현한다. 위의 결과에서 알 수 있듯이 서울은 ‘+9’이다. 즉, UTC보다 9시간(32400초=60*60*9)이 빠르다.

1
2
ZoneOffset krOffset = ZonedDateTime.now().getOffset();
int krOffsetInSec = KrOffset.get(ChronoField.OFFSET_SECONDS); // 32400초

OffsetDateTime

ZonedDateTime은 ZoneId로 구역을 표현하는데, ZoneId가 아닌 ZoneOffset을 사용하는 것이 OffSetDateTime이다. ZoneId는 일광절약시간처럼 시간대와 관련된 규칙들을 포함하고 있는데, ZoneOffset은 단지 시간대를 시간의 차이로만 구분한다. 컴퓨터에게 일광절약시간처럼 계절별로 시간을 더했다 뺐다 하는 것과 같은 행위는 위험하다. 아무런 변화 없이 일관된 시간체계를 유지하는 것이 더 안전하다. 같은 지역 내의 컴퓨터 간에 데이터를 주고 받을 때, 전송시간을 표현하기에 LocalDateTime이면, 충분하겠지만, 서로 다른 시간대에 존재하는 컴퓨터간의 통신에는 OffsetDateTime이 필요하다.

일광 절약 시간제(Daylight saving time, DST) 또는 서머 타임(summer time)은 하절기에 표준시를 원래 시간보다 한 시간 앞당긴 시간을 쓰는 것을 말한다. 즉, 0시에 일광 절약 시간제를 실시하면 1시로 시간을 조정해야 하는 것이다. 실제 낮 시간과 사람들이 활동하는 낮 시간 사이의 격차를 줄이기 위해 사용한다.

1
2
3
4
5
ZonedDateTime zdt = ZondedDateTime.of(date, time, zid);
OffsetDateTime odt = offsetDateTime.of(date, time, krOffset);

// ZonedDatetime -> OffsetDateTime
OffsetDateTime odt = zdt.toOffsetDateTime();

OffsetDateTime을 ZonedDateTime처럼, LocalDate와 LocalTime에 ZonedOffset을 더하거나, ZonedDateTime에 toOffsetDateTime()을 호출해서 얻을 수도 있다.

지금까지의 내용을 예제로 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
System.out.println("LocalDate : " + ld);
System.out.println("LocalTime : " +lt);

LocalDateTime dt = LocalDateTime.of(ld, lt);
System.out.println("LocalDateTime : " + dt);

ZoneId zid = ZoneId.of("Asia/Seoul");
ZonedDateTime zdt = dt.atZone(zid);
System.out.println("ZonedDateTime1 : " + zdt);

ZonedDateTime seoulTime = ZonedDateTime.now();
System.out.println("ZonedDateTime2 : " + seoulTime);

// 특정 구역 시간의 다른 구역 시간 구하기
ZoneId nyId = ZoneId.of("America/New_York");
ZonedDateTime nyTime = ZonedDateTime.now().withZoneSameInstant(nyId);
System.out.println("ZonedDateTime3 : " + nyTime);

// 출력 결과
LocalDate : 2018-06-06
LocalTime : 10:28:37.743
LocalDateTime : 2018-06-06T10:28:37.743
ZonedDateTime1 : 2018-06-06T10:28:37.743+09:00[Asia/Seoul]
ZonedDateTime2 : 2018-06-06T10:28:37.744+09:00[Asia/Seoul]
ZonedDateTime3 : 2018-06-05T21:28:37.747-04:00[America/New_York]

TemporalAdjusters

plus(), minus()와 같은 메서드로 날짜와 시간을 계산하기에는 불편한 경우가 있다. (Ex. 이번 달의 3번째 금요일) 그래서 자주 쓰일만한 날짜 계산들을 대신 해주는 메서드를 정의해놓은 것이 TemporalAdjusters 클래스이다.

1
2
LocalDate today = LocalDate.now();
LocalDate nextMonday = today.with(TemporalAdjusters.next(DayofWeek.MONDAY))l

with()는 LocalTime, LocalDateTime, ZonedDateTime, Instant 등 대부분의 날짜와 시간에 관련된 클래스에 포함되어 있다.

Period와 Duration

Period는 날짜의 차이를, Duration은 시간의 차이를 계산하기 위한 것이다.

between()

두 날짜 date1과 date2의 차이를 나타내는 Period는 between()으로 얻을 수 있다.

1
2
3
LocalDate date1 = LocalDate.of(2014, 1, 1);
LocalDate date2 = LocalDate.of(2018, 6, 6);
Period pe = Period.between(date1, date2)

date1이 date2보다 날짜 상으로 이전이면 양수로, 이후면 음수로 Period에 저장된다. 그리고 시간차이를 구할 때는 Duration을 사용한다는 것을 제외하고는 Period와 똑같다.

Period, Duration에서 특정 필드의 값을 얻을 때는 get()을 사용한다.

1
2
3
4
5
6
long year = pe.get(ChronoUnit.YEARS); // int getYears()
long month = pe.get(ChronoUnit.MONTHS); // int getMonths()
long day = pe.get(ChronoUnit.DAYS); // int getDays()

long sec = du.get(ChronoUnit.SECONDS); // long getSeconds()
long nano = du.get(ChronoUnit.NANOS); // int getNano()

between()과 until()

until()은 between()과 거의 같은 일을 한다. between()은 static 메서드이고, until()은 인스턴스 메서드라는 차이가 있다.

Period는 년월일을 분리해서 저장하기 때문에, D-day를 구하려는 경우에는 두 개의 매개변수를 받는 until()을 사용하는 것이 낫다.

파싱과 포맷

날짜와 시간을 원하는 형식으로 출력하고 해석(파싱)을 위한 형식화(formatting)와 관련된 클래스들은 java.time.format 패키지에 들어 있다. 그 중에서 DateTimeFormatter가 핵심이다. 이 클래스에는 자주 쓰이는 다양한 형식들을 기본적으로 정의하고 있으며, 그 외의 형식이 필요하다면 직접 정의해서 사용할 수도 있다.

1
2
3
LocalDate date = LocalDate.of(2016, 6, 6);
String yyyymmdd = DateTimeFormatter.ISO_LOCAL_DATE.format(date); // "2016-06-06"
String yyyymmdd = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // "2018-06-06

날짜와 시간의 형식화에는 format()이 사용되는데, 이 메서드는 DateTimeFormatter뿐만 아니라 LocalDate나 LocalTime같은 클래스에도 있다. 같은 기능을 하기 때문에 상황에 따라 편한 쪽을 선택해서 사용하면 된다.

문자열을 날짜와 시간으로 파싱하기

문자열을 날짜 또는 시간으로 변환하려면 static 메서드 parse()를 사용하면 된다. 자주 사용되는 기본적인 형식의 문자열은 ISO_LOCAL_DATE와 같은 형식화 상수를 사용하지 않고도 파싱이 가능하다.

1
2
3
4
LocalDate date = LocalDate.parse("2018-06-06", DateTimeFormatter.ISO_LOCAL_DATE);
LocalDate newDate = LocalDate.parse("2018-06-06");
LocalTime newTime = LocalTime.parse("23:59:59");
LocalDateTime newDateTime = LocalDateTime.parse("2018-06-06T23:59:59");

참고

java.lang 패키지와 유용한 클래스

java.lang 패키지와 유용한 클래스

java.lang 패키지는 자바프로그래밍에 가장 기본이 되는 클래스들을 포함하고 있다. 그렇기 때문에 java.lang 패키지의 클래스들은 import문 없이도 사용할 수 있게 되어 있다. 그 동안 String 클래스나 System 클래스를 import문 없이 사용할 수 있었던 이유가 바로 java.lang 패키지에 속한 클래스들이기 때문이었던 것이다.

java.lang 패키지

Object 클래스

Object 클래스는 멤버변수는 없고 오직 11개의 메서드만 가지고 있다.

equals(Object obj)
매개변수로 객체의 참조변수를 받아서 비교하여 그 결과를 booelan 값으로 알려주는 역할을 한다.* 아래의 코드는 Object 클래스에 정의되어 있는 equals 메서드의 실제 내용이다.

1
2
3
public boolean equals(Object obj) {
return (this==obj);
}

두 객체의 같고 다름을 참조변수의 값으로 판단한다.

String 클래스는 Object 클래스의 equals 메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통해서 String 인스턴스가 갖는 문자열 값을 비교하도록 되어있다.

String 클래스 뿐만 아니라 Date, File, Wrapper 클래스(Integer, Double 등)의 equals 메서드도 주소값이 아닌 내용을 비교하도록 오버라이딩되어 있다. 그러나 StringBuffer 클래스는 오버라이딩되어 있지 않다.

hashCode()
이 메서드는 해싱(hashing)기법에 사용되는 ‘해시함수(hash function)’를 구현한 것이다. 해싱은 데이터관리기법 중의 하나인데 다량의 데이터를 저장하고 검색하는 데 유용하다.*

해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드(hashcode)를 반환한다.

Object 클래스에 정의된 hashCode 메서드는 객체의 주소값을 이용해서 해시코드를 만들어 반환하기 때문에 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없다. (해싱기법을 사용하는 HashMap이나 HashSet과 같은 클래스에 저장할 객체라면 반드시 hashCode 메서드를 오버라이딩해야 한다.)

String 클래스는 문자열의 내용이 같으면, 동일한 해시코드를 반환하도록 hashCode 메서드가 오버라이딩되어 있기 때문에, 문자열의 내용이 같은 str1과 str2에 대해 hashCode()를 호출하면 항상 동일한 해시코드값을 얻는다.

반면에 System.identifyHashCode(Object x)는 Object 클래스의 hashCode 메서드처럼 객체의 주소값으로 해시코드를 생성하기 때문에 모든 객체에 대해 항상 다른 해시코드값을 반환할 것을 보장한다. 그래서 str1과 str2가 해시코드는 같지만 서로 다른 객체라는 것을 알 수 있다.

toString()
인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의한 것이다. 인스턴스의 정보를 제공한다는 것은 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻이다.

Object클래스에 정의된 toString()은 아래와 같다.

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

clone()
자신을 복제하여 새로운 인스턴스를 생성하는 일을 한다. Object 클래스에 정의된 clone()은 단순히 인스턴스변수의 값만을 복사하기 때문에 참조 변수 타입의 인스턴스 변수가 정의되어 있는 클래스는 완전한 인스턴스 복제가 이루어지지 않는다.

cloneable 인터페이스를 구현한 클래스에서만 clone()을 호출할 수 있다. 또한 clone()을 오버라이딩하면서 접근 제어자를 protected에서 public으로 변경해야 한다.

clone()은 단순히 객체에 저장된 값을 그대로 복제할 뿐, 객체가 참조하고 있는 객체까지 복제하지는 않는다. 반면에 원본이 참조하고 있는 객체까지 복제하는 것을 ‘깊은 복사’라고 한다. 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 원본의 변경이 복사본에 영향을 미치지 않는다.

공변 반환타입

JDK 1.5부터 ‘공변 반환타입’ 이라는 것이 추가되었다. 이 기능은 오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경하는 것이다. 따라서 clone()의 반환타입을 Object가 아닌 자손의 타입으로 변경가능하다.

‘공변 반환타입’을 사용하면 조상의 타입이 아닌 실제로 반환되는 자손 객체의 타입으로 반환할 수 있어서 번거로운 형변환이 줄어든다는 장점이 있다.

getClass()
자신이 속한 클래스의 Class객체를 반환하는 메서드인데, Class 객체는 이름이 ‘Class’인 클래스의 객체이다. Class 객체는 아래와 같이 정의되어 있다.

1
2
3
public final class Class implements ... {	// Class 클래스
...
}

Class 객체는 클래스의 모든 정보를 담고 있으며, 클래스당 단 1개만 존재한다. 그리고 클래스 파일이 ‘클래스 로더(ClassLoader)’에 의해서 메모리에 올라갈 때, 자동적으로 생성된다.

클래스 로더는 실행 시에 필요한 클래스를 동적으로 메모리에 로드하는 역할을 한다. 먼저 기존에 생성된 클래스 객체가 메모리에 존재하는지 확인하고, 있으면 객체의 참조를 반환하고 없으면 클래스 패스(classpath)에 지정된 경로를 따라서 클래스 파일을 찾는다. 못 찾으면 ClassNotFoundException이 발생하고, 찾으면 해당 클래스 파일을 읽어서 Class 객체로 변환한다.

파일 형태로 저장되어 있는 클래스를 읽어서 Class 클래스에 정의된 형식으로 변환하는 것이다. 즉, 클래스 파일을 읽어서 사용하기 편한 형태로 저장해 놓은 것이 클래스 객체이다. (클래스 파일을 메모리에 로드하고 변환하는 일은 클래스 로더가 한다.)

Class 객체를 얻는 방법

Class 객체에 대한 참조를 얻는 방법은 여러 가지가 있다.

1
2
3
Class cObj = new Card().getClass();	// 생성된 객체로 부터 얻는 방법
Class cObj = Card.class; // 클래스 리터럴(*.class)로 부터 얻는 방법
Class cObj = Class.forName("Card"); // 클래스 이름으로 부터 얻는 방법

특히 forName()은 특정 클래스 파일, 예를 들어 데이터베이스 드라이버를 메모리에 올릴 때 주로 사용한다. Class 객체를 이용하면 클래스에 정의된 멤버의 이름이나 개수 등, 클래스에 대한 모든 정보를 얻을 수 있기 때문에 Class 객체를 통해서 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드를 작성할 수 있다.

String 클래스

기존의 다른 언어에서는 문자열을 char형의 배열로 다루었으나 자바에서는 문자열을 위한 클래스를 제공한다.

변경 불가능한(immutable) 클래스
String 클래스는 문자열을 저장하기 위해서 문자형 배열 변수(char[]) value를 인스턴스 변수로 정의해놓고 있다. 인스턴스 생성 시 생성자의 매개변수로 입력받는 문자열은 이 인스턴스변수(value)에 문자형 배열(char[])로 저장되는 것이다.

1
2
3
4
public final class String implements java.io.Serializable, Comparable {
private char[] value;
...
}

한번 생성된 String 인스턴스가 갖고 있는 문자열은 읽어 올 수만 있고, 변경할 수는 없다. 예를 들어 ‘+’ 연산자를 이용해서 문자열을 결합하는 경우 인스턴스내의 문자열이 바뀌는 것이 아니라 새로운 문자열이 담긴 String 인스턴스가 생성되는 것이다.

덧셈 연산자 ‘+’를 사용해서 문자열을 결합하는 것은 매 연산 시 마다 새로운 문자열을 가진 String 인스턴스가 생성되어 메모리공간을 차지하게 되므로 가능한 한 결합횟수를 줄이는 것이 좋다.

문자열을 다루는 작업이 많이 필요한 경우에는 String 클래스 대신 StringBuffer 클래스를 사용하는 것이 좋다. StringBuffer 인스턴스에 저장된 문자열은 변경이 가능하므로 하나의 StringBuffer 인스턴스만으로도 문자열을 다루는 것이 가능하다.

문자열의 비교
문자열을 만들 때는 두 가지 방법, 문자열 리터럴을 지정하는 방법과 String 클래스의 생성자를 사용해서 만드는 방법이 있다.

1
2
3
4
Strig str1 = "abc";				// 문자열 리터럴 "abc"의 주소가 str1에 저장됨
Strig str2 = "abc"; // 문자열 리터럴 "abc"의 주소가 str2에 저장됨
Strig str3 = new String("abc"); // 새로운 String 인스턴스를 생성
Strig str4 = new String("abc"); // 새로운 String 인스턴스를 생성

String 클래스의 생성자를 이용한 경우에는 new 연산자에 의해서 메모리할당이 이루어지기 때문에 항상 새로운 String 인스턴스가 생성된다. 그러나 문자열 리터럴은 이미 존재하는 것을 재사용하는 것이다. (문자열 리터럴은 클래스가 메모리에 로드될 때 자동적으로 미리 생성된다.)

equals()를 사용했을 때는 두 문자열의 내용(“abc”)을 비교하기 때문에 두 경우 모두 true를 결과로 얻는다. 하지만, 각 String 인스턴스의 주소를 등가비교연산자 “==”로 비교했을 때는 결과가 다르다.

문자열 리터럴
자바 소스파일에 포함된 모든 문자열 리터럴은 컴파일 시에 클래스 파일에 저장된다. 이대 같은 내용의 문자열 리터럴은 한번만 저장된다.* 문자열 리터럴도 String 인스턴스이고 한번 생성하면 내용을 변경할 수 없기 때문에 하나의 인스턴스를 공유하면 되기 때문이다.

String 리터럴들은 컴파일 시에 클래스파일에 저장된다. 클래스 파일에는 소스파일에 포함된 모든 리터럴의 목록이 있다. 해당 클래스 파일이 클래스 로더에 의해 메모리에 올라갈 때, 이 리터럴의 목록에 있는 리터럴들이 JVM내에 있는 ‘상수 저장소(constant pool)’에 저장된다.

빈 문자열(empty string)
길이가 0인 배열이 존재할 수 있다. char형 배열도 길이가 0인 배열을 생성할 수 있고, 이 배열을 내부적으로 가지고 있는 문자열이 바로 빈 문자열이다.*

‘String s = “”;’ 과 같은 문장이 있을 때, 참조변수 s가 참조하고 있는 String 인스턴스는 내부에 ‘new char[0]’과 같이 길이가 0인 char형 배열을 저장하고 있는 것이다.

그러나 ‘String s = “”;’과 같은 표현이 가능하다고 해서 ‘char c = ‘’;’와 같은 표현도 가능한 것은 아니다. char형 변수에는 반드시 하나의 문자를 지정해야한다.

일반적으로 변수를 선언할 때, 각 타입의 기본값으로 초기화 하지만 String은 참조형 타입의 기본값인 null 보다는 빈 문자열로, char형인 기본값은 ‘₩u0000’ 대신 공백으로 초기화 하는 것이 보통이다. (‘₩u0000’은 유니코드의 첫 번째 문자로써 아무런 문자도 지정되지 않은 빈 문자이다.)

문자 인코딩 변환
getBytes(String charsetName)를 사용하면, 문자열의 문자 인코딩을 다른 인코딩으로 변경할 수 있다. 자바가 UTF-16을 사용하지만, 문자열 리터럴에 포함되는 문자들은 OS의 인코딩을 사용한다.

1
2
byte[] utf8_str = "가".getBytes("UTF-8");	// 문자열을 UTF-8로 분환
String str = new String(utf8_str, "UTF-8"); // byte 배열을 문자열로 변환

서로 다른 문자 인코딩을 사용하는 컴퓨터 간에 데이터를 주고받을 때는 적절한 문자 인코딩이 필요하다.

UTF-8은 한글 한 글자를 3 byte로 표현하고, CP949는 2 byte로 표현한다.

기본형 값을 String으로 변환
기본형을 문자열로 변경하는 방법은 간단하다. 숫자에 빈 문자열””을 더해주기만 하면 된다. 이 외에도 valueOf()를 사용하는 방법도 있다. 성능은 valueOf()가 더 좋지만, 빈 문자열을 더하는 방법이 간단하고 편하기 때문에 성능향상이 필요한 경우에만 valueOf()를 쓰자.

참조변수에 String을 더하면, 참조변수가 가리키고 있는 인스턴스의 toString()을 호출하여 String을 얻은 다음 결합한다.

String을 기본형 값으로 변환
이전에는 parseInt()와 같은 메서드를 많이 섰는데, 메서드의 이름을 통일하기 위해 valueOf()가 나중에 추가되었다. valueOf(String s)는 메서드 내부에서 그저 parseInt(String s)를 호출할 뿐이므로, 두 메서드는 반환 타입만 다르지 같은 메서드이다.

1
2
3
public static Integer valueOf(String s)throws NumberFormatException {
return Integer.valueOf(ParseInt(s, 10));
}

StringBuffer 클래스와 StringBuilder 클래스

String 클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없지만 StringBuffer 클래스는 변경이 가능하다. 내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며, StringBuffer 인스턴스를 생성할 때 그 크기를 지정할 수 있다.

StringBuffer 클래스는 String 클래스와 같이 문자열을 저장하기 위한 char형 배열의 참조변수 인스턴스로 선언해 놓고 있다. StringBuffer 인스턴스가 생성될 때, char형 배열이 생성되며 이 때 생성된 char형 배열을 인스턴스변수 value가 참조하게 된다.

1
2
3
4
public final class StringBuffer implements java.io.Serializable {
private char[] value;
...
}

StringBuffer의 생성자
StringBuffer 클래스의 인스턴스를 생성할 때, 적절한 길이의 char형 배열이 생성되고, 이 배열은 문자열을 저장하고 편집하기 위한 공간(buffer)으로 사용된다.

StringBuffer 인스턴스를 생성할 때는 생성자 StringBuffer(int length)를 사용해서 StringBuffer 인스턴스에 저장될 문자열의 길이를 고려하여 충분히 여유있는 크기로 지정하는 것이 좋다. StringBuffer 인스턴스를 생성할 때, 버퍼의 크기를 지정해 주지 않으면 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public StringBuffer(int length) {
value = new char[length];
shared = false;
}

public StringBuffer() {
this(16);
}

public StringBuffer(String str) {
this(str.length() + 16);
append(str);
}

StringBuffer 인스턴스로 문자열을 다루는 작업을 할 때, 버퍼의 크기가 작업하려는 문자열의 길이보다 작을 때는 내부적으로 버퍼의 크기를 증가시키는 작업이 수행된다.

배열의 길이는 변경될 수 없으므로 새로운 길이의 배열을 생성한 후에 이전 배열의 값을 복사해야 한다.

StringBuffer의 비교
String 클래스에서는 equals메서드를 오버라이딩해서 문자열의 내용을 비교하도록 구현되어 있지만, StringBuffer 클래스는 equals메서드를 오버라이딩하지 않아서 StringBuffer클래스의 equals메서드를 사용해도 등가비교연산자(==)로 비교한 것과 같은 결과를 얻는다.

1
2
3
4
5
StringBuffer sb = new StringBuffer("abc");
StringBuffer sb2 = new StringBuffer("abc");

System.out.println(sb == sb2); // false
System.out.println(sb.equals(sb2)); // false

반면에 toString()은 오버라이딩되어 있어서 StringBuffer 인스턴스에 toString()을 호출하면, 담고있는 문자열을 String으로 변환한다.

그래서 StringBuffer 인스턴스에 담긴 문자열을 비교하기 위해서는 StringBuffer 인스턴스에 toString()을 호출해서 String 인스턴스를 얻은 다음, 여기에 equals 메서드를 사용해서 비교해야한다.

StringBuilder란?
StringBuffer는 멀티쓰레드에 안전(thread safe)하도록 동기화되어 있다. 멀티쓰레드로 작성된 프로그램이 아닌 경우, StringBuffer의 동기화는 불필요하게 성능만 떨어뜨리게 된다.

그래서 StringBuffer에서 쓰레드의 동기화만 뺀 StringBuilder가 새로 추가되었다.

래퍼(wrapper) 클래스

경우에 따라 기본형(primitive type) 변수도 어쩔 수 없이 객체로 다뤄야 하는 경우가 있다. 예를 들면, 매개변수로 객체를 요구할 때, 기본형 값이 아닌 객체로 저장해야할 때, 객체 간의 비교가 필요할 때 등등의 경우에는 기본형 값들을 객체로 변환하여 작업을 수행해야 한다.

이 때 사용되는 것이 래퍼(wrapper)클래스이다. 8개의 기본형을 대표하는 8개의 래퍼클래스가 있는데, 이 클래스들을 이용하면 기본형 값을 객체로 다룰 수 있다.

래퍼 클래스들은 객체생성 시에 생성자의 인자로 주어진 각 자료형에 알맞은 값을 내부적으로 저장하고 있으며, 이에 관련된 여러 메서드가 정의되어 있다.

래퍼 클래스들은 모두 equals()가 오버라이딩되어 있어서 주소값이 아닌 객체가 가지고 있는 값을 비교한다. 오토박싱이 된다고 해도 Integer객체에 비교연산자를 사용할 수 없다. 대신 compareTo()를 제공한다.

그리고 toString()도 오버라이딩되어 있어서 객체가 가지고 있는 값을 문자열로 변환하여 반환한다.

문자열을 숫자로 변환하기

1
2
3
int i = new Integer("100").intValue();
int i2 = Integer.parseInt("100");
Integer i3 = Integer.valueOf("100");

타입.parse타입(String s) 의 반환값이 기본형(primitive type)이고, 타입.valueOf()는 반환값이 래퍼 클래스 타입이라는 차이가 있다.

문자열 -> 기본형
int i = Integer.parseInt(“100”);

문자열 -> 래퍼 클래스
Integer i = Integer.valueIf(“100”);

JDK 1.5부터 도입된 ‘오토박싱(autoboxing)’ 기능 때문에 반환값이 기본형일 때와 래퍼 클래스일 때의 차이가 없어졌다. 그래서 그냥 구별없이 valueOf()를 쓰는 것도 괜찮은 방법이다. 단, 성능은 valueOf()가 조금 더 느리다.

오토박싱 & 언박싱

JDK 1.5 이전에는 기본형과 참조형 간의 연산이 불가능했기 때문에, 래퍼 클래스로 기본형을 객체로 만들어서 연산해야 했다.

그러나 이제는 기본형과 참조형 간의 덧셈이 가능하다. 자바 언어의 규칙이 바뀐 것은 아니고, 컴파일러가 자동으로 변환하는 코드를 넣어주기 때문이다.

기본형 값을 래퍼 클래스의 객체로 자동 변환해주는 것을 ‘오토박싱’이라고 하고, 반대로 변환하는 것을 ‘언박싱’이라고 한다.

1
2
3
4
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(10); // 오토박싱

int value = list.get(0); // 언박싱

ArrayList에 숫자를 저장하거나 꺼낼 때, 기본형 값을 래퍼클래스의 객체로 변환하지 않아도 되므로 편리하다.

유용한 클래스

java.util.Objects 클래스

Object 클래스의 보조 클래스로 Math 클래스처럼 모든 메서드가 ‘static’이다. 객체의 비교나 널 체크(null check)에 유용하다.

IsNull()은 해당 객체가 널인지 확인해서 null이면 true를 반환하고 아니면 false를 반환한다. nonNull()은 isNull()과 정확히 반대의 일을 한다.

1
2
static boolean isNull(object obj)
static boolean nonNull(object obj)

그리고 requireNonNull()은 해당 객체가 널이 아니어야 하는 경우에 사용한다. 만일 객체가 널이면, NullPointerException을 발생시킨다. 두 번째 매개변수로 지정하는 문자열은 예외의 메시지가 된다.

1
2
static <T> requireNonNull(T obj)
static <T> requireNonNull(T obj, String message)

Object 클래스에는 두 객체를 비교하는 메서드가 등가비교를 위한 equals()만 있고, 대소비교를 위한 compare()가 없는 것이 좀 아쉬웠다. 그래서인지 Objects에는 compare()가 추가되었다. compare()는 두 비교대상이 같으면 0, 크면 양수, 작으면 음수를 반환한다.

1
static int compare(Object a, Object b, Comparator c)

이 메서드는 a와 b 두 객체를 비교하는데, 두 객체를 비교하는데 사용할 비교 기준이 필요하다. 그 역할을 하는 것이 Comparator이다.

Objects 클래스의 equals()는 Object 클래스와는 달리, null 검사를 하지 않아도 된다. equals()의 내부에서 a와 b의 null 검사를 하기 때문에 따로 null 검사를 위한 조건식을 넣지 않아도 된다. 실제 메서드의 코드는 다음과 같다.

1
2
3
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}

a와 b가 모두 null인 경우에는 참을 반환한다는 점을 빼고는 특별한 것이 없다. deepEquals() 메서드는 객체를 재귀적으로 비교하기 때문에 다차원 배열의 비교도 가능하다.

java.util.Scanner 클래스

Scanner는 화면, 파일, 문자열과 같은 입력소스로부터 문자데이터를 읽어오는데 도움을 줄 목적으로 JDK 1.5부터 추가되었다. Scanner에는 다음과 같은 생성자를 지원하기 때문에 다양한 입력소스로부터 데이터를 읽을 수 있다.

1
2
3
4
5
6
Scanner(String source)
Scanner(File source)
Scanner(InputStream source)
Scanner(Readable source)
Scanner(ReadableByteChannel source)
Scanner(Path source) .. JDK 1.7부터 추가

java.util.StringTokenizer 클래스

StringTokenizer는 긴 문자열을 지정한 구분자(delimiter)를 기준으로 토큰(token)이라는 여러 개의 문자열로 잘라내는 데 사용된다. StringTokenizer를 이용하는 방법 이외에도 String의 split(String reges)이나 Scanner의 useDelimiter(Spring pattern)를 사용할 수도 있다.

위의 이 두 가지 방법은 정규식 표현(Regular expression)을 사용해야 하므로 정규식 표현에 익숙하지 않은 경우 StringTokenizer를 사용하는 것이 간단하면서도 명확한 결과를 얻을 수 있다.

그러나 StirngTokenizer는 구분자로 단 하나의 문자열 밖에 사용하지 못하기 때문에 복잡한 형태의 구분자로 문자열을 나누어야 하는 경우에는 정규식을 사용하는 메서드를 사용해야 한다.

split()은 빈 문자열도 토큰으로 인식하는 반면 StringTokenizer는 빈 문자열을 토큰으로 인식하지 않기 때문에 인식하는 토큰의 개수가 서로 다른 것을 알 수 있다.

이 외에도 성능의 차이가 있는데, split()은 데이터를 토큰으로 잘라낸 결과를 배열에 담아서 반환하기 때문에 데이터를 토큰으로 바로바로 잘라서 반환하는 StringTokenizer보다 성능이 떨어질 수밖에 없다. 그러나 데이터의 양이 많은 경우가 아니라면 별 문제가 되지 않으므로 크게 신경 쓸 부분은 아니다.

java.math.BigInteger 클래스

정수형으로 표현할 수 있는 값의 한계가 있다. 가장 큰 정수형 타입인 long으로 표현할 수 있는 값은 10진수로 19자리 정도이다. 이 값도 상당히 큰 값이지만, 과학적 계산에서는 더 큰값을 다뤄야할 때가 있다. 그럴 때 사용하면 좋은 것이 BigIntenger이다.

BigInteger는 내부적으로 int배열을 사용해서 값을 다룬다. 그래서 long 타입보다 훨씬 큰 값을 다룰 수 있는 것이다. 대신 성능은 long 타입보다 떨어질 수밖에 없다.

1
2
final int signum; // 부호. 1(양수), 0, -1(음수) 셋 중의 하나
final int[] mag; // 값

BigIntenger는 String처럼 불변(immutable)이다. 그리고 모든 정수형이 그렇듯이 BigInteger 역시 값을 ‘2의 보수’의 형태로 표현한다.

위의 코드에서 알 수 있듯이 부호를 따로 저장하고 배열에는 값 자체만 저장한다. 그래서 signum의 값이 -1, 즉 음수인 경우, 2의 보수법에 맞게 mag의 값을 변환해서 처리한다. 그래서 부호만 다른 두 값의 mag의 값을 변환해서 처리한다. 그래서 부호만 다른 두 값의 mag는 같고 signum은 다르다.

BigInteger는 불변이므로, 반환타입이 BigInteger이란 얘기는 새로운 인스턴스가 반환된다는 뜻이다.

비트 연산 메서드
워낙 큰 숫자를 다루기 위한 클래스이므로, 성능을 향상시키기 위해 비트단위로 연산을 수행하는 메서드들을 많이 갖고 있다. 따라서 가능하면 산술연산 대신 비트연산으로 처리하도록 노력해야 한다.

java.math.BigDecimal 클래스

double 타입으로 표현할 수 있는 값은 상당히 범위가 넓지만, 정밀도가 최대 13자리 밖에 되지 않고 실수형의 특성상 오차를 피할 수 없다. BigDecimal은 실수형과 달리 정수를 이용해서 실수를 표현한다. 실수의 오차는 10진 실수를 2진 실수로 정확히 변환할 수 없는 경우가 있기 때문에 발생하는 것이므로, 오차가 없는 2진 정수로 변환하여 다루는 것이다.

참고

자주 보지 않으면 잊게되는 Java 기초 정리

매번 공부해도 너무 당연하다는 듯이 넘어가 잊게되거나, 기억해 두면 좋을것 같은 기초적인 문법과 개념들을 간단히 정리해보겠습니다.

변수

덧셈 연산자(+)는 피연산자가 모두 숫자일 때는 두 수를 더하지만, 피연산자 중 어느 한쪽이 String이면 나머지 한 쪽을 먼저 String으로 변환한 다음 두 String을 결합한다.

참조변수의 출력이나 덧셈연산자를 이용한 참조변수와 문자열의 결합에는 toString()이 자동적으로 호출되어 참조변수를 문자열로 대치한 후 처리한다.

연산자

  • x << 2 + 1 : 쉬프트연산자(<<)는 덧셈연산자보다 우선순위가 낮다. 그래서 해당 식은 ‘x << (2 + 1)’ 과 같다.
  • data & 0xFF == 0 : 논리연산자(&)는 비교연산자(==)보다 우선순위가 낮으므로 비교연산 후에 논리연산이 수행된다. 그래서 해당 식은 ‘data & (0xFF == 0)’ 과 같다.
  • x < -1 || x > 3 && x < 5 : 논리연산자 중에서 AND를 의미하는 ‘&&’가 OR을 의미하는 ‘||’보다 우선순위가 높다. 그래서 해당 식은 ‘x < -1 || (x > 3 && x < 5)’ 과 같다.

산술 변환 : 연산 전에 피연산자 타입의 일치를 위해 자동 형변환되는 것. 이 변환은 이항 연산에서 뿐만 아니라 단항 연산에서도 일어난다. ‘산술 변환’의 규칙은 다음과 같다.

  1. 두 피연산자의 타입을 같게 일치시킨다. (보다 큰 타입으로 일치)
    • long + int -> long + long -> long
    • float + int -> float + float -> float
  2. 피연산자의 타입이 int보다 작은 타입이면 int로 변환된다.
    • byte + short -> int + int -> int
    • char + short -> int + int -> int

첫 번째 규칙은 자동 형변환처럼 피연산자의 값손실을 최소화하기 위한 것이고, 두 번째 규칙은 정수형의 기본 타입인 int가 가장 효율적으로 처리할 수 있는 타입이기 때문에, 그리고 int보다 작은 타입, 예를 들면 char나 short의 표현범위가 좁아서 연산중에 오버플로우(overflow)가 발생할 가능성이 높기 때문에 만들어진 것이다.

1
2
3
4
5
6
7
8
long a = 1000000 * 1000000;
long b = 1000000 * 1000000L;
System.out.println("a="+a);
System.out.println("b="+b);

// 결과
a = -727379968
b = 1000000000000

위의 예제에서 ‘1000000 * 1000000’의 결과가 1000000000000(2*10의 12승)임에도 불구하고, -727379968라는 결과가 출력되었다. 그 이유는 int 타입과 int 타입의 연산 결과는 int 타입인데, 연산결과가 int 타입의 최대값인 1000000000(2*10의 9승)을 넘으므로 오버플로우(overflow)가 발생했기 때문이다. 이미 오버플로우가 발생한 값을 아무리 long 타입의 변수에 저장해도 소용이 없다.

1
2
3
4
5
6
7
8
9
char c1 = 'a';      // c1에는 문자 'a'의 코드값은 97이 저장된다.
char c2 = c1; // c1에 저장되어 있는 값이 c2에 저장된다.
char c3 = ' '; // c3를 공백으로 초기화 한다.

int i = c1 +1; // 'a' + 1 -> 97 + 1 -> 98

c3 = (char)(c1 + 1);
c2++;
c2++;

위의 예제에서 c2++; 대신에 c2=c2+1;을 사용하면 에러가 발생할 것이다. c2+1의 연산결과는 int형이며, 그 결과를 다시 c2에 담으려면 형변환 연산자를 사용하여 char형으로 형변환해야 하기 때문이다.

1
2
3
char c1 = 'a';
char c2 = c1 + 1; // 컴파일 에러 발생 O
char c2 = 'a' + 1; // 컴파일 에러 발생 X

‘a’ + 1은 리터럴 간의 연산이기 때문에 에러가 발생하지 않는다. 상수 또는 리터럴 간의 연산은 실행과정동안 변하는 값이 아니기 때문에, 컴파일 시에 컴파일러가 계산해서 그 결과로 대체함으로써 코드를 보다 효율적으로 만든다. 컴파일러가 미리 덧셈연산을 수행하기 때문에 실행 시에는 덧셈 연산이 수행되지 않는다. 수식에 변수가 들어가 있는 경우에는 컴파일러가 미리 계산을 할 수 없기 때문에 형변환을 해줘야한다. (char c2 = (char) (c1 + 1)) 그렇지 않으면 컴파일 에러가 발생한다.

1
2
float f = 0.1f;         // f에 0.10000000149011612로 저장된다.
double d = 0.1; // d에 0.10000000000000001로 저장된다.

float 타입의 값을 double 타입으로 형변환하면, 부호와 지수는 달라지지 않고 그저 기수의 빈자리를 0으로 채울 뿐이므로 0.1f를 double타입으로 형변환해도 그 값은 전혀 달라지지 않는다. 즉, float 타입의 값을 정밀도가 더 높은 double 타입으로 형변환했다고 해서 오차가 적어지는 것이 아니라는 말이다.

1
2
3
4
5
6
7
8
9
10
String str1 = "abc";
String str2 = new String("abc");

"abc" == "abc" ? true
str1 == "abc" ? true
str2 == "abc" ? false
str1.equals("abc") ? true
str2.equals("abc") ? true
str2.equals("ABC") ? false
str2.equalsIgnoreCase("ABC") ? true

str2와 “abc”의 내용이 같은데도 ‘==’로 비교하면, false를 결과로 얻는다. 내용은 같지만 서로 다른 객체이기 때문이다. 그러나 equals()는 객체가 달라도 내용이 같으면 true를 반환한다. 그래서 문자열을 비교할 때는 항상 equals()를 사용해야 한다.

  • 효율적인 연산
    • OR 연산 ‘||’의 경우, 두 피연산자 중 어느 한 쪽만 ‘참’이어도 전체 연산결과가 ‘참’이므로 좌측 피연산자가 ‘true(참)’이면, 우측 피연산자의 값은 평가하지 않는다.
    • AND 연산 ‘&&’의 경우, 어느 한쪽만 ‘거짓(0)’이어도 전체 연산결과가 ‘거짓(0)’이므로 좌측 피연산자가 ‘거짓(0)’이면, 우측 피연산자의 값은 평가하지 않는다.

비트 XOR 연산자 ‘^’는 두 피연산자의 비트가 다를 때만 1이 된다. 그리고 같은 값으로 두고 XOR 연산을 수행하면 원래의 값으로 돌아온다는 특징이 있어서 간단한 암호화에 사용된다.

비트 전환 연산자는 피연산자의 타입이 int보다 작으면 int로 자동 형변환(산술 변환) 후에 연산하기 때문에 연산결과는 32자리의 2진수이다.

쉬프트 연산자의 좌측 피연산자는 산술변환이 적용되어 int보다 작은 타입은 int타입으로 자동 변환되고 연산결과 역시 int타입이 된다. 그러나 쉬프트 연산자는 다른 이항연산자들과 달리 피연산자의 타입을 일치시킬 필요가 없기 때문에 우측 피연산자에는 산술변환이 적용되지 않는다.

변수 앞에 키워드 ‘final’을 붙이면 상수가 된다. 상수는 반드시 선언과 동시에 값을 저장해야하며, 한 번 저장된 값은 바꿀 수 없다.

조건문과 반복문

JDK 1.5부터 배열과 컬렉션에 저장된 요소에 접근할 때 기존보다 편리한 방법으로 처리할 수 있도록 for문의 새로운 문법이 추가되었다.

1
2
3
for (타입 변수명 : 배열 또는 컬렉션) {
// 반복할 문장
}

위의 문장에서 타입은 배열 또는 컬렉션의 요소의 타입이어야 한다. 배열 또는 컬렉션에 저장된 값이 매 반복마다 하나씩 순서대로 읽혀서 변수에 저장된다. 그러나 향상된 for문은 일반적인 for문과 달리 배열이나 컬렉션에 저장된 요소들을 읽어오는 용도로만 사용 수 있다는 제약이 있다.

반복문은 그저 같은 문장을 반복해서 수행하는 것이지만, 메서드를 호출하는 것은 반복문 보다 몇 가지 과정, 예를 들면 매개변수 복사와 종료 후 복귀할 주소저장 등, 이 추가로 필요하기 때문에 반복문보다 재귀호출의 수행시간이 더 오래 걸린다.

배열

배열은 같은 타입의 여러 변수를 하나의 묶음으로 다루는 것이다.
배열을 선언하는 것은 단지 생성된 배열을 다루기 위한 참조변수를 위한 공간이 만들어질 뿐이고, 배열을 생성해야만 비로소 값을 저장할 수 있는 공간이 만들어지는 것이다.

Java에서는 배열의 길이가 0일 수도 있다.

배열이름.length - 자바에서는 JVM이 모든 배열의 길이를 별도로 관리하며, ‘배열이름.length’를 통해서 배열의 길이에 대한 정보를 얻을 수 있다. ‘배열이름.length’는 상수이다.

자바에서는 다음과 같이 배열을 간단히 초기화 할 수 있는 방법을 제공한다.

1
2
int[] score1 = new int[]{50, 60, 70, 80, 90}; // 배열의 생성과 초기화를 동시에
int[] score2 = {50, 60, 70, 80, 90} // new int[]를 생략할 수 있음

그러나 배열의 선언과 생성을 따로 하는 경우에는 생략할 수 없다.

1
2
3
int [] score;
score = new int[]{50, 60, 70, 80, 90}; // OK
score = {50, 60, 70, 80, 90}; // 에러. new int[]를 생략할 수 없음

만약 score의 값을 바로 출력하면 어떻게 될까? 타입@주소의 형식으로 출력된다. ‘[I’는 1차원 int 배열이라는 의미이고, ‘@’뒤에 나오는 16진수는 배열의 주소인데 실제 주소가 아닌 내부 주소이다.

1
2
// 배열을 가리키는 참조변수 score의 값을 출력
System.out.println(score); // [I@14318bb와 같은 형식의 문자열이 출력된다.

예외적으로 char배열은 println메서드로 출력하면 각 요소가 구분자없이 그대로 출력되는데, 이것은 println메서드가 char배열일 때만 이렇게 동작하도록 작성되었기 때문이다.

for문 대신 System클래스의 arraycopy()를 사용하면 보다 간단하고 빠르게 배열을 복사할 수 있다.

자바에서 char배열이 아닌 String클래스를 이용해서 문자열을 처리하는 이유는 String클래스가 char배열에 여러 가지 기능을 추가하여 확장한 것이기 때문이다.
char배열과 String클래스의 한 가지 중요한 차이는, String객체(문자열)는 읽을 수만 있을 뿐 내용을 변경할 수 없다. (변경 가능한 문자열을 다루려면, StringBuffer 클래스를 사용하면 된다.)

JVM의 메모리구조

JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

  • 메서드 영역 (method area)
    프로그램 실행 중 어떤 클래스가 사용되면, JVM은 해당 클래스의 클래스파일(*.class)을 읽어서 분석하여 클래스에 대한 정보(클래스 데이터)를 이곳에 저장한다. 그 클래스의 클래스변수도 이 영역에 함께 생성된다.
  • 힙 (heap)
    인스턴스가 생성되는 공간. 프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 즉, 인스턴스 변수들이 생성되는 곳이다.
  • 호출스택 (call stack or execution stack)
    호출스택은 메서드의 작업에 필요한 메모리 공간을 제공한다. 메서드가 호출되면, 호출스택에 호출된 메서드를 위한 메모리가 할당되고, 이 메모리는 메서드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다. 메서드가 작업을 마치면 할당되었던 메모리공간은 반환되어 비워진다.

클래스

인스턴스 메서드는 인스턴스 변수와 관련된 작업을 하는, 즉 메서드의 작업을 수행하는데 인스턴스 변수를 필요로 하는 메서드이다. 반면에 인스턴스와 관계없는(인스턴스 변수나 인스턴스 메서드를 사용하지 않는) 메서드를 클래스 메서드(static 메서드)로 정의한다.

가변인자

기존에는 메서드의 매개변수 개수가 고정적이었으나 JDK1.5부터 동적으로 지정해 줄 수 있게 되었으며, 이 기능을 가변인자라고 한다. 가변인자는 **’타입… 변수명’**과 같은 형식으로 선언한다. PrintStream클래스의 printf()가 대표적인 예이다.

1
public PrintStream printf(String format, Object... args); { ... }

가변인자 외에도 매개변수가 더 있으면, 가변인자를 매개변수 중에서 제일 마지막에 선언해야 한다. 그렇지 않으면, 컴파일 에러가 발생한다.

가변인자는 내부적으로 배열을 이용한다. 그래서 가변인자가 선언된 메서드를 호출할 때마다 배열이 새로 생성된다. 가변인자가 편리하지만, 이런 비효율이 숨어있기 때문에 꼭 필요한 경우에만 사용해야한다.

가변인자를 사용한 메서드를 호출할 때는 인자가 아예 없어도 되고 배열을 사용할 수도 있다. (C언어와 달리 자바에서는 길이가 0인 배열을 생성하는 것이 허용된다.)

생성자

컴파일러가 자동적으로 기본 생성자를 추가해주는 경우는 ‘클래스 내에 생성자가 하나도 없을 때’ 뿐이다.

생성자에서 다른 생성자를 호출할 때에는 생성자의 이름으로 클래스이름 대신 this를 사용한다. ‘this’는 참조변수로 인스턴스 자신을 가리킨다. 또한 한 생성자에서 다른 생성자를 호출할 때는 반드시 첫 줄에서만 호출이 가능하다.

  • this : 인스턴스 자신을 가리키는 참조변수, 인스턴스의 주소가 저장되어 있다. 모든 인스턴스 메서드에 지역변수에 숨겨진 채로 존재한다.
  • this(), this(매개변수) : 생성자 같은 클래스의 다른 생성자를 호출할 때 사용한다.

변수의 초기화

멤버변수는 초기화를 하지 않아도 자동적으로 변수의 자료형에 맞는 기본갑으로 초기화가 이루어지므로 초기화하지 않고 사용해도 되지만, 지역변수는 사용하기 전에 반드시 초기화해야 한다. (멤버변수(클래스변수와 인스턴스변수)와 배열의 초기화는 선택적이지만, 지역변수의 초기화는 필수적이다.)

멤버변수의 초기화는 지역변수와 달리 여러 가지 방법이 있다.

  • 명시적 초기화 : 변수를 선언과 동시에 초기화하는 것
  • 생성자
  • 초기화 블럭
    • 인스턴스 초기화 블럭 : 인스턴스변수를 초기화 하는데 사용
    • 클래스 초기화 블럭 : 클래스 변수를 초기화 하는데 사용
1
2
3
4
5
6
7
class testClass() {
static { /* 클래스 초기화 블럭 */ }

{ /* 인스턴스 초기화 블럭 */ }

// ...
}

클래스 초기화 블럭은 클래스가 메모리에 처음 로딩될 때 한번만 수행되며, 인스턴스 초기화 블럭은 생성자와 같이 인스턴스를 생성할 때 마다 수행된다. (생성자보다 인스턴스 초기화 블럭이 먼저 수행된다.)

인스턴스 변수의 초기화는 주로 생성자를 사용하고, 인스턴스 초기화 블럭은 모든 생성자에서 공통으로 수행돼야 하는 코드를 넣는데 사용한다.

  • 클래스변수의 초기화 시점 : 클래스가 처음 로딩될 때 단 한번 초기화 된다.
  • 인스턴스변수의 초기화 시점 : 인스턴스가 생성될 때마다 각 인스턴스별로 초기화가 이루어진다.
  • 클래스변수의 초기화 순서 : 기본값 -> 명시적초기화 -> 클래스 초기화 블럭
  • 인스턴스변수의 초기화 순서 : 기본값 -> 명시적초기화 -> 인스턴스 초기화 블럭 -> 생성자

프로그램 실행도중 클래스에 대한 정보가 요구될 때, 클래스에 로딩된다. 하지만, 해당 클래스가 이미 메모리에 로딩되어 있다면, 또 다시 로딩하지 않는다.

클래스의 로딩 시기는 JVM의 종류에 따라 좀 다를 수 있는데, 클래스가 필요할 때 바로 메모리에 로딩하도록 설계가 되어있는 것도 있고, 실행효율을 높이기 위해서 사용될 클래스들을 프로그램이 시작될 때 미리 로딩하도록 되어있는 것도 있다.

상속

생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.

오버라이딩의 조건

오버라이딩시 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있다.

  • 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
    접근범위는 public, protected, (default), private이다.
  • 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않으면 컴파일러는 생성자의 첫 줄에 ‘super();’를 자동적으로 추가한다.

어떤 클래스의 인스턴스를 생성하면, 클래스 상속관계의 최고조상인 Object 클래스까지 거슬러 올라가면서 모든 조상클래스의 생성자가 순서대로 호출된다.

package와 import

패키지(package)

패키지란, 클래스의 묶음이다. 클래스의 실제 이름은 패키지명을 포함한 것이다.

클래스가 물리적으로 하나의 클래스파일(.class)인 것과 같이 패키지는 물리적으로 하나의 디렉토리이다. 디렉토리가 하위 디렉토리를 가질 수 있는 것처럼, 패키지도 다른 패키지를 포함할 수 있으며 점’.’으로 구분한다.

패키지 선언문은 반드시 소스파일에서 주석과 공백을 제외한 첫 번째 문장이어야 하며, 하나의 소스파일에 단 한번만 선언될 수 있다.

소스파일에 자신이 속할 패키지를 지정하지 않은 클래스는 자동적으로 ‘이름 없는 패키지’에 속하게 된다. 결국 패키지를 지정하지 않는 모든 클래스들은 같은 패키지에 속한다.

클래스패스(classpath)는 컴파일러(javac.exe)나 JVM 등이 클래스의 위치를 찾는데 사용되는 경로이다.

import문

클래스의 코드를 작성하기 전에 import문으로 사용하고자 하는 클래스의 패키지를 미리 명시해주면 소스코드에 사용되는 클래스이름에서 패키지명은 생략할 수 있다.

import문의 역할은 컴파일러에게 소스파일에 사용된 클래스의 패키지에 대한 정보를 제공하는 것이다.

import문에서 클래스의 이름 대신 ‘*’을 사용하는 것이 하위 패키지의 클래스까지 포함하는 것은 아니라는 것이다.

import문으로 패키지를 지정하지 않으면 위와 같이 모든 클래스이름 앞에 패키지명을 반드시 붙여야 한다. 단, 같은 패키지 내의 클래스들은 import문을 지정하지 않고도 패키지명을 생략할 수 있다.

제어자

생성자가 private인 클래스는 다른 클래스의 조상이 될 수 없다. 그래서 클래스 앞에 final을 더 추가하여 상속할 수 없는 클래스라는 것을 알려야 한다.

제어자를 조합해서 사용할 때 주의해야 할 사항

  • 메서드에 static과 abstract를 함께 사용할 수 없다. (static 메서드는 몸통이 있는 메서드에만 사용할 수 있기 때문이다.)
  • 클래스에 abstract와 final을 동시에 사용할 수 없다.

참조변수

형변환

컴파일 시에는 참조변수간의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못한다. 그래서 컴파일 시에는 문제가 없었지만, 실행 시에는 에러가 발생하여 실행이 비정상적으로 종료될 수 있다.

참조변수가 참좋고 있는 인스턴스의 실제 타입을 알아보기 위해 instance 연산자를 사용한다. 실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상타입의 instance 연산에도 true를 결과로 얻으며, instanceof의 연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무런 문제가 없다는 뜻이다.

참조변수와 인스턴스의 연결

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상 타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다. 하지만 중복 정의되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다.

인터페이스

인터페이스는 일종의 추상클래스이다. 인터페이스는 추상클래스처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높아서 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다. 오직 추상메서드와 상수만을 멤버로 가질 수 있다.

  • 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.
  • 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
    단, static 메서드와 디폴트 메서드는 예외

원래는 인터페이스의 모든 메서드는 추상메서드이어야 하는데, JDK1.8부터 인터페이스에 static 메서드와 디폴트 메서드의 추가를 허용하는 방향으로 변경되었다.

디폴트 메서드

조상 클래스에 새로운 메서드를 추가하는 것은 별 일이 아니지만, 인터페이스의 경우에는 보통 큰 일이 아니다. 인터페이스에 메서드를 추가한다는 것은, 추상 메서드를 추가한다는 것이고, 이 인터페이스를 구현한 기존의 모든 클래스들이 새로 추가된 메서드를 구현해야 하기 때문이다.

디폴트 메서드는 추상 메서드의 기본적인 구현을 제공하는 메서드로, 추상 메서드가 아니기 때문에 디폴트 메서드가 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.

디폴트 메서드는 메서드 앞에 키워드 default를 붙이며, 추상 메서드와 달리 일반 메서드처럼 몸통{}이 있어야 한다. 디폴트 메서드 역시 접근 제어자가 public 이며, 생략가능하다.

내부 클래스

내부 클래스는 클래스 내에 선언된 클래스이다. 클래스에 다른 클래스를 선언하는 이유는 간단하다. 두 클래스가 서로 긴밀한 관계에 있기 때문이다.

내부 클래스의 장점

  • 내부 클래스에서 외부 클래스의 멤버들을 쉽게 접근할 수 있다.
  • 코드의 복잡성을 줄일 수 있다(캡슐화).

내부 클래스의 종류와 특징

내부 클래스의 종류는 변수의 선언위치에 따른 종류와 같다. 내부 클래스는 마치 변수를 선언하는 것과 같은 위치에 선언할 수 있으며, 변수의 선언위치에 따라 인스턴스변수, 클래스변수(static 변수), 지역변수로 구분되는 것과 같이 내부 클래스도 선언위치에 따라 다음과 같이 구분된다.

  • 인스턴스 클래스 (instance class)
    외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 인스턴스 멤버처럼 다루어 진다. 주로 외부 클래스의 인스턴스멤버들과 관련된 작업에 사용될 목적으로 선언된다.
  • 스태틱 클래스 (static class)
    외부 클래스의 멤버변수 선언위치에 선언하며, 외부 클래스의 static 멤버처럼 다루어진다. 주로 외부 클래스의 static 멤버, 특히 static 메서드에서 사용될 목적으로 선언된다.
  • 지역 클래스 (local class)
    외부 클래스의 메서드나 초기화블럭 안에 선언하며, 선언된 영역 내부에서만 사용될 수 있다.
  • 익명 클래스 (anonymous class)
    클래스의 선언과 객체의 생성을 동시에 하는 이름없는 클래스(일회용)

지역 클래스(LocalInner)는 외부 클래스의 인스턴스멤버와 static 멤버를 모두 사용할 수 있으며, 지역 클래스가 포함된 메서드에 정의된 지역변수도 사용할 수 있다. 단, final이 붙은 지역변수만 접근가능한데 그 이유는 메서드가 수행을 마쳐서 지역변수가 소멸된 시점에도, 지역 클래스의 인스턴스가 소멸된 지역변수를 참조하려는 경우가 발생할 수 있기 때문이다.

JDK 1.8부터 지역 클래스에서 접근하는 지역 변수 앞에 final을 생략할 수 있게 바뀌었다. 대신 컴파일러가 자동으로 붙여준다. 즉, 편의상 final을 생략할 수 있게 한 것일 뿐 해당 변수의 값이 바뀌는 문장이 있으면 컴파일 에러가 발생한다.

내부 클래스는 컴파일 했을 때 생성되는 파일명은 외부 클래스명$내부 클래스명.class 형식으로 되어 있다.

익명 클래스 (anonymous class)

익명클래스는 특이하게도 다른 내부 클래스들과는 달리 이름이 없다. 클래스의 선언과 객체의 생성을 동시에 하기 때문에 단 한번만 사용될 수 있고 오직 하나의 객체만을 생성할 수 있는 일회용 클래스이다.

1
2
3
4
5
6
7
new 조상클래스이름() {
// 멤버 선언
}
또는
new 구현인터페이스이름() {
// 멤버 선언
}

이름이 없기 때문에 생성자도 가질 수 없으며, 조상클래스의 이름이나 구현하고자 하는 인터페이스의 이름을 사용해서 정의하기 때문에 하나의 클래스로 상속받는 동시에 인터페이스를 구현하거나 둘 이상의 인터페이스를 구현할 수 있다. 오로지 단 하나의 클래스를 상속받거나 단 하나의 인터페이스만을 구현할 수 있다.

익명 클래스는 이름이 없기 때문에 외부 클래스명$숫자.class의 형식으로 클래스파일명이 결정된다.

예외처리

프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우가 있다. 이러한 결과를 초래하는 원인을 프로그램 에러 또는 오류라고 한다.

이를 발생시점에 따라 컴파일 에러런타임 에러로 나눌 수 있다.

컴파일 에러 - 컴파일 시에 발생하는 에러

런타임 에러 - 실행 시에 발생하는 에러

논리적 에러 - 실행은 되지만, 의도와 다르게 동작하는 것

컴파일은 잘되었어도 실행 중에 에러에 의해서 잘못된 결과를 얻거나 프로그램이 비정상적으로 종료될 수 있다.

자바에서는 실행 시(runtime) 발생할 수 있는 프로그램 오류를 에러(error)예외(exception) 두 가지로 구분하였다.

에러는 메모리 부족(OutOfMemoryError)이나 스택오버플로우(StackOverflowError)와 같이 일단 발생하면 복구할 수 없는 심각한 오류이고, 예외는 발생하더라도 수습될 수 있는 비교적 덜 심각한 것이다.

모든 예외의 최고 조상은 Exception 클래스이다.

예외 처리하기 - try-catch 문

예외처리의

정의 - 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것

목적 - 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것

에러와 예외는 모두 실행 시(runtime) 발생하는 오류이다.

발생한 예외를 처리하지 못하면, 프로그램은 비정상적으로 종료되며, 처리되지 못한 예외(uncaught exception)는 JVM의 ‘예외처리기(UncaughtExceptionHandler)’가 받아서 예외의 원인을 화면에 출력한다.

예외를 처리하기 위해서는 try-catch문을 사용한다. 하나의 try 블럭 다음에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한 개의 catch 블럭만 수행된다.

try 블럭 또는 catch 블럭에 또 다른 try-catch 문이 포함될 수 있다. catch 블럭 내의 코드에서도 예외가 발생할 수 있기 때문이다. catch 블럭 내에 또 하나의 try-catch 문이 포함된 경우, 같은 이름의 참조변수를 사용해서는 안 된다.

정수는 0으로 나누는 것이 금지되어 있지만, 실수를 0으로 나누는 것은 금지되어있지 않으며 예외가 발생하지 않는다.

try 블럭에서 예외가 발생하면, 예외가 발생한 위치 이후에 있는 try 블럭의 문장들은 수행되지 않으므로, try 블럭에 포함시킬 코드의 범위를 잘 선택해야 한다.

예외가 발생한 문장이 try-catch 블럭부터 차례로 내려가면서 catch 블럭의 괄호()내에 선언된 참조변수의 종류와 생성된 예외클래스의 인스턴스에 instanceof 연산자를 이용해서 검사하게 되는데, 검사 결과가 true인 catch 블럭을 만날 때까지 검사는 계속된다.

printStackTrace()와 getMessage()

예외가 발생했을 때 생성되는 예외 클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨져 있으며, getMessage()와 printStackTrace()를 통해서 이 정보들을 얻을 수 있다.

printStackTrace() - 예외발생 당시의 호출스택(Call Stack)에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.

getMessage() - 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.

printStackTrace(PrintStream s) 또는 printStackTrace(PrintWriter s)를 사용하면 발생한 예외에 대한 정보를 파일에 저장할 수도 있다.

멀티 catch 블럭

JDK 1.7부터 여러 catch 블럭을 ‘|’ 기호를 이용해서, 하나의 catch 블럭으로 합칠 수 있게되었으며, 이를 ‘멀티 catch 블럭’이라 한다. (멀티 catch 블럭에 사용되는 ‘|’는 논리 연산자가 아니라 기호이다.)

만일 멀티 catch 블럭의 ‘|’ 기호로 연결된 예외 클래스가 조상과 자손의 관계에 있다면 컴파일 에러가 발생한다. (그냥 조상 클래스만 써주는 것과 똑같기 때문이다.)

예외 발생시키기

키워드 throw를 사용해서 프로그래머가 고의로 예외를 발생시킬 수 있다.

  1. 먼저, 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
  2. 키워드 throw를 이용해서 예외를 발생시킨다.

Exception 인스턴스를 생성할 때, 생성자에 String을 넣어 주면, 이 String이 Exception 인스턴스에 메시지로 저장된다. 이 메시지는 getMessage()를 이용해서 얻을 수 있다.

RuntimeException을 발생시키는 코드는 이에 대한 예외 처리를 하지 않았음에도 불구하고 성공적으로 컴파일 된다. RuntimeException 클래스들과 그 자손 클래스에 해당하는 예외는 프로그래머가 실수로 발생하는 것들이기 때문에 예외처리를 강제하지 않는 것이다.

컴파일러가 예외처리를 확인하지 않는 RuntimeException 클래스들은 unchecked 예외라고 부르고, 예외처리를 확인하는 Exception 클래스들은 checked 예외라고 부른다.

Error와 그 자손도 unchecked 예외이다. try-catch 블럭으로 처리할 수 없기 때문이다.

메서드에 예외 선언하기

예외를 처리하는 방법에는 try-catch 문을 사용하는 것 외에, 예외를 메서드에 선언하는 방법이 있다. 메서드에 예외를 선언하려면, 메서드의 선언부에 키워드 throws를 사용해서 메서드 내에서 발생할 수 있는 예외를 적어주기만 하면 된다. (예외를 발생시키는 키워드 throw와 예외를 메서드에 선언할 때 쓰이는 throws를 구별해야 한다.)

메서드에 예외를 선언할 때 일반적으로 RuntimeException 클래스들은 적지 않는다.

예외를 메서드의 throws에 명시하는 것은 예외를 처리하는 것이 아니라, 자신(예외가 발생할 가능성이 있는 메서드)을 호출한 메서드에게 예외를 전달하여 예외처리를 떠맡기는 것이다.

예외를 전달받은 메서드가 또다시 자신을 호출한 메서드에게 전달할 수 있으며, 이런 식으로 계속 호출스택에 있는 메서드들을 따라 전달되다가 제일 마지막에 있는 main 메서드에서도 예외가 처리되지 않으면, main 메서드 마저 종료되어 프로그램이 전체가 종료된다.

finally 블럭

finally 블럭은 try-catch 문과 함께 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

try 블럭에서 return문이 실행되는 경우에도 finally 블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다. 마찬가지로 catch 블럭의 문장 수행 중에 return 문을 만나도 finally 블럭의 문장들은 수행된다.

자원 자동 반환 - try-with-resources문

JDK 1.7부터 try-with-resources문이라는 try-catch문의 변형이 새로 추가되었다.

try-with-resources문의 괄호()안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음에 catch 블럭 또는 finally 블럭이 수행된다.

try-with-resources문에 의해 자동으로 객체의 close()가 호출될 수 있으려면, 클래스가 AuthCloseable이라는 인터페이스를 구현한 것이어야만 한다.

참고