- 컬렉션 팩토리 사용하기
- 리스트 및 집합과 사용할 새로운 관용 패턴 배우기
- 맵과 사용할 새로운 관용 패턴 배우기
컬렉션 API가 없었다면 개발자는 힘들었을거다. 거의 모든 자바 애플리케이션에서 컬렉션을 사용한다. 지금까지 컬렉션과 스트림 API를 이용하여 데이터 처리 쿼리를 어떻게 효율적으로 하는 지 살펴봤다. 하지만 여전히 쉽지 않고 에러를 유발하는 여러 단점이 있다.
8장에서는 자바8,9에 추가된 편리한 컬렉션 API에 대해서 배운다. 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 자바 9에 새로 추가된 컬렉션 팩토리를 살펴보고, 개선사항으로 리스트와 집합에서 요소를 삭제하거나 바꾸는 관용 패턴을 적용하는 방법을 배워본다.
List<String> friends = new ArrayList<>;
friends.add("raphael");
friends.add("eddy");
friends.add("thibaut");
문자열 세개를 저장하는데도 많은 코드가 필요하다.
List<String> friends = Arrays.asList("raphael", "eddy", "thibaut");
고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 있지만, 새 요소를 추가하거나 요소를 삭제할 순 없다.
요소를 추가하려면 UnsupportedOperationException
이 발생한다. 내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문이다.
Set<String> friends = new HashSet<>(Arrays.asList("raphael","eddy","thibaut"));
Set<String> friends = Stream.of("raphael","eedy","thibaut").collect(toSet());
리스트를 인수로 받는 HashSet 생성자를 사용할 수 있겠지만, 내부적으로 불필요한 객체 할당을 필요로하며 매끄럽지 못하다. 그리고 결과는 변환할 수 있는 집합이라는 사실도 있다.
그렇다면 map은 어떨까 ? 자바 9에서는 작은 집합, 리스트, 맵을 쉽게 만들 수 있는 팩토리 메서드를 제공한다.
List.of : 변경할 수 없는 불변 리스트를 만듦
Set.of : 변경할 수 없는 불변 집합을 만듦, 중복된 요소를 제공하여 집합 생성하면 IllegalArgumentException 발생
Map.of : 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있음
Map.ofEntries : Map.Entry<K,V> 객체를 인수로 받아 맵을 만들 수 있다. 엔틑리 생성은 Map.entry 팩토리 메서드를 이용하여 전달.
자바 8에 다음 메서드들이 추가 되었다.
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
이들은 새로운 결과를 만드는 스트림 동작과는 달리 기존 컬렉션을 바꾼다. 왜 추가되었나 ? 컬렉션을 바꾸는 동작은 복잡하고 에러를 유발하기 때문이다.
for(Transaction transaction : transactions){
if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
transactions.remove(transaction);
}
}
for-each 루프는 내부적으로 Iterator 객체를 사용하므로 두개의 개별 객체가 컬렉션을 관리하게 된다.
결과적으로 반복자의 상태는 컬렉션의 상태와 서로 동기화 되지 않는다. Iterator 객체를 명시적으로 사용하고 객체의 remove() 메서드를 호출함으로 이 문제를 해결할 순 있지만, 코드가 복잡해진다.
맵에서 키와 값을 반복하여 확인하는 것은 번거롭다. 때문에 아래와 같이 반복자를 이용해 맵의 항목 집합을 반복한다.
for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {
String friend = entry.getKey();
Integer age = entry.getKey();
System.out.println(friend + age);
자바 8에서부터 Map 인터페이스는 BiConsumer (키와 값을 인수로 받는다.)를 인수로 받는 forEach메서드를 지원하므로 코드를 조금 더 간단하게 만들 수 있다.
ageOfFriends.forEach((friend, age) -> System.out.println(friend + age));
다음 두개의 새 유틸리티를 이용하여 맵의 항목을 값 또는 키를 기준으로 정렬할 수 있다.
Entry.comparingByValue
Entry.comparingByKey
favouriteMusics
.entrySet()
.stream()
.sorted(Entry.comparingByKey())
// 사람의 이름을 알파벳 순으로 스트림 요소 처리
.forEachOrdered(System.out::println);
요청한 키가 맵에 존재하지 않을 때, 이를 어떻게 처리하냐도 흔히 발생하는 문제인데, 새로 추가된 getOrDefault
메서드를 이용하면 쉽게 해결할 수 있다.
기존에 찾으려는 키가 존재하지 않으면 널이 반환되므로 NPE를 발생하는데 이를 방지하려면 요청 결과가 null인지 확인해야한다. 기본값을 반환하는 방식으로 이 문제를 해결한다.
맵에 키가 존재하는 지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황(값비싼 동작 후 얻은 결과를 캐시하려고 할 때)이라면 다음의 세가지 연산을 사용하면 도움이 된다.
computeIfAbsent
: 제공된 키에 해당하는 값이 없거나 null이라면, 키를 이용해 새로운 값을 계산하고 맵에 추가한다.
computeIfPresent
: 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다. 계산한 값이 null이라면 맵에 추가하지 않으면 오히려 존재하던 key 또한 제거한다.
compute
: 제공된 키로 새 값을 계산하고 맵에 저장한다.
default boolean remove(Object key, Object value)
제공된 키에 해당하는 맵 항목을 제거하는 remove 메서드와 더불어, 키가 특정한 값에 연관되어 있을 때만 항목을 제거하면 오버로드 버전 메서드를 제공한다.
맵의 항목을 바꾸는데 사용할 수 있는 메서드이다
replaceAll
: BiFunction을 적용한 결과로 각 항목의 값을 교체한다. List의 replaceAll과 비슷한 동작을 수행한다.
replace
: 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 존재한다.
두 개의 맵을 합칠 때 putAll 메서드를 사용했는데, 이때 중복된 키가 있다면 원하는 동작이 이루어지지 못할 수 있다. 새로 제공되는 merge 메서드는 중복된 키에 대한 동작(BiFunction)을 정의해줄 수 있다.
forEach : 각 (키, 값) 쌍에 주어진 액션을 수행
reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
search : 날이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
또한 연산에 병렬성 기준값(threshold)을 정해야 한다. 맵의 크기가 기준값보다 작으면 순차적으로 연산을 진행한다. 기준값을 1로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화할 수 있다.
맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다. 기존에 제공되던 size 함수는 int형으로 반환하지만 long 형으로 반환하는 mappingCount를 사용할 때 매핑의 개수가 int의 범위를 넘어서는 상황에 대하여 대처할 수 있을 것이다.
ConcurrentHashMap을 집합 뷰로 반환하는 keySet 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다. newKeySet이라는 메서드를 통해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.