1. 서론
이번 포스팅에서는 Chapter8의 컬렉션 API 개선 에 대해 진행하도록 하겠습니다.
2. 컬렉션 팩토리
자바 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 팩토리 메서드를 제공하고 있습니다.
기존 작은 List 객체를 간단히 생성할 때는 Arrays.asList() 를 통해 만들었습니다.
Arrays 를 통해 만든 리스트에는 새 요소를 추가하거나 삭제는 불가능하지만 갱신이 가능했습니다.
자바 9에서는 컬렉션에 Immutable 한 객체를 생성할 수 있도록 팩토리 메서드를 제공하고 있습니다.
1) 리스트 팩토리
List.of 팩토리 메서드를 통해 간단하게 리스트를 만들 수 있습니다.
아래는 예제입니다.
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
System.out.println(friends);
List.of 로 만든 리스트의 경우에는 추가, 삭제, 변경 모두 되지 않는 리스트입니다.
또한, null은 허용하지 않아 의도치 않은 버그를 방지할 수 있습니다.
of 메서드의 경우 가비지 컬렉션 비용으로 인해 10개까지는 고정인자로 받을 수 있도록 오버로딩이 되어 있습니다.
10개 이상으로는 ... 문법을 통해 가변인자로 받고 있습니다.
이것은 Set, Map에서도 동일하게 적용되어 있습니다.
2) 집합 팩토리
List.of와 비슷하게 Set.of 를 통해 간단히 Set을 만들 수 있습니다.
3) 맵 팩토리
Map의 경우 자바 9에서 만들 수 있는 방법을 2가지 제공하고 있습니다.
1. Map.of
첫째로 Map.of 를 통해 만들 수 있습니다.
아래는 예제입니다
Map<String, Integer> ageOfFriends
= Map.of("Raphael", 30, "Olivial", 25, "Thibaut", 26);
System.out.println(ageOfFriends);
위 방법은 10개 이하의 키와 값을 통해 만들 때 유용합니다.
10개가 넘어가는 Map을 만들때는 Map.ofEntries 를 사용해야합니다.
Map<String, Integer> ageOfFriends = Map.ofEntries(
Map.entry("Raphael", 30),
Map.entry("Olivial", 25),
Map.entry("Thibaut", 26)
);
3. 리스트와 집합 처리
자바8 에서는 List, Set 인터페이스에 아래와 같은 메서드가 추가되었습니다.
- removeIf: 프레디케이트를 만족하는 요소를 제거
- replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꿈
- sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
위 메서드는 새로운 컬렉션을 반환하는것이 아니라 메서드를 호출한 컬렉션 자체를 바꿉니다.
컬렉션을 바꾸는 동작은 에러를 유발하는 위험한 코드입니다.
이를 위해 위와 같은 메서드가 만들어지게 되었습니다.
1) removeIf
아래는 리스트에서 숫자로 시작되는 값을 삭제하는 코드입니다.
for(Transaction transaction : transactions) {
if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
transactions.remove(transaction);
}
}
아쉽게도 위 코드는 ConcurrentModificationException을 발생시킵니다.
내부적으로 for-each는 Iterator 객체를 사용하게 되는데 위와같이 remove를 하게 되면,
Iterator와 컬렉션의 상태가 동기화가 되지 않기 때문입니다.
하지만 자바8에서 제공하는 removeIf를 통해 이 문제를 해결할 수 있습니다. 또한, 더욱 readable해진 코드가 탄생합니다.
아래는 removeIf를 적용한 예제 코드입니다.
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));
2) replaceAll 메서드
자바8에서는 변경관련하여 replcateAll이라는 메서드를 제공하고 있습니다.
아래와 같이 stream API를 통해서도 가능합니다.
List<String> referenceCodes = List.of("a12", "C14", "b13");
referenceCodes
.stream()
.map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1))
.collect(Collectors.toList());
하지만 위의 코드의 경우에는 새로운 컬렉션이 생성될 뿐더러 코드가 replcaceAll보다는 길게 만들어집니다.
아래는 replcaceAll를 적용한 코드입니다.
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
4. 맵 처리
자바8에서는 Map 인터페이스에도 몇가지 디폴트 메서드가 추가되었습니다.
1) forEach 메서드
아래는 기존 Map을 순회하는 코드입니다.
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에서는 forEach 메서드를 제공하여 아래와 같이 간편하게 코드를 사용할 수 있도록 되었습니다.
ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " + age + " years old"));
2) 정렬 메서드
정렬 관련해서드 아래와 같이 2개 메서드를 제공하고 있습니다
- Entry.comparingByValue
- Entry.comparingByKey
아래는 Entry.comparingByKey의 예제입니다.
Map<String, String> favoriteMovies = Map.ofEntries(
Map.entry("Raphael", "Star Wars"),
Map.entry("Cristina", "Matrix"),
Map.entry("Olivia", "James Bond")
);
favoriteMovies
.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(System.out::println);
3) getOrDefault 메서드
Map에 키가 없는경우, null이 아닌 지정한 디폴트 값을 가져오도록 getOrDefault 메서드를 제공하고 있습니다.
이는, NullPointerException을 방지하기 위한 null 체크로직이 없어져 개발자에게 큰 이득을 주게되었습니다.
아래는 예제입니다.
Map<String, String> favoriteMovies = Map.ofEntries(
Map.entry("Raphael", "Star Wars"),
Map.entry("Cristina", "Matrix"),
Map.entry("Olivia", "James Bond")
);
System.out.println(favoriteMovies.getOrDefault("Olivia", "Matrix"));
System.out.println(favoriteMovies.getOrDefault("Thibaut", "Matrix"));
getOrDefault 도 키가 존재하지만 값이 null인 경우에는 null을 반환하는 점을 유의해야 합니다.
4) 계산 패턴
맵에 키 존재여부에 따라 동작을 수행 후 결과를 저장해야 하는 경우가 있습니다.
이를 위해 아래 3개 메서드를 제공하고 있습니다.
- computeIfAbsent : 제공된 키에 해당하는 값이 없으면, 키를 이용해 새 값을 계산하고 맵에 추가
- computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가
- compute : 제공된 키로 새 값을 계산하여 맵에 저장
아래는 computeIfAbsent 예제입니다.
Map<String, List<String>> favoriteMovies = new HashMap<>();
favoriteMovies.computeIfAbsent("Raphael", name -> new ArrayList()).add("Star Wars");
위와 같이 Map<String, List<String>> 같은 자료구조를 사용할때 유용하게 사용할 수 있는것을 볼 수 있습니다.
computeIfPresent는 computeIfAbsent와 반대라고 생각하시면 됩니다.
단, 한가지 특이점은 computeIfPresent의 경우 반환값이 null인 경우 매핑을 해제하게 됩니다.
5) 삭제 패턴
자바8 에서는 기존에 있는 remove메서드를 오버로드하여, 키가 특정한 값과 연관되었을때만 지우도록 제공하고 있습니다.
아래는 예제입니다.
favoriteMovies.remove(key, value);
6) 교체 패턴
맵의 항목을 바꾸는데 사용할 수 있는 메서드도 제공하고 있습니다.
- replaceAll : BiFunction을 적용한 결과로 각 항복의 값을 교체
- Replace : 키가 존재하면 맵의 값을 바꿈
아래는 replaceAll의 예제 코드입니다.
Map<String, String> favoriteMovies = new HashMap<>();
favoriteMovies.put("Raphael", "Star Wars");
favoriteMovies.put("Olivia", "James Bond");
favoriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
7) 합침
두개의 Map을 합치는 경우에는 putAll 메서드를 제공하고 있습니다.
하지만, 이 메서드의 경우에는 두 Map에 중복된 키가 없는 경우에만 가능합니다.
이를 위해 자바 8에서는 merge 메서드를 제공하고 있습니다.
아래는 merge 메서드를 사용한 예제입니다.
Map<String, String> family = Map.ofEntries(
Map.entry("Teo", "Star Wars"),
Map.entry("Cristina", "James Bond")
);
Map<String, String> friends = Map.ofEntries(
Map.entry("Raphael", "Star Wars"),
Map.entry("Cristina", "Matrix")
);
Map<String, String> everyOne = new HashMap<>(family);
friends.forEach(
(k, v) -> everyOne.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2));
3번째 인자로 중복키가 있는 경우 어떻게 처리할건지에 대해 받고 있는것을 볼 수 있습니다.
5. 개선된 ConcurrentHashMap
ConcurrentHashMap은 내부 자료구조의 특정 부분만 잠궈 동시추가, 갱신 작업을 허용하는 동시성에 친화적인 Map입니다.
1) 리듀스와 검색
ConcurrentHashMap에서는 스트림과 비슷하게 아래 3개의 연산을 지원하고 있습니다.
- forEach : 각 (키, 값) 쌍에 주어진 액션 실행
- reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
- search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
아래는 위 연산을 수행할 수 있도록 제공하는 메서드입니다.
- 키 값으로 연산 -> forEach, reduce, search
- 키로 연산 -> forEachKey, reduceKeys, searchKeys
- 값으로 연산 -> forEachValue, reduceValues, searchValues
- Map.Entry 객체로 연산 -> forEachEntry, reduceEntries, searchEntries
위 연산들은 모두 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행하게 됩니다.
따라서, 연산이 수행된는 동안 변경에 대해 의존이 있으면 안됩니다.
2) 계수
기존 Map의 사이즈는 size 메서드를 사용했습니다.
하지만 size는 int형으로서 int 범위를 넘어서는 상황을 대처하기 위해 mappingCount 함수를 제공하고 있습니다.
mappingCount는 long형 입니다.
6. 마무리
이번 포스팅에서는 Chapter8 컬렉션 API 개선에 대해 진행하였습니다.
다음에는 Chapter9 리팩터링, 테스팅, 디버깅에 대해 포스팅하겠습니다.
'Programming > ModernJavaInAction' 카테고리의 다른 글
(10) 람다를 이용한 도메인 전용 언어 (0) | 2020.04.19 |
---|---|
(9) 리팩터링, 테스팅, 디버깅 (0) | 2020.04.13 |
(7) 병렬 데이터 처리와 성능 (0) | 2020.04.11 |
(6) 스트림으로 데이터 수집 (0) | 2020.04.04 |
(5) 스트림 활용 (0) | 2020.04.04 |