java의 Date

오늘도 어김없이 java와 관련된 이야기를 포스팅 하려한다. 요 근래 자주 java와 관련있는 포스팅만 하는 것 같다. 다른 것들도 해야 되는데.. 아무튼 오늘은 java의 날짜와 관련된 포스팅을 한다.

java의 날짜 관련API(Date, Calendar 기타 등) 설계부터가 잘못 되었고 구현조차 잘못된 클래스로 java의 가장 악명 높은 대표적인 클래스이다. 기존의 날짜 관련 API들은 문서없이 사용하기 쉽지 않고 일관성도 없는데다 알 수 없는 상수들을 남발한다. 한개씩 살펴보자.

이상한 월 상수

@Test
public void constantTest() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(2017, 6, 18);
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
  assertThat(format.format(calendar.getTime())).isEqualTo("2017-06-18");
}

위는 Calendar 클래스를 이용해서 오늘날짜인 2017.6.18일을 지정하였다. 누가 봐도 올바른 코드라고 생각할 듯 하다. 하지만 이상하게도 이 테스트는 통과하지 못한다. 왜나면 월을 0부터 시작했기 때문이다. 아무리 개발자라고 1월을 0으로 표시하는 건 좀 그렇다. 이 코드를 정상 작동 시키려면 아래와 같이 코드를 변경해야 한다.

calendar.set(2017, 5, 18);

그래서 IEDA에서는 경고를 보여준다. Calendar에 있는 상수를 사용하라는 경고인 듯 싶다. 월을 지정할 때는 Calendar의 상수를 가져와서 쓰는 것이 정신건강상 나을 듯하다.

calendar.set(2017, Calendar.JUNE, 18);

실질적으로 Calendar.JUNE 값은 5를 의미한다. 헷갈린다 헷갈려..

일관성 없는 요일 상수

생각하는 사람 나름이겠지만 요일을 상수로 지정하여 개발한다고 치자. 월요일부터 일요일까지의 숫자로 표현한다면 어떤게 가장 이상적일까? 필자와 생각이 다를 수도 있겠지만 필자 같은 경우에는 1을 월요일 7을 일요일로 지정했을 듯 하다. 물론 0을 일요일로 생각할 수도 있겠지만 필자생각엔 그닥 좋은 방법은 아니라고 생각된다.

@Test
public void constantTest1() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(2017, Calendar.JUNE, 18);
  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  assertThat(dayOfWeek).isEqualTo(7);
}

위의 코드는 요일을 상수로 가져오는 그런 코드이다. 여기서도 마음에 들지 않는 코드가 있다. 바로 Calendar.DAY_OF_WEEK 이 아이다. 저 아이도 7이라는 상수 값이다. 7은 뭘 의미할까? 요일이 7개라 7로 했나. 차라리 Enum 타입이였다면 좋았겠지만 저 클래스는 Enum 타입이 나오기전 아주아주 오래된 클래스라 그럴수 없었던 것으로 보인다.
아무튼 다시 돌아와 위 코드를 테스트를 해보자. 위의 테스트는 성공할까? 다들 알겠지만 위는 테스트를 실패한다. 음 그럼 아주 드물게 일요일을 0 으로 생각할 수도 있으니 0으로 코드를 바꾸어서 테스트를 해보자.

@Test
public void constantTest1() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(2017, Calendar.JUNE, 18);
  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  assertThat(dayOfWeek).isEqualTo(0);
}

0으로 바꾸어봤지만 또 다시 테스트에 실패한다. 이게 무슨 일인가.. 위 테스트를 성공시키려면 아래와 같이 변경해야 된다.

@Test
public void constantTest1() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(2017, Calendar.JUNE, 18);
  int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
  assertThat(dayOfWeek).isEqualTo(1);
}

위에서 말했다시피 2017년 6월 18일은 일요일인데 요일 상수는 1을 리턴한다. 그래 이것까지 넓고 넓은 우리 개발자들은 이해 할 수 있었다고 치자. 하지만 더 어이 없는건 다음에 있다.

@Test
public void constantTest2() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(2017, Calendar.JUNE, 18);
  assertThat(calendar.getTime().getDay()).isEqualTo(1);
}

