8.1 컬렉션 팩토리
Arrays.asList() 팩토리 메서드를 이용하면 코드가 간단하게 생성 가능하다.
요소를 추가하거나 삭제하면 AbstractList 클래스에서 UnsupportedOperationException이 발생한다.
UnsupportedOperationException이 발생하는 이유는 뭘까??
asList에서 사용하는 ArrayList는 java.util.ArrayList가 아니라 Arrays 클래스 내부의 정적인 클래스인 ArrayList를 사용하고 있다.
- Arrays클래스의 ArrayList 멤버 변수를 보면 Element가 final 키워드와 함께 초기화 되는것을 볼 수 있다.
- java.util.ArrayList와 동일한 추상 클래스인 AbstractList<E>를 상속하고 있다.
집합의 경우 리스트를 인수로 받는 HashSet 생성자를 사용하거나 스트림 API를 사용하는 방법이 존재하지만 코드가 매끄럽지 못하며 내부적으로 불필요한 객체 할당을 필요로 한다.
- 이 때는 java.util에 있는 Collection 객체들로 한번 감싸줘서 변경 가능하다 라고 쉽게 이해했다.
- 생성자에 팩토리 메서드 객체들이 들어올 수 있는 이유는 AbstractList가 AbstractCollection을 상속하고 있기 때문이다.
HashSet<String> map = new HashSet<>(Arrays.asList("java","study"));
HashSet<String> map = Stream.of("java", "study").collect(Collectors.toCollection(HashSet::new));
ArrayList<String> list = new ArrayList<>(Arrays.asList("java", "study"));
java 9에서 개선된 다양한 컬렉션 API를 확인해보자
리스트 팩토리
List.of 팩토리 메서드를 이용해 간단하게 리스트를 만들 수 있다.
List<String> test = List.of("java", "study");
- Arrays.asList() 팩토리 메서드와 동일하게 UnsupportedOperationException이 발생하며 이유 또한 비슷하다.
- 컬렉션이 의도치 않게 변하는 것을 막을 수 있다.
오버로딩 vs 가변 인수
가변인수를 사용하는 Arrays.asList와 다르게 List.of는 다양한 오버로드 버전이 존재한다.
- List.of도 매개변수가 10개를 넘을 경우 이 때는 가변 인수를 사용해야 한다.
//가변 인수
public static <T> List<T> asList(T... a){...}
//오버로딩으로 고정된 숫자의 요소 0~10개 까지 존재한다.
static <E> List<E> of(){...}
...
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10){...}
오버로딩을 택한 이유가 무엇일까?
내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다.
- 따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 지불해야 한다.
집합 팩토리
List.of와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> set = Set.of("java","study"); //Ok
Set<String> set = Set.of("java","study","study"); //IllegalArgumentException
중복된 요소를 제공해서 집합을 만들려고 하면 IllegalArgumentException이 발생(집합은 오직 고유의 요소만 포함할 수 있다는 원칙을 상기시킨다.)하며, add()를 통해 추가시 UnsupportedOperationException이 발생한다.
맵 팩토리
두 가지 방법으로 바꿀 수 없는 맵을 초기화할 수 있다.
1. Map.of: 10개 이하의 키와 값 쌍을 가진 작은 맵을 만들 때는 키와 값을 번갈아 제공하는 방법으로 만들 수 있다.
Map<String, String> map = Map.of("1-key", "1-value", "2-key", "2-value");
2. Map.ofEntries: 10개 이상의 맵을 만들 때는 Map.Entry<K, V> 객체를 인수로 받으며 가변 인수로 구현한다.
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries) {...}
Map<String, String> map = Map.ofEntries(
Map.entry("1-key", "1-value"),
Map.entry("2-key", "2-value")
);
- Map.entry는 Map.Entry 객체를 만드는 새로운 팩토리 메서드다.
데이터 처리 형식을 설정하거나 데이터를 변환할 필요가 없다면 구현이 단순한 팩토리 메서드를 사용하자
- 아니라면 Collectors.toList()등 이용해서 스트림을 다양한 Collection으로 변환하는 방법을 사용하자
8.2 리스트와 집합 처리
java 8에서는 새로운 컬렉션이 아니라 기존 컬렉션을 바꾸는 동작의 에러와 복잡함을 해결하고자 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
- removeIf: Predicate를 만족하는 요소를 제거한다. List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용할 수 있다.
- replaceAll: List에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
- sort: List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
removeIf (제거)
for-each 루프는 Iterator 객체를 사용하기 때문에 위에 코드와 동일한 ConcurrentModificationException이 발생하는 코드다.
- Iterator 객체, next(), hasNext()를 이용해 소스를 질의한다.
- Collection 객체 자체, remove()를 호출해 요소를 삭제한다.
for (String str : list){ //"1test", "test", "2test", "test2"
if (Character.isDigit(str.charAt(0))){
list.remove(str);
}
}
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();){
String str = iterator.next();
if (Character.isDigit(str.charAt(0))){
list.remove(str); //반복하면서 별도의 두 객체를 통해 컬렉션을 바꾸는 문제
}
}
- 반복자의 상태가 컬렉션의 상태와 서로 동기화 되지 않아 발생하고 있다.
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();){ //Iterator 객체를 명시적으로 사용
String str = iterator.next();
if (Character.isDigit(str.charAt(0))){
iterator.remove(); //Iterator 객체의 remove() 호출
}
}
- Iterator 객체를 명시적으로 사용하고 그 객체의 remove() 메서드를 호출함으로 문제를 해결할 수 있다.
list.removeIf(str -> Character.isDigit(str.charAt(0))); //삭제할 요소를 가리키는 Predicate
removeIf 메서드로 코드가 단순해지며 버그도 예방할 수 있으며, 제거가 아니라 변경이 필요할 경우는 replaceAll을 사용하자
replaceAll (변경)
Stream을 통해서 바꾸는 것은 새 문자열 컬렉션을 만든다
- ListIterator 객체의 set 메서드를 이용해 요소를 바꿀 수 있다.
for (ListIterator<String> iterator = list.listIterator(); //"a12", "b12", "c12", "d12"
iterator.hasNext();) {
String code = iterator.next();
iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1)); //[A12, B12, C12, D12]
}
컬렉션 객체를 Iterator 객체와 혼용하면 반복과 컬렉션 변경이 동시에 이루어지면서 쉽게 문제를 일으킨다.
- replaceAll 메서드를 이용하면 간단하게 구현할 수 있다.
list.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
8.3 맵 처리
java 8에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다.
forEach 메서드
Map.Entry<K,V>의 반복자를 이용해 맵의 항목 집합을 반복할 수 있다.
for (Map.Entry<Object,Object> entry : map.entrySet()){
Object key = entry.getKey();
Object value = entry.getValue();
System.out.println("key="+key+" value="+value);
}
BiConsumer(key,value를 인수로 받음)를 인수로 받는 forEach 메서드를 이용해서 간단하게 구현할 수 있다.
map.forEach((key,value)->System.out.println("key="+key+" value="+value));
정렬 메서드
두 개의 새로운 유틸리티를 이용하면 맵의 항목을 key 또는 value를 기준으로 정렬할 수 있다.
- Entry.comparingByValue
- Entry.comparingByKey
Map<String, String> favouriteMovies = Map.ofEntries(
entry("Raphael", "Star Wars"), //3
entry("Cristina", "Matrix"), //1
entry("Olivia", "James Bond")); //2
favouriteMovies.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEachOrdered(System.out::println);
- Ascii기준으로 오름차순 정렬되는 것을 볼 수 있다.
getOrDefault 메서드
찾으려는 key가 map의 존재하지 않을면 NULL이 반환되기 때문에 NullPointException이 발생할 수 있다.
- getOrDefault 메서드를 사용하면 map의 key가 존재하지 않을 경우 기본값을 반환한다.
- key가 존재하더라도 값이 NULL인 상황에서는 getOrDefault가 NULL이 반환되기 때문에 NullPointException이 발생할 수 있다.
계산 패턴
키를 이용해 값비싼 동작을 실행해서 얻은 결과를 캐시하려고 할 때, 키가 존재하면 결과를 다시 계산할 필요가 없다.
- computeIfAbsent : 제공된 키에 해당하는 값이 없으면, 키를 이용해 새 값을 계산하고 맵에 추가 한다.
- computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
- compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.
HashMap<String, List<String>> friendsToMovies = new HashMap<>(); //{Raphael=[Star Wars]}
friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>())
.add("Star Wars");
1. friendsToMovies라는 map에서 computeIfAbsent메서드를 통해서 key를 넘겼을 때 List가 null이면 새로운 List를 만든다.
2. friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>()) 반환값이 1번에서 만든 새로운 List다.
3. 2번 반환값, 즉 List에 "Star Wars"를 추가한다.
삭제 패턴
밑에 코드와 같이 동작하는 패턴이 있다고 할 때
private static <K, V> boolean remove(Map<K, V> favouriteMovies, K key, V value) {
if (favouriteMovies.containsKey(key) && Objects.equals(favouriteMovies.get(key), value)) {
favouriteMovies.remove(key);
return true;
}
return false;
}
java 8에서는 key가 특정한 값과 연관되었을 때만 항목을 제거하는 remove가 추가 되었다.
favouriteMovies.remove(key, value);
교체 패턴
Map의 항목을 바꾸는데 사용할 수 있는 두 개의 메서드가 맵에 추가되었다.
Map<String, String> favouriteMovies = new HashMap<>();
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "james bond");
- replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다. (이전 List의 replaceAll과 비슷한 동작)
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase()); //(K,V)->V
System.out.println(favouriteMovies); //{Olivia=JAMES BOND, Raphael=STAR WARS}
- replace : key가 존재하면 map의 value를 바꾼다. (key가 특정 value로 매핑되었을 때만 값을 교체하는 것도 있음)
favouriteMovies.replace("Olivia","james bond","test"); //key, oldValue, newValue
System.out.println(favouriteMovies); //{Olivia=test, Raphael=Star Wars}
병합 패턴
두 메서드 모두 서로 다른 Map을 하나로 합치지만 중복된 키가 있는 경우에는 merge() 를 이용하자
- putAll()
- merge()
Map<String, String> family = Map.ofEntries(
entry("Teo", "Star Wars"),
entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(
entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"));
Key가 "Cristina" 중복되어 있다. 중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 넘겨주면 된다.
friends.forEach((key,value)->
everyone.merge(key,value,(everyoneMovie, friendsMovie)-> everyoneMovie+"&&"+friendsMovie));
System.out.println(everyone); //{Raphael=Star Wars, Cristina=James Bond&&Matrix, Teo=Star Wars}
- merge의 2번째 인수인 value는 1번째 인수인 key와 관련된 기존 value에 합쳐질 null이 아닌 값이다.
- 1번째 인수인 key의 value가 값이 없거나 null이면 1번째 인수 key는 2번째 인수 value와 연결되어 사용된다. (default value처럼)
8.4 개선된 ConcurrentHashMap
표준 HashMap은 비동기로 동작하지만 ConcurrentHashMap은 동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다.
- 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다.
- 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신등의 작업을 허용하기 때문이다.
리듀스와 검색
ConcurrentHashMap은 스트림에서 봤던 것과 비슷한 종류의 세 가지 새로운 연산을 지원한다.
- forEach : 각 (키,값) 쌍에 주어진 액션을 실행
- reduce : 모든 (키,값) 쌍을 제공된 리듀스 함수를 이용해 결과로 합친다.
- search : null이 아닌 값을 반환할 때까지 각 (키,값) 쌍에 함수를 적용한다.
아래 4가지 연산은 ConcurrentHashMap의 상태를 잠그지 않고 연산을 수행하기 때문에, 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하면 안된다.
- 키 값으로 연산(forEach, reduce, search)
- 키로 연산(forEachKey, reduceKeys, searchKeys)
- 값으로 연산(forEachValue, reduceValues, searchValues)
- Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
이들 연산에 병렬성 기준값(threshold)을 지정해야 한다.
- 맵의 크기가 주어진 기준값보다 작으면 순차적 연산을 실행한다.
- 기준값을 1로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화 한다.
- Long.MAX_VALUE로 기준값을 설정하면 한 개의 스레드로 연산을 실행한다.
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
map.put("max",1234L);
map.put("max",Long.MAX_VALUE);
long parallelismThreshold = 1;
Optional<Long> maxValue = Optional.ofNullable(map.reduceValues(parallelismThreshold, Long::max));
System.out.println(Long.MAX_VALUE == maxValue.get()); // true
- reduceValuesToInt, reduceValuesToLong 등을 이용해서 박싱 작업을 피하자
계수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount 메서드를 제공한다.
- 기존 size 메서드 대신 mappingCount를 사용하자!! 매핑의 개수가 int의 범위를 넘어서는 이후의 상황을 대처할 수 있다.
집합뷰
ConcurrentHashMap을 집합 뷰로 변환하는 keySet이라는 메서드를 제공한다.
- 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다.
- newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.
HashMap 성능
많은 키가 같은 해시코드를 반환하는 상황이 되면 O(n)의 시간이 걸리는 LinkedList로 버킷을 반환해야 하므로 성능이 저하된다.
최근에는 버킷이 커지면 O(log(n))의 시간이 소요되는 정렬된 트리를 이용해서 동적으로 치환해 충돌이 일어나는 요소 반환 성능을 개선했다. (하지만 key가 String, Number 클래스 같은 Comparable의 형태여야만 정렬된 트리가 지원된다.)
'책 > 모던 자바 인 액션' 카테고리의 다른 글
10. 람다를 이용한 도메인 전용 언어 (1) | 2023.08.04 |
---|---|
9. 리팩터링, 테스팅, 디버깅 (0) | 2023.08.02 |
7. 병렬 데이터 처리와 성능 (0) | 2023.07.26 |
6. 스트림으로 데이터 수집 (0) | 2023.07.21 |
5. 스트림 활용 (0) | 2023.07.17 |