컬렉션 팩토리(Collection Factory)
java 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 방법을 제공한다.
자바에서 적은 요소를 포함하는 리스트를 만들때 아래처럼 사용할 것이다.
List<String> cars = new ArrayList<>();
cars.add("아반테");
cars.add("소나타");
cars.add("그랜저");
이렇게 코드를 작성하게 되면 새 문자열을 저장하는데도 많은 코드가 필요하다.
Arrays.asList() 팩토리 메서드를 이용하면 코드를 간단하게 줄일 수 있다.
List<String> cars = Arrays.asList("아반테", "소나타", "그랜저");
내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 요소를 갱신할 순 있지만 추가하게 되면 UnsupportedOperationException이 발생한다.
List<String> cars = Arrays.asList("아반테", "소나타", "그랜저");
cars.set(0, "K3");
cars.add("K5"); // UnsupportedOperationException 발생!
Set 컬렉션을 생성할때는 HashSet의 생성자를 사용할 수 있고, 스트림 API를 사용할 수도 있다.
Set<String> cars = new HashSet<>(Arrays.asList("아반테", "소나타", "그랜저"));
Set<String> carStreams = Stream.of("아반테", "소나타", "그랜저").collect(Collectors.toSet());
하지만 두 방법 모두 매끄럽지 못하며 내부적으로 불필요한 객체 할당을 필요로 한다. 그리고 결과는 변환할 수 있는 집합이다.
이러한 문제들 때문에 java9에서는 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 팩토리 메서드를 제공한다.
리스트 팩토리(List Factory)
List.of 팩토리 메소드를 이용해서 간단하게 리스트를 만들 수 있다.
하지만 List.of 팩토리 메소드를 사용해서 List를 생성하면 변경할 수 없는 리스트가 만들어진다.
갱신, 추가, 삭제 모두 UnsupportedOperationException이 발생한다.
List<String> cars = List.of("아반테", "소나타", "그랜저");
cars.set(0, "K3"); // UnsupportedOperationException 발생!
cars.add("K5"); // UnsupportedOperationException 발생!
리스트를 바꿔야 하는 상황이라면 직접 다시 리스트를 생성해야 한다.
데이터 처리 형식을 설정하거나 데이터를 변환할 필요가 없다면 더 단순하고 간편한 팩토리 메서드를 이용할 것을 권장한다.
집합 팩토리(Set Factory)
List.of와 마찬가지로 Set.of를 사용한다.
마찬가지로 요소를 추가하거나 삭제하면 UnsupportedOperationException이 발생하고, 만약 중복된 요소로 집합을 만드려고 한다면 IllegalArgumentException이 발생한다.
Set<String> cars = Set.of("아반테", "소나타", "그랜저");
cars.add("K3"); // UnsupportedOperationException 발생!
cars.remove("아반테"); // UnsupportedOperationException 발생!
Set<String> overlapCars = Set.of("아반테", "아반테", "소나타", "그랜저"); // IllegalArgumentException 발생!
맵 팩토리(Map Factory)
맵을 만들때는 Map.of 팩토리 메서드를 사용해 키와 값을 번갈아 제공하는 방법으로 맵을 만들 수 있다.
역시 마찬가지로 요소를 추가, 삭제하면 UnsupportedOperationException이 발생한다.
Map<String, String> carInfo = Map.of("name", "아반테", "price", "23,000,000", "options", "2.0 Engine, SunRoof");
carInfo.put("fuel", "gasoline"); // UnsupportedOperationException 발생!
carInfo.remove("options"); // UnsupportedOperationException 발생!
이 방법은 요소의 개수가 적을때만 사용하는 것이 좋다.
그 이상의 맵을 만들때는 Map.ofEnties 팩토리 메서드를 이용하는 것이 좋다.
Map<String, String> carInfo = Map.ofEntries(
Map.entry("name", "아반테"),
Map.entry("price", "23,000,000"),
Map.entry("options", "2.0 Engine, SunRoof")
);
리스트(List)와 집합(Set) 처리
java8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
- removeIf : Predicate를 만족하는 요소를 제거한다. (List, Set 구현 및 구현을 상속받은 곳에서 사용가능)
- replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
- sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
컬렉션을 바꾸는 동작은 에러를 유발하며 복잡함을 더하기 때문에 이러한 메서드들이 추가되었다.
removeIf
만약 컬렉션을 아래 코드와 같이 for문으로 반복하며 제거한다고 하면 ConcurrentModificationException이 발생한다.
List<String> cars = new ArrayList<>(){{
add("아반테"); add("소나타"); add("그랜저");
}};
for (String car : cars) {
if (car.equals("아반테")) {
cars.remove(car); // ConcurrentModificationException 발생!
}
}
for문은 내부적으로 Iterator 객체를 사용하므로 위 코드는 위 코드는 다음과 같이 해석된다.
for ( Interator<String> iterator = cars.iterator(); iterator.hasNext(); ) {
String car = iterator.next();
if (car.equals("아반테")) {
cars.remove(car);
}
}
결과적으로 반복자(Iterator)의 상태는 컬렉션(cars)의 상태와 서로 동기화되지 않기 때문에 ConcurrentModificationException이 발생하는 것이다.
이럴때 Iterator 객체를 명시적으로 사용하고 그 객체의 remove()를 호출하면 해결할 수 있다.
for ( Iterator<String> iterator = cars.iterator(); iterator.hasNext(); ) {
String car = iterator.next();
if (car.equals("아반테")) {
iterator.remove();
}
}
이 코드 패턴을 removeIf 메서드로 간단하게 구현할 수 있다.
cars.removeIf(car -> car.equals("아반테"));
replaceAll
List 인터페이스의 replaceAll 메서드를 이용해 리스트의 각 요소를 새로운 요소로 바꿀 수 있다.
만약 스트림 API에 map 메서드를 이용해 변경한다면 새 문자열 컬렉션을 만들어 버린다.
우리가 원하는건 기존 컬렉션을 변경하는 것이다.
for문을 이용하면 removeIf와 마찬가지로 아래와 같은 코드로 해석된다.
for (ListIterator<String> iterator = cars.listIterator(); iterator.hasNext(); ) {
String car = iterator.next();
iterator.set(car + " 풀옵션");
}
이 코드 패턴을 removeAll 메서드로 간단하게 구현할 수 있다.
List<String> cars = new ArrayList<>(){{
add("아반테");
add("소나타");
add("그랜저");
}};
cars.replaceAll(car -> car + " 풀옵션");
맵(Map) 처리
java8에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다.
forEeah
맵에서 키와 값을 반복하면서 확인하는 작업이 필요할때 Map.Entry<K, V>의 반복자를 이용할 수 있다.
Map<String, String> carInfo = Map.ofEntries(
Map.entry("name", "아반테"),
Map.entry("price", "23,000,000"),
Map.entry("options", "2.0 Engine, SunRoof")
);
for (Map.Entry<String, String> entry: carInfo.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + ":" + value);
}
carInfo.forEach((key, value) -> System.out.println(key + ":" + value));
정렬 메서드
다음 두 개의 새로운 유틸리티를 이용하면 맵의 항목을 값 또는 키 기준으로 정렬할 수 있다.
- Entry.comparingByValue
- Entry.comparingByKey
carInfo.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(System.out::println);
getOrDefault
기존에 찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException을 방지하려면 요청 결과가 널인지 확인해야 한다. 이를 기본값을 반환하는 방식으로 해결할 수 있다.
이 메서드는 첫 번쨰 인수로 키를, 두 번쨰 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다.
Map<String, String> carInfo = Map.ofEntries(
Map.entry("name", "아반테"),
Map.entry("price", "23,000,000"),
Map.entry("options", "2.0 Engine, SunRoof")
);
System.out.println("fuel : " + carInfo.getOrDefault("fuel", "gasoline")); // fuel : gasoline
여기서 키가 존재하더라도 값이 null인 상황에서는 getOrDefault가 널을 반환할 수 있다는 사실을 기억해야 한다.
계산 패턴
맵에 키가 존재하는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황이 필요할때가 있다.
이럴때 아래의 세 가지 연산이 이런 상황에서 도움을 준다.
- computeIfAbsent : 제공된 키에 해당하는 값이 없으면(값이 없거나 null), 키를 이용해 연산 후 맵에 추가한다.
- computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
- compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.
Map<Integer, Integer> square = new HashMap<>() {{
put(1, 1);
put(2, 0);
}};
square.computeIfAbsent(3, (key) -> key * key); // key3의 값(value)이 없으므로 연산 후 추가 {3 : 9}
square.computeIfAbsent(3, (key) -> key * key * key); // key3의 값(value)이 존재하므로 추가되지 않음
square.computeIfPresent(4, (key, value) -> key * key); // key4가 존재하지 않아 추가되지 않음
square.compute(2, (key, value) -> key * key); // key2의 값(value) 연산 후 추가
System.out.println("square : " + square); // square : {1=1, 2=4, 3=9}
Map<K, List<V>> 같은 경우 요소를 추가할때 computeIfAbsent를 사용할 수 도 있다.
Map<String, List<String>> carsOfBrand = new HashMap<>();
// Kia 값이 없으면 new ArrayList<>() 반환, 있으면 기존 값 반환
carsOfBrand.computeIfAbsent("Kia", brand -> new ArrayList<>()).add("K5");
삭제 패턴
맵에서 특정 키에 해당하는 맵 항목을 제거하는 remove는 이미 많이 사용해봤을 것이다.
java8에서는 키가 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공한다.
map.remove(key, value);
교체 패턴
맵의 항목을 바꾸는 데 사용할 수 잇는 두 개의 메서드가 맵에 추가되었다.
- replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다.
- replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값과 연관되었을 때만 값을 교체하는 overload 버전도 있다.
Map<String, Integer> numbers = new HashMap<>() {{
put("number1", 1);
put("number2", 2);
put("number3", 3);
}};
numbers.replaceAll((key, value) -> value + 10);
System.out.println("numbers : " + numbers); // numbers : {number1=11, number2=12, number3=13}
numbers.replace("number1", 1); // if (key == "number1") value = 1;
System.out.println("numbers : " + numbers); // numbers : {number1=1, number2=12, number3=13}
numbers.replace("number2", 2, 2); // if (key == "number2" && value == 2) value = 2;
System.out.println("numbers : " + numbers); // numbers : {number1=1, number2=12, number3=13}
합침
두 맵을 합쳐야 할때 forEach와 merge 메서드를 이용해 충돌없이 두 맵을 합칠 수 있다.
Map<String, Integer> numbers1 = new HashMap<>() {{
put("number1", 10);
put("number2", 20);
put("number3", 30);
put("number4", 40);
}};
Map<String, Integer> numbers2 = new HashMap<>() {{
put("number1", 1);
put("number2", 2);
put("number3", 3);
}};
numbers1.forEach((k, v) -> numbers2.merge(k, v, Integer::sum));
System.out.println("numbers2 : " + numbers2);
// numbers2 : {number1=11, number2=22, number3=33, number4=40}
개선된 ConcurrentHashMap
ConcurrentHashMap 클래스는 동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다.
ConcurrentHashMap은 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다.
따라서 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다. (표준 HashMap은 비동기로 동작함)
리듀스와 검색
ConcurrentHashMap은 세 가지 새로운 연산을 지원한다.
- forEach : 각 (키, 값) 쌍에 주어진 액션을 실행
- reduce : 모든 (키, 값) 쌍을 제공된 리듀스 함수를 이용해 결과로 함침
- search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
다음처럼 키에 함수 받기, 값, Map.Entry, (키, 값) 인수를 이용한 네 가지 연산 형태를 지원한다.
- 키, 값으로 연산(forEach, reduce, search)
- 키로 연산(forEachKey, reduceKey, searchKey)
- 값으로 연산(forEachValue, reduceValues, searchValues)
- Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
이들 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행한다는 점을 주목하자.
따라서 이들 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
또한 이들 연산에 병렬성 기준값을 지정해야 한다. 맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다.
기준값을 1로 지정하며 공통 스레드 풀을 이용해 병렬성을 극대화한다.
Long.MAX_VALUE를 기준값으로 설정하면 한 개의 스레드로 연산을 실행한다.
소프트웨어 아키텍처가 고급 수준의 자원 활용 최적화를 사용하고 있지 않다면 기준값 규칙을 따르는 것이 좋다.
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
long parallelismThreshold = 1;
Optional<Long> maxValue = Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
int, long, double 등의 기본값에는 전용 each reduce 연산이 제공되므로 reduceValuesToInt, reduceKeysToLong 등을 이용하면 박싱 작업을 할 필요가 없고 효율적으로 작업을 처리할 수 있다.
계수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다.
기존의 size 메서드 대신 새 코드에서는 int를 반환하는 mappingCount 메서드를 사용하는 것이 좋다. 그래야 매핑의 개수가 int의 범위를 넘어서는 이후의 상황을 대처할 수 있기 때문이다.
집합뷰
ConcurrentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 keySet이라는 새 메서드를 제공한다.
맵을 바꾸면 집합도 바뀌고 반대로 맵도 영향을 받는다. newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.
'Java' 카테고리의 다른 글
[모던 자바] 디자인 패턴 소개 및 람다를 이용해 리팩터링 하기 (0) | 2022.03.11 |
---|---|
[모던 자바] 가동석과 유연성을 개선하는 리팩터링 (0) | 2022.03.11 |
[모던 자바] Spliterator 인터페이스란 무엇인가? (0) | 2022.03.10 |
[모던 자바] 포크/조인 프레임워크란 무엇인가? (0) | 2022.03.10 |
[모던 자바] 병렬 데이터 처리와 성능 측정 (0) | 2022.03.08 |