위의 코드는 calendar를 Date로 반환하여 다시 요일을 테스트하는 그런 코드이다. 요일에 일요일이 1이라고 했으니 당연히 테스트를 통과 하겠지? 안타깝게 이 테스트는 다시 실패로 돌아간다. 그 이유는 Date의 getDay() 메서드는 우리가 생각했듯이 일요일을 0으로 판단하다. (하지만 필자는 7이 더 나은듯) 이렇게 날짜 관련 API의 일관성이 매우 떨어진다.
참고로 Date 에 getDay() Deprecated 메서드이다.

동등성 (equality)

자바 개발자라면 필독서인 effective java에는 이런 내용이 있다. equals 메서드 규약을 설명하는 내용이다. 5가지 정도 되니 잠깐 살펴보자.

  1. 반사성 : null이 아닌 참조 x가 있을때 x.equals(x)는 true를 반환한다. x.equals(x) = true
  2. 대칭성 : null이 아닌 참조 x와 y가 있을 때 x.equals(y)는 y.equals(x) 가 true일때만 true를 반환한다. x.equals(y) = true , y.equals(x) = true
  3. 추이성 : null이 아닌 참조 x,y,z가 있을 때 x.equals(y)가 true이고 x.equals(z)가 true면 x.equals(z)도 true이다. x.equals(y) = true, y.equals(z) = true, x.equals(z) = true
  4. 일관성 : null이 아닌 참조 x,y가 있을 때 equals 통해 비교되는 정보에 아무 변화가 없다면 호출 결과는 횟수에 상관 없이 항상 같아야 된다. x.equals(y) = true
  5. null 아닌 참조 x에 대하여 x.equals(null)은 항상 false이다. x.equals(null) = false

위의 언급한 5가지가 equals를 구현하는 메서드 규약이다. 일반적으로 상속관계에서 깨지기 쉽다. (그래서 상속이 그리 좋은 것만은 아니다. 상속보다는 조립을..) java의 Date와 Timestamp 클래스가 그 대표적인 예이다.

@Test
public void symmetryTest() {
  long now = System.currentTimeMillis();
  Timestamp timestamp = new Timestamp(now);
  Date date = new Date(now);
  assertThat(date.equals(timestamp)).isTrue();
  assertThat(timestamp.equals(date)).isTrue();
}

Date와 Timestamp 클래스는 상속 관계의 클래스이다. Timestamp 클래스의 상위 클래스가 Date 클래스가 이다. 그래서 비교를 했을경우 date.equals(timestamp)true 라면 timestamp.equals(date)true를 반환해야 한다. 하지만 위의 테스트 코드는 실패로 돌아간다. date.equals(timestamp)true로 리턴하지만 timestamp.equals(date) 경우에는 false를 리턴한다. 대칭성이 맞지 않아 equals 규약을 어긴 꼴이 된 셈이다. 둘다 true 이거나 둘다 false 이어야만 대칭성을 어기지 않는 셈이 되는 것이다.

불변이 아니다. (mutable)

가장 개발이 잘못 이루워진 부분이다. Date와 Calendar 이외에 날짜 관련 API들은 불변이 아니다. 불변이 아니기 때문에 멀티 스레드에 안전하지 못하다. 보통은 다른 언어의 경우에는 날짜와 돈 관련 API들은 모두 불변으로 개발 되어 있다. 하지만 java에서는 그렇지 못하다. effective java를 다시 언급하자면 저자인 Joshua Bloch도 Date 클래스는 불변이였어야 했다고 지적했다. 그래서 만약 필요 하다면 방어적 본사본을 만들라 라고 한다. 아래 코드처럼 말이다. 아래의 Period는 불변의 객체이다. 생성자를 호출할 경우에도 다시 Date를 생성하고 getter를 호출할 경우에도 다시 Date를 생성해서 만든다.

public class Period {
  private final Date start;
  private final Date end;
  public Period(Date start, Date end) {
    this.start = new Date(start.getTime());
    this.end = new Date(end.getTime());
  }

  public Date getStart() {
    return new Date(start.getTime());
  }

