[Java] JDK9의 새로운 기능
현재 Java Release가 21까지 나온 상황이다. 아직도 Java8은 많이 사용되고 있지만, 옛날 기술이 되어간다고 느낍니다.
이후 Java 9버전, 11버전에서 강력한 기능들도 추가됐으니 8버전을 쓰는것이 점점 손해라 생각합니다.
더 늦기 전에 9버전, 11버전을 사용해보면서 익숙해지기 위해 새로운 기능들에 대해 정리합니다.
먼저 9버전을 정리하는데 많은 기능들 중에 가장 자주 쓰이는 기능을 정리하겠습니다.
(Java 9버전의 더 많은 기능은 오라클의 JDK9 문서를 참고해주세요!)
컬렉션을 위한 팩터리 메서드
자바의 컬렉션 추상화 최상위 수준에는 List, Set, Map이 있습니다.
지금까지 Java에서 컬렉션을 초기화함과 동시에 요소들을 넣는 방법이 간단하지 않았습니다. 또한 컬렉션의 요소를 변경할 수 없도록 하려면 추가 작업이 필요합니다.
예를들어 Java 8에서는 다음과 같이 작성합니다.
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
Collections.unmodifiableList(numbers);
물론 Java 8에서도 Arrays.asList(T... t) 가 있습니다.
List<Integer> numbers = Arrays.asList(1,2,3,4,5);
이렇게 사용하면 별 문제가 없을 것같지만 두가지 문제가 있습니다.
- 불변 컬렉션이 되지 못한다.(요소 변경이 가능, set() replaceAll() 메서드)
- 요소에 null 값을 허용한다.
Arrays.asList 메서드 내부를 보면 놀랍게도 흔히 사용하는 List의 구현체 (ArrayList, LinkedList)가 아닌
Arrays 내부에 정의한 ArrayList를 사용합니다. (무슨 의미인지 모르겠다면 아래의 그림을 봐주세요!)
Arrays.asList는 Arrays 내부에 정의한 ArrayList를 사용하고 있습니다. 이것도 AbstractList를 구현했으므로 컬렉션 API로 사용될 수는 있습니다. 하지만 컬렉션의 요소를 변경할 수 있는가? 에 대해서는 반은 맞고 반은 틀립니다.
우선 추가(add), 삭제(remove)에 대해서는 막혀있습니다. 해당 기능을 사용하면 UnsupportedOperationException이 발생합니다.
하지만 set(), replaceAll() 메서드는 재정의됐으므로, 사용이 가능합니다. 즉, 요소에 대한 변경이 가능합니다. 또한 요소에 null을 넣는것을 허용합니다.
(여기서 말하는 요소의 변경이란 요소 자체가 다른 요소로 변경되는 것을 의미합니다. 참조객체의 내부 상태를 변경하는것을 의미하는 것은 아닙니다. 해당 부분이 혼란스럽다면 댓글로 질문해주세요!)
우리는 Java 9의 List.of() 를 사용해서 이런 문제를 해결할 수 있습니다.
List<Integer> numbers = List.of(1,2,3,4,5);
(Set, Map의 경우에도 of 팩터리 메서드가 존재합니다.)
of 팩터리 메서드 같은 경우 요소를 변경할 수 없는 불변 컬렉션을 반환합니다. 삽입(add), 삭제(remove) 는 물론이고 변경(set, replace)도 불가능합니다. 또 원소에 null 값을 허용하지 않습니다.
행위 | Arrays.asList | List.of |
add | 불가능 | 불가능 |
remove | 불가능 | 불가능 |
수정(set,replace) | 가능 | 불가능 |
null 허용 | 가능 | 불가능 |
또 한가지 차이는 List.of 같은 경우 메서드 다중 정의(overloading)를 통해 성능도 준수합니다.
Arrays.asList(T... a)는 가변인자를 받는 하나의 메서드만 구현했습니다. 따라서 인자로 넣는 모든 요소들은 배열로 생성되고 List에 들어가게됩니다.
List.of() 같은 경우 인자를 0~10개까지 받는 다중 정의 메서드와 가변인자를 받는 1개의 메서드로 구현했습니다.
//예시
public List<T> of() { ... }
public List<T> of(T a1) { ... }
public List<T> of(T a1, T a2) { ... }
public List<T> of(T a1, T a2, T3 a3) { ... }
...
public List<T> of(T a1, T a2, T3 a3, T a4, T a5, T a6, T a7, T a8, T3 a9, T a10) { ... }
public List<T> of(T... a) { ... }
이렇게 다중정의를 해두면 인자 0 ~ 10개인 메서드를 호출할 때 배열을 따로 생성하지 않습니다.
대부분의 리스트를 초기화할 때 10개 이하의 인자를 넣으므로 성능적으로 상당히 최적화했다고 볼 수 있습니다.
즉 성능 측면에서도 Arrays.asList 보다는 List.of를 쓰는것이 낫습니다.
(단, 요소의 수정에 대해서 허용해야 한다면 Arrays.asList를 쓰는것을 고려할 수 있습니다.)
Optional 새로운 메서드 추가
NullPointerException의 발생 지점을 줄이기 위해 Optional 클래스를 활용할 수 있었습니다. 또한 스트림 API를 더 견고하게 해줬습니다.
Java 9부터 새로 등장한 Optional 메서드
- ifPresent(Consumer action) : 값이 존재한다면 Consumer 함수의 동작을 실행한다.
- ifPresentOrElse(Consumer action, Runnable emptyAction) : ifPresent와 유사하지만, 추가로 값이 없다면 emptyAction 을 실행한다.
- or(Supplier supplier) : 이 메서드는 Optional이 항상 값을 가지고 있기를 바랄 때 유용하게 사용할 수 있습니다. 값이 있는경우 해당 Optional을 그대로 반환하고, 값이 없는 경우 Supplier에 의해 생성되는 Optional을 반환합니다.
- stream() : 값이 있다면 요소가 하나인 스트림, 값이 없다면 요소가 0개인 스트림을 반환합니다.
Stream API 기능 향상
JDK 9에서 스트림 인터페이스에 네 가지 메서드를 추가했습니다.
takeWhile 과 dropWhile
먼저 두 가지 메서드 takeWhile(Predicate)과 dropWhile(Predicate)가 있습니다.
이 메서드는 기존의 skip(int), limit(int) 를 보완합니다.
skip(int), limit(int) 같은 경우 정수 값을 인자로 받지만 takeWhile과 dropWhile은 Predicate 함수를 인자로 받는다.
takeWhile(Predicate)의 경우, 입력 스트림에서 Predicate의 test() 메서드가 true를 반환할 때 까지 출력 스트림으로 전달합니다.
dropWhile(Predicate)의 경우, takeWhile과 반대로 test() 메서드가 true를 반환할 때 까지 입력 스트림에서 요소를 제거합니다. 요소제거 이후 입력 스트림의 나머지 요소는 출력 스트림에 전달됩니다.
이 두 가지 메서드의 경우 순서가 없는 스트림의 경우 위험할 수 있습니다. 첫 번째로 true를 반환하는 지점에서 멈추기 때문에 순서가 없다면 원하는 결과를 얻기 어려울 수 있습니다.
nullable(T t)
전달된 값이 null인지 여부에 따라, null이면 0개의 스트림, null이 아니면 1개 요소의 스트림을 반환합니다.
nullable 메서드를 활용해서 스트림 전에 null 검사를 제거할 수 있습니다.
Optional 클래스에 추가된 stream() 메서드와 비슷합니다.
Iterate() 다중 정의 (3개의 인자를 받는 iterate() 메서드)
Java 8에서 Stream.iterate() 메서드는 iterate(T seed, UnaryOperator<T> f)가 있었습니다.
이 메서드는 seed가 초기값을 의미하고, f는 반복을 진행함에 따라 값을 어떻게 변경할지를 정하는 함수입니다.
사용 예시는 다음과 같습니다.
Stream.iterate(0, i -> i + 1)
.limit(5)
이는 초기 값이 0이고, 반복마다 값에 1씩 더하게 됩니다. 즉, 1,2,3,4,5 ... 계속 증가합니다.
이런 스트림을 무한 출력 스트림이라 하는데, 이를 다루기 위해서는 limit() 메서드가 필요합니다.
Java 9 부터는 iterate 에 인자가 총 3개인 다중 정의를 구현해서 언제까지 반복할지에 대한 함수를 추가할 수 있습니다.
Stream.iterate(0, i -> i < 5, i -> i + 1)
첫 번째 인자는 초기값 입니다. 예시에서 초기값은 0 입니다.
두 번째 인자는 반복문 진행 조건입니다. 예시에서는 값이 5 미만일 때 까지 반복을 진행합니다.
세 번째 인자는 값을 어떻게 변경 시킬지 입니다. 예시에서는 반복마다 값에 1을 더합니다.
즉, 위의 예시는 0부터 4까지의 값을 출력 스트림으로 보냅니다.
사용한 모습을 보면 전통적인 for문 처럼 느껴집니다.
- https://www.oracle.com/corporate/features/jdk9-new-developer-features.html