8장
1. 컬렉션 팩토리
자바 9는 작은 컬렉션 객체를 쉽게 만들 수 있는 팩토리 메서드를 제공한다.
List 생성: List.of
Set 생성: Set.of
Map 생성: Map.of, Map.Entries
// List.of
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
// Set.of
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
// Map.of
Map<String, Integer> ageOfFriends =
Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
// Map.ofEntries
Map<String, Integer> ageOfFriends =
Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25), entry("Thibaut", 26));
팩토리 메서드는 내부적으로 고정된 크기의 변환할 수 있는 배열로 구현된다. 따라서 새 요소를 추가하려고 하면 UnsupportedOperationException이 발생한다.
2. List와 Set 처리
자바 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
removeIf: 프레디케이트를 만족하는 원소 제거
replaceAll: 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 원소를 바꿈
sort: 인터페이스에서 제공하는 기능으로 리스트를 정렬
2.1. removeIf 메서드
기존 코드는 컬렉션에서 원소를 삭제하기 위해 iterator를 사용해야 했다.
for (Iterator<Transaction> iterator = transactions.iterator();
iterator.hasNext(); ) {
Transaction transaction = iterator.next();
if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
transactions.remove(transaction);
}
}
자바 8부터 컬렉션의 원소를 삭제하고 싶을 때 removeIf 메서드를 제공한다.
transactions.removeIf(transaction ->
Character.isDigit(transaction.getReferenceCode().charAt(0)));
2.2. replaceAll 메서드
기존 코드는 기존 컬렉션을 바꾸기 위해 ListIterator 객체를 사용해야 했다.
for (ListIterator<String> iterator = referenceCodes.listIterator();
iterator.hasNext(); ) {
String code = iterator.next();
iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}
자바 8부터 컬렉션의 원소를 변경하고 싶을 때 replaceAll 메서드를 제공한다.
referenceCodes.replaceAll(code ->
Character.toUpperCase(code.charAt(0)) + code.substring(1));
3. Map 처리
자바 8에서는 HashMap의 내부 구조를 바꿔 성능을 개선했다. 기존에 맵의 항목은 키로 생성한 해시코드로 접근할 수 있는 버켓에 저장했다. 많은 키가 같은 해시 코드를 반환하는 상황이 되면 O(n)의 시간이 걸리는 LinkedList로 버킷을 반환해야 하므로 성능이 저하되었다. 최근에는 버킷이 너무 커질 경우 O(log(n))의 시간이 걸리는 정렬된 트리를 이용해 반환 성능을 개선했다. 다만 키가 String, Number 같은 Comparable 형태여야만 지원된다.
3.1. forEach 메서드
Map.Entry<K, V>의 반복자를 이용해 항목 집합을 반복할 수 있다.
for(Map.Entry<String, Integer> entry: ageOfFriends.entrySet()) {
String friend = entry.getKey();
Integer age = entry.getValue();
System.out.println(friend + "is " + age + "years old.");
}
자바 8부터 Map 인터페이스는 BiConsumer를 인수로 받는 forEach 메서드를 지원한다.
ageOfFriends.forEach((friend, age) ->
System.out.pritnln(friend + "is " + age + "years old."););
3.2. 정렬 메서드
Entry.comparingByValue
Entry.comparingByKey
두 개의 새로운 유틸리티를 이용하면 맵의 항목을 값 또는 키 기준으로 정렬할 수 있다.
Map<String, String> favouriteMovies =
Map.ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"), entry("Olivia", "James Bond"));
favouriteMovies.entrySet()
.stream()
.sorted(Entry.comparingByKey())
.forEachOrdered(System.out::println);
3.3. getOrDefault 메서드
기존에는 찾으려는 키가 존재하지 않으면 null이 반환되었다. 자바 8부터는 getOrDefault 메서드로 기본값을 반환할 수 있다. getOrDefault 메서드는 첫 번째 인수로 키, 두 번째 인수로 기본값을 받아 값이 없는 경우 기본값을 반환한다.
Map<String, String> favouriteMovies =
Map.ofEntries(entry("Raphael", "Star wars"),
entry("Olivia", "James Bond"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix"));
System.out.println(favouriteMovies.getOrDefault("Thibaut", "matrix"));
3.4. 계산 패턴
맵에 키 존재 여부에 따라 동작을 실행해야 할 때 도와주는 세 가지 연산이 있다.
computeIfAbsent: 제공된 키에 해당하는 값이 없거나 null이면 키를 이용해 새 값을 계산하고 맵에 추가
computeIfPresent: 제공된 키가 존재하면 새 값을 계산하고 맵에 추가
compute: 제공된 키로 새 값을 계산하고 맵에 저장
Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
line.forEach(line ->
dataToHash.computeIfAbsent(line, this::calculateDigest));
private byte[] calculateDigest(String key) {
return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
}
3.5. 삭제 패턴
기존의 remove 메서드를 활용한 삭제 패턴은 다음과 같다.
String key = "Raphael";
String value = "Jack Reacher 2";
if (favouriteMovies.containsKey(key) &&
Objects.equals(favouriteMovies.get(key, value))) {
favouriteMovies.remove(key);
return true;
} else {
return false;
}
자바 8부터 키가 특정 값과 연관되었을 때만 항목을 제거하는 remove 오버로드 버전 메서드를 제공한다.
favouriteMovies.remove(key, value);
3.6. 교체 패턴
맵의 항목을 바꾸는 데 사용할 수 있는 두 개의 메서드가 추가되었다.
replaceAll: BiFunction을 적용한 결과로 각 항목의 값을 교체
Replace: 키가 존재하면 맵의 값을 바꿈
Map<String, String> favouriteMovies = new HashMap<>();
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "James Bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies);
3.7. 합침
자바 8부터 merge 메서드를 활용해서 중복된 키를 합칠 수 있다. merge는 중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 받는다.
Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) ->
everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " = movie2));
System.out.println(everyone);
4. 개선된 CuncurrentHashMap
ConcurrentHashMap 클래스는 동시성 친화적인 HashMap이다. ConcurrentHashMap은 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다. 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다. ConcurrentHashMap은 세 가지 새로운 연산을 지원한다.
forEach: 각 (키, 값) 쌍에 주어진 액션을 실행
reduce: 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
search: null이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
또한 네 가지 연산 형태를 지원한다.
키, 값으로 연산(forEach, reduce, search)
키로 연산(forEachKey, reduceKeys, searchKeys)
값으로 연산(forEachValue, reduceValues, searchValues)
Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다. 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다. 또한 병렬성 기준값(threshold)을 지정해야 한다. 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다. 기준값을 1로 지정하면 공통 스레드 풀을 이용하므로 병렬성이 극대화된다. Concurrent 메서드는 새로운 메서드도 제공한다.
mappingCount: 맵의 매핑 개수 반환
keySet: ConcurrentHashMap을 집합 뷰로 반환
newKeySet: ConcurrentHashMap으로 유지되는 집합 반환
참고 자료
모던 자바 인 액션 - 한빛미디어
Last updated