  public Date getEnd() {
    return new Date(end.getTime());
  }
}

위와 같이 경우에는 좀 더 안전한 클래스가 된다. 가능한 불변의 객체를 만드는 것이 가장 이상적이다.

SimpleDateFormat

java 의 Date 클래스를 특정 포맷에 맞게 변환해주는 아주 괜찮은 API이다. 사용하기도 쉽게 개발자가 원하는 포맷 그대로 변환해 줘서 더할 것도 없다. 하지만 여기도 문제점이 많다.

@Test
public void simpleDateFormatTest()  {
  Date date = new Date();
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  String now = format.format(date);
  try {
    format.parse(now);
  } catch (ParseException e) {
    e.printStackTrace();
  }
}

일단 첫 번째로 불필요한 checked exception을 사용한다. 굳이 저기에 checked exception 을 던질 필요가 있나 싶다. 그리고 제대로 되지 않는 값을 넘겨도 exception을 던지지 않는다. 그럼 뭐하러 checked exception으로 던지는지 의문이다. checked exception은 자제하는 것이 좋다. 아니 특별한 이유가 아니라면 쓰지 않는게 정신건강상 좋다.

@Test
public void simpleDateFormatTest()  {
  try {
    Date parse = format.parse("2012-18-22 33:11:37");
    System.out.println(parse);
  } catch (ParseException e) {
    e.printStackTrace();
  }
}

위 코드를 보긴엔 이상한 코드이다. 우리 달력에는 18월달이 없으므로 에러를 내뱉는다고 생각할 수도 있다. 그게 ParseException의 목적이 아닌가? 생각되는데 말이다. 하지만 이 코드는 정상 작동하며 실제로는 2013년 6월이 출력된다.

SimpleDateFormat 클래스의 두번째 문제인데 Date 클래스와 동일하다. 멀티 스레드에 안전하지 못하다. 가장 큰 문제가 아닌가 싶다. SimpleDateFormat 문서에는 아래와 같은 주의가 있다.

Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.

Date formats은 동기화되지 않는다. 각각의 스레드별로 인스턴스를 만드는 것이 낫다. 만약 multiple threads가 동시에 접근할 경우에는 외부에서 동기화 해야 한다. 라고 설명한다.

아래의 예를 살펴보자.

private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Test
public void threadNotSafeTest() {
  Runnable r[] = new Runnable[10];
  for (int i = 0; i < r.length; i++) {
    r[i] = () -> {
        String str = "2017-06-18 20:12:22";
        try {
          Date d = format.parse(str);
          String str2 = format.format(d);
        } catch (ParseException e) {
          e.printStackTrace();
        }
    };
    new Thread(r[i]).start();
  }
}

thread 10개를 생성하여 실행하였다. 하지만 이 코드는 아래와 같은 에러를 발생한다.

java.lang.NumberFormatException: multiple points

멀티 스레드에서 안전하지 못하다. 이 경우에는 외부에서 동기화 시켜줘야 한다.

private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Test
public void threadNotSafeTest() {
  Runnable r[] = new Runnable[10];
  for (int i = 0; i < r.length; i++) {
    r[i] = () -> {
      String str = "2017-06-18 20:12:22";
      synchronized (format) {
        try {
          Date d = format.parse(str);
          String str2 = format.format(d);
        } catch (ParseException e) {
          e.printStackTrace();
        }
      }
    };
    new Thread(r[i]).start();
  }
}

그레고리력

그레고리력은 현재 세계적으로 통용되는 양력으로, 1582년에 교황 그레고리오 13세가 율리우스력을 개정하여 이 역법을 시행했기 때문에 그레고리력이라고 부른다. 율리우스력의 1년 길이는 365.25일이므로 천문학의 회귀년 365.2422일보다 0.0078일(11분 14초)이 길어서 128년마다 1일의 편차가 났다. 위키 출처. 자세한 내용은 위키를 참고하길 바란다.

율리우스력에 의해 누적된 오차를 교정하기 위해 10일 정도를 건너 뛰었다. 그래서 1582년 10월 4일 다음일이 1582년 10월 15일이다.

@Test
public void gregorianCalendarTest() {
  Calendar calendar = Calendar.getInstance();
  calendar.set(1582, Calendar.OCTOBER , 4);
  calendar.add(Calendar.DATE, 1);
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
  String gregorian = format.format(calendar.getTime());
  assertThat(gregorian).isEqualTo("1582-10-05");
}

위와 같이 1582년 10월 4일 다음날을 계산하는 코드이다. 당연히 성공할 것 처럼 보이지만 테스트는 실패를 한다. 왜냐하면 java의 Date는 그레고리력을 이용해서 계산하기 때문에 1582년 10월 4일 다음날은 1582년 10월 15일이다. 그래서 위의 코드는 아래 코드처럼 변경해야 된다.

assertThat(gregorian).isEqualTo("1582-10-15");

java의 Date 클래스는 이렇게 많은 문제점들이 있다. 이 외에도 timeZone Id를 만들때도 이상한 값을 넣어도 에러를 내뱉지 않고 java.sql의 Date와 java.util의 Date가 클래스명이 같다(java.sql.Date는 java.util.Date를 상속 받고 있다.)는 이유로 이름을 지을때 깜빡 존 듯하다는 조롱까지 나왔다.

이렇게 java의 Date 클래스는 많은 문제가 있다. 되도록이면 사용하지 않는 것을 추천하고 java8에 나온 LocalDateTime이라는 클래스가 위의 모든 문제들을 해결했다고 해도 과언이 아니다. 만약 java8을 쓰지 못한다면 joda-time을 사용하길 권장한다. 물론 joda time 만드신 분이 오라클 주도하에 java8의 LocalDateTime을 만들어서 비슷한 구석이 많다. android 개발자들은 date4j를 사용하길 추천한다. joda-time 보다 가벼워서 android 개발자한테는 joda-time보다는 좀 더 낫지 않을까 생각된다.

이렇게 오늘 java의 Date 클래스의 문제점들을 살펴봤다.

자바의 제네릭 (Generic)

오늘은 자바의 제네릭과 관련해서 글을 써내려가려 한다. 자바의 제네릭은 어렵다. 필자가 생각하기엔 자바에서 가장 어려운 문법? 부분에 속한다고 할 수 있다. 누군가가 자바에서 가장 어려운게 무엇이냐고 물어보면 1초도 망설이지 않고 제네릭이라고 대답했을 것이다. 그만큼 나에겐 어렵다.
자바의 제네릭을 아주 자유자재로 사용할 수 있는 개발자는 많지 않을 것이라고 생각한다. (내가 못해서 그렇게 생각할지도..)

자바의 Generic은 처음 나왔을 때 부터 있었던 것은 아니다. 1996년에 자바가 처음 발표되고 8년이 지난 2004년에 java5가 발표되면서 Generic이 추가되었다. java5가 발표되면서 아주아주 많은 변화가 있었다. java8의 람다만큼 큰 변화가 많이 있었다. (어쩌면 람다보다 더..) 그 중에서 가장 대표적인 것들은 Generic, 오토박싱/언박싱, foreach, enum type, 가변인자(varargs), 어노테이션(Annotation) 등 무수히 많은 변화를 들고 나왔다. 그 때 당시에는 자바개발자들이 고생을 많이 했겠다. 구현한 사람이나 사용하는 사람이나..

제네릭 작성해보기

일단 간단하게 제네릭 타입인 List<E>를 작성해보자. 뭐 자주 사용하는 인터페이스니 예제를 남길 필요도 없을 듯 한데..

List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);

우리가 흔히 작성하는 코드이다. List의 <E>를 타입 파라미터라고 부른다. 이것이 있기에 우리는 컴파일 타임에 해당 타입이 일치하는지 확인 할 수 있다. 좀 더 안전하게 사용할 수 있게 되었다. 예전 자바5 이전에는 Generic이 존재 하지 않았기 때문에 아무 객체나 넣을 수 있었다.

List numbers = new ArrayList();
numbers.add(1);
numbers.add("2");
numbers.add(3);

위와 같이 말이다. 하지만 현재도 위와 같이 작성해도 문제 없이 컴파일이 되고 실행된다. 그 이유는 아래가서 설명하겠다.
그럼 한번 타입파라미터가 있는 클래스를 간단하게 만들어보자.

MyList

자바의 ArrayList를 참고해서 나만의 리스트를 만들어보자.

public class MyList<E> {
  private static final int DEFAULT_CAPACITY = 10;
  private Object element[];
  private int index;

  public MyList() {
    element = new Object[DEFAULT_CAPACITY];
  }

  public void add(E e) {
    this.element[index++] = e;
  }

  public E get(int index){
    return (E) element[index];
  }
}

아주 심플하게 나만의 리스트를 만들었다. 보기에도 심플하고 만들기도 심플하다. 물론 예외처리라든지 리스트의 길이 동적으로 변한다는지는 나중 문제지 여기서 핵심은 그게 아니라서 제외시켰다. 물론 알겠지만..
아무튼 우리는 자바의 Generic을 이용해서 클래스를 만들어 봤다. MyList라는 클래스는 타입 파라미터가 존재한다. 이는 MyList라는 클래스에 사용될 클래스를 제한한다는 뜻이다. add(E) 라는 메서드의 E는 클래스의 타입과 동일해야만 한다. 마찬가지로 get 메서드의 리턴타입도 E 로 정의해 두었으니 클래스의 타입과 동일해야 된다.
만약 String으로 정의했는데 int, long, double, pojo나 기타 다른 타입이 들어간다면 컴파일 타임에 에러를 발생시킨다.
한번 사용해보자.

MyList<String> myList = new MyList<>();
myList.add("wonwoo");
myList.add("seungwoo");
myList.add(1); //컴파일 에러
System.out.println(myList.get(0));
System.out.println(myList.get(1));

우리는 안전하게 해당 타입에 맞는 List를 만들수 있어서 보다 버그나 에러를 줄일 수 있게 되었다.

근데 코드를 자세히보면 뭔가 꺼림칙하다. element을 Object로 선언하고 (Object 선언은 봐줄수 있을 언정) get() 메서드를 보면 E 타입으로 캐스팅까지 한다. 뭔가 마음에 들지 않는다.

Type erasure

꺼림칙하니 코드를 바꾸어 보자.

public class MyList<E> {
  private static final int DEFAULT_CAPACITY = 10;
  private E element[];
  private int index;

  public MyList() {
    element = new E[DEFAULT_CAPACITY];
  }

  public void add(E e) {
    this.element[index++] = e;
  }

  public E get(int index){
    return element[index];
  }
}

Object 타입을 제거하고 E 타입으로 변경하였다. 그러고 나니 get() 메서드에 캐스팅하는 부분이 사라졌다. 깔끔하다. 하지만 안타깝게도 이코드는 동작하지 않는다. 아니 컴파일 조차 되지 않는다. (필자가 그렇게 한 이유가 다..) 컴파일 에러가 발생하는 부분은 다음과 같다.

element = new E[DEFAULT_CAPACITY];

어떤 에러인지 IDEA에서 살펴보면 Type Parameter 'E' cannot be instantiated directly 이와 같은 에러가 발생한다. 타입파라미터 E는 직접적으로 인스턴스화 할 수 없다. 엥 이건 또 무슨 말인가? 왜 타입파라미터는 인스턴화 할 수 없을까?
그 이유는 눈치빠른 사람들은 알겠지만 소 제목에 있다시피 type erasure 때문이다. 그렇다면 type erasure란 뭘까?

자바에서는 제네릭 클래스를 인스턴화화 할 때 해당 타입 타입을 지워버린다. 그 타입은 컴파일시까지만 존재하고 컴파일된 바이트코드에서는 어떠한 타입파라미터의 정보를 찾아볼 수 없다. 필자 말이 진짜인지 한번 살펴보자.

List<Integer> numbers = new ArrayList<>();

//기타 생략

아까 사용했던 List를 바이트 코드 레벨에서 한번 살펴보자.

L0
LINENUMBER 11 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1

//기타 생략

보시다시피 ArrayList를 생성할 때 어떠한 타입정보도 들고 있지 않다. new ArrayList()로 생성한 것과 동일하게 바이트 코드가 생성된다.
그럼 자바가 이렇게 한 이유는 무엇일까? 이유는 단 한개 밖에 없는 듯하다. (물론 다른 이유도 있을 수 있겠지만 필자 추측에는) 당시 특히나 자바는 하위 호환성을 매우 중요시했다. 그래서 위와 같은 결정을 내린듯 하다. 그래서 제네릭을 사용한 java5에서 컴파일된 코드를 java4에서도 실행 시킬수 있고 제네릭을 사용하지 않았던 레거시 코드들도 java5이상에서 무사히 잘 실행 될 수 있었던 이유이다.
그래서 우리는 아래와 같은 코드들를 만들 수 없다.

if(numbers instanceof List<Integer>) {

}

List<Integer>.class

타입파라미터와 Object

그럼 타입파라미터가 있는 클래스를 컴파일 해보면 어떨까? 어떠한 타입으로 변환을 시켜야 되는데 그 어떠한 타입은 무엇일까?
그건 바로 Object 타입으로 변경시킨다.

class Node<T> {
  private T data;
  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }
}

위와 같이 Node라는 클래스에 타입파라미터가 존재 한다고 가정하자. 그럼 자바 컴파일러는 아래와 같이 변경한다.

class Node {
  private Object data;
  public Object getData() {
    return data;
  }

  public void setData(Object data) {
    this.data = data;
  }
}

그렇다면 만약 타입파라미터에 범위를 제한한다면 어떨까? 아래와 같이 Comparable을 상속받은 클래스만 가능하다고 해보자.

class Node<T extends Comparable<T>> {

  private T data;

  public T getData() {
    return data;
  }

  public void setData(T data) {
    this.data = data;
  }
}

만약 위와 같이 작성된 코드라면 아래와 같이 변경 된다.

class Node {

  private Comparable data;

  public Comparable getData() {
    return data;
  }

  public void setData(Comparable data) {
    this.data = data;
  }
}

참 자바 컴파일러는 열심히도 일한다..

이렇게 오늘은 자바의 제네릭에 대해서 살펴봤다. 위와서 봤던 클래스에도 타입파라미터를 사용할 수 있지만 메서드에도 사용할 수 있다. 또한 바로 위에서 봤던 제네릭에 범위를 제한 할 수도 있다. 이건 예전에 글쓴게 있는데.. 어디 있더라..
제네릭 제한 여기에 보면 허접하지만 간단하게 제네릭 제한에 관련된 글이 있으니 참고 하면 되겠다.

위의 예제만 보고 쉽다고 생각하면 큰 오산일지 모른다. 인터넷에 많은 예제도 살펴보고 제네릭을 많이 사용한 라이브러리를 참고히면 더욱 많은 도움이 될 수 있을 듯 하다.

미리보는 java9

오늘은 미리보는 java9의 새로운 기능들을 살펴보자. 물론 지금은 릴리즈전이라 바뀔 내용이 있을 수 있으니 너무 깊게는 살펴보지 말자.

조만간 java9가 릴리즈 될 예정이다. 원래 일정은 올해 초에 릴리즈 될거라고 했었는데 일정이 밀렸다. 왜 밀린지는 모르겠지만.. 아무튼 담달 27일인 7월27일에 다시 릴리즈 예정일이다. 역시 또 밀릴지는 의문이다.
(수정) 또 다시 딜레이 되었다고 한다. 릴리즈 일정은 아래와 같다.

2017/07/06      Final Release Candidate
2017/09/21      General Availability

그전에 안타까운 소식이 하나 있다. java7부터 언급이 많이 되었던 직소(Jigsaw) 프로젝트가 JCP에 통과하지 못하는 일이 발생하였다. 대부분의 회사들이 반대표를 던졌다. oracle, intel , Azul Systems등이 찬성표를 던졌으나, 이클립스 재단, IBM, 트위터, 레드햇(개빈 킹이겠지?) 등 이 반대표를 던저 23개 회사중 13개의 회사가 반대를 하였다.
그래서 한달이내로 다시 리뷰를 받아야 되는데 해결할 일이 많다고 한다. 그 한달이 거의 다 된거 같은데.. 조만간 소식이 들릴듯 하다. 그래서 여기서는 직소(Jigsaw)는 언급하지 않겠다. 양도 꽤 있어서 만약 java9에 들어간다면 그때 다시..

Collections 의 팩토리 메서드

List에는 Arrays 클래스에 존재 했던거지만 List 인터페이스에 새로운 팩토리메서드가 추가로 생겼다.

List.of(1,2,3,4,5);

위와 같이 간단하게 팩토리 메서드를 호출해서 생성할 수 있다. 메서드 형태들는 다음과 같다.

static <E> List<E> of(E e1) 
static <E> List<E> of(E e1, E e2)
static <E> List<E> of(E e1, E e2, E e3)
...
static <E> List<E> of(E... elements)

List에만 생겼다면 그닥 의미가 별로 없었을 것이다. 하지만 Set, Map에도 추가 되었다.

Set.of(1,2,3,4,5);
Map.of("key1", "value1", "key2","value2");

Set의 경우에는 중복을 허용하지 않는다. 그래서 만약 같은 값을 넣을 경우에는 에러를 발생시킨다.

Set.of(1,2,3,4,1);

... java.lang.IllegalArgumentException: duplicate element: 1
...

또 한 Map도 키는 중복을 허용하지 않는다. 그래서 Set과 마찬가지로 같은 키값을 넣을 경우 에러를 발생시킨다.

Map.of("key1", "value1", "key1","value2");

참고로 위에 모든 컬렉션은 immutable하다. 변경할 수 없다. 만약 List.of()로 생성하고 add() 메서드를 호출하면 UnsupportedOperationException 이 발생한다.

좀 더 쉽게 Set과 Map을 사용할 수 있어서 괜찮은거 같다.

Stream API

Stream에 몇가지 API가 추가 되었다. takeWhile(), dropWhile(), ofNullable()가 추가 되었다.

takeWhile

takeWhile() 의 메서드 형태는 다음과 같다.

default Stream<T> takeWhile(Predicate<? super T> predicate)

이 아이는 특정한 엘리먼트까지 왔다면 멈추고 그 엘리먼트까지 반환한다. Predicate을 파라미터로 받으니 boolean 값을 리턴하면 된다.

List<Integer> numbers = List.of(1, 3, 7, 8, 15, 4)
        .stream()
        .takeWhile(i -> i < 10)
        .collect(toList());

filter와 유사하다. 하지만 filter와 다른점은 해당 조건까지 왔다면 멈추고 반환한다는 것이다. filter와 비교해보자.

List<Integer> numbers = List.of(1, 3, 7, 8, 15, 4)
        .stream()
        .filter(i -> i < 10)
        .collect(toList());

filter를 사용했을 경우에는 1, 3, 7, 8, 4를 반환하는 반면에 takeWhile를 사용할 경우에는 1, 3, 7, 8까지 데이터를 반환한다.

dropWhile

dropWhile 의 메서드 형태는 다음 과 같다.

default Stream<T> dropWhile(Predicate<? super T> predicate)

dropWhile() 메서드는 takeWhile() 메서드와 반대 개념이다. takeWhile()메서드가 1, 3, 7, 8를 반환하였다면 dropWhile() 메서드는 나머지인 15, 4를 반환한다.

List<Integer> numbers = List.of(1, 3, 7, 8, 15, 4)
        .stream()
        .dropWhile(i -> i < 10)
        .collect(toList());
//[15, 4]

ofNullable

ofNullable의 메서드 형태는 다음 과 같다.

public static<T> Stream<T> ofNullable(T t) 

ofNullable() 메서드는 Optional.ofNullable() 과 동일하다. null safe한 메서드 이다.

Stream.ofNullable(null)

위와 같이 작성해도 에러는 발생하지 않는다.

Optional API

Optional에도 몇가지 API가 추가 되었다. stream(), or(), ifPresentOrElse() 메서드가 추가되었다.

stream

메서드 명 그대로 Optional을 Stream 타입으로 변경하는 메서드 이다. 메서드 형태는 다음과 같다.

public Stream<T> stream()

Optional 타입을 만들어서 stream 형태로 만드는 예이다.

Optional<String> foo = Optional.ofNullable("foo");
Stream<String> stream = foo.stream();

or

or 메서드는 기존의 orXXX 와 비슷한 메서드이다. or 메서드 경우에는 Optional을 다시 리턴한다. 메서드 형태는 다음과 같다.

public Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

Supplier에 Optional 타입을 받고 Optional을 리턴한다.

Optional<String> foo = Optional.ofNullable("foo");
Optional<String> bar = foo.or(() -> Optional.of("bar"));
// Optional[foo]
Optional<String> foo = Optional.ofNullable(null);
Optional<String> bar = foo.or(() -> Optional.of("bar"));
// Optional[bar]

ifPresentOrElse

기존의 ifPresent(Consumer<? super T> action) 메서드 형태에서 조금 확장된 형태이다. ifPresent() 메서드 경우에는 값이 있을 경우에만 동작하지만 ifPresentOrElse() 메서드 경우에는 값이 없을 경우에도 동작하는 부분이 추가 되었다.
메서드 형태는 다음과 같다.

public void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)

Runnable을 파라마터로 받는 부분이 추가 되었다.

Optional<String> foo = Optional.ofNullable("foo");
foo.ifPresentOrElse(f -> System.out.println(f), () -> System.out.println("bar"));
// foo

Optional<String> foo = Optional.ofNullable(null);
foo.ifPresentOrElse(f -> System.out.println(f), () -> System.out.println("bar"));
// bar 

Process

ProcessHandle 인터페이스가 추가 되었다. Process관련된 정보들을 쉽게 가져올 수 있다.

ProcessHandle processHandle = ProcessHandle.current();
ProcessHandle.Info processInfo = processHandle.info();
long pid = processHandle.pid();
System.out.println(pid);
System.out.println(processInfo.command().get());
System.out.println(processInfo.startInstant().isPresent());
System.out.println(processInfo.totalCpuDuration().isPresent());

간단한 인터페이스이기 때문에 한번씩 해보면 될 듯 싶다.

interface

우리가 아는 java의 interface를 말하는 거 맞다. java8 부터 인터페이스에 구현을 할 수 있게 된건 누구나 아는 사실이다.

interface SomeInterface {
    default void doSomething() {
              System.out.println("blabla");
    }
}

위와 같이 interface에 default라는 키워드를 사용해서 구현을 할 수 있다. java9 부터는 메서드에 private 접근제한을 둘수 있다.

interface SomeInterface {

    private void doSomething(){
              System.out.println("blabla");
    }
}

나쁘지 않다. 하지만 protecteddefault 접근제한은 사용할 수 없다.

Reactive

요즘 대두가 많이 되고 있는 Reactive API가 java9에 추가 되었다. 그 구현체 중 Netflix의 rxJava와 Spring의 Reactor 라는 프로젝트가 그 대표적인 예 이다. 자세한 내용은 rxJava와 Reactor라는 프로젝트를 참고하면 되겠다. 필자의 경우에는 Spring을 자주 사용하니 Spring 관련해서 Reactor를 공부할 듯 싶다. Spring5 에서 정식으로 Reactive를 지원하니 Spring5가 나올 때까지 슬슬 공부하면 될 것 같다. Spring boot는 2.0 부터 지원하니 현재 M1 버전으로 공부해도 될 듯 싶다.
그리고 java9가 나오면 java9 쪽으로 인터페이스를 바꾸지 않을까 생각된다. 그냥 필자 생각이지만..
Reactive 또한 이야기 할 것도 많고 쉬운 내용도 아니니 나중에 좀 더 공부를 한뒤에 글을 작성해보도록 하자.

좀 더 많은 내용이 있겠지만 필자가 알아본 정도는 여기까지이다. java9가 릴리즈 되면 차근차근 좀 더 알아볼 수 있도록 하자. 회사에서 사용하려면 좀 더 안정화 되면 사용해야겠지만 개인적으로는 릴리즈 되면 바로 올려서 사용할 예정이다.
직소가 JCP 리뷰에 통과해서 java9에서 만났으면 좋겠다. java7부터 고생이 많다..

위의 예제들을 간단하게 만들었는데 github에서 살펴보자.