컬렉션 팩토리
Java 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 몇 가지 방법을 제공한다.
List<String> friends = new ArrayList<>();
friends.add("Raphael");
friends.add("Olivia");
friends.add("Thibaut");
세 문자열을 저장하는데도 많은 코드가 필요하다
Arrays.asList()
메서드를 이용하면 코드를 간단하게 줄일 수 있다.
List<String> friends = Arrays.asList("Rapheal", "Olivia", "Thibaut");
고정 크기의 리스트를 만들었으므로 요소를 갱신할 순 있지만 새 요소를 추가하거나 요소를 삭제할 순 없다.
List<String> friends = Arrays.asList("Raphael", "Olivia");
friends.set(0, "Richard");
friends.add("Thibaut");
위 코드의 경우 내부적으로 고정된 크기의 변환할 수 있는 배열로 구현되었기 때문에 UnsupportedOperationException
이 발생한다.
리스트(List) 팩토리
List.of
팩토리 메서드를 이용해서 간단하게 리스트를 만들 수 있다.
List<String> friends = List.of("Raphael", "Olivia", "Thibaut");
하지만 위 코드에 새요소를 추가하면 java.lang.UnsupportedOperationException
이 발생한다.
사실 변경할 수 없는 리스트가 만들어졌기 때문이다. set()
메서드로 아이템을 바꾸려해도 비슷한 예외가 발생한다.
이러한 제약이 꼭 나쁜 것은 아니다. 컬렉션이 의도치 않게 변하는 것을 막을 수 있기 때문이다.
하지만 요소 자체가 변하는 것을 막을 수 있는 방법은 없다. 리스트를 바꿔야 하는 상황이라면 직접 리스트를 만들면 된다.
오버로딩 vs 가변 인수
List 인터페이스를 조금 더 살펴보면List.of
의 다양한 오버로드 버전이 있다는 사실을 알 수 있다.static <E> List<E> of(E e1, E e2, E e3, E e4)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)
여기서 다중 요소를 받을 수 있도록 다음과 같이 가변 인수를 사용하지 않았을까?static <E> List<E> of(E... elements)
내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싼다.
따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용일 지불해야 한다.
고정된 숫자의 요소를 API로 정의하므로 이런 비용을 제거할 수 있다.
집합(Set) 팩토리
List.of
와 비슷한 방법으로 바꿀 수 없는 집합을 만들 수 있다.
Set<String> friends = Set.of("Raphael", "Olivia", "Thibaut");
중복된 요소를 제공해 집합을 만들려고 하면 요소가 중복되어 있다는 설명과 함께 IllegalArgumentException이 발생한다.
맵(Map) 팩토리
맵을 만드는 것은 리스트나 집합을 만드는 것에 비해 조금 복잡한데 맵을 만들려면 키와 값이 있어야 하기 때문이다.
열 개 이하의 키와 값 쌍을 가진 작은 맵을 만들 때는 Map.of
팩토리 메서드가 유용하다.
Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
그 이상의 맵에서는 Map.Entry<K, V>
객체를 인수로 받으며 가변 인수로 구현된 Map.ofEntries
팩토리 메서드를 이용하는 것이 좋다.
import static java.util.Map.entry;
Map<String, Integer> ageOfFriends = Map.ofEntries(entry("Raphael", 30),
entry("Olivia", 25),
entry("Thibaut", 26));
리스트와 집합 처리
자바8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했다.
- removeIf : 프레디케이트를 만족하는 요소를 제거한다. List나 Set을 구현하거나 그 구현을 상송받은 모든 클래스에서 이용할 수 있다.
- replaceAll : 리스트에서 사용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꾼다.
- sort : List 인터페이스에서 제공하는 기능으로 리스트를 정렬한다.
removeIf 메서드
다음은 숫자로 시작되는 참조 코드를 가진 트랜잭션을 삭제하는 코드다.
for(Transaction transaction : transactions) {
if(Character.isDigit(transaction.getReferenceCode().charAt(0))){
transactions.remove(transaction);
}
}
위 코드는 ConcurrentModificationException
을 일으킨다.
이유는 내부적으로 for-each 루프는 Iterator 객체를 사용하므로 반복자의 상태와 컬랙션의 상태가 서로 동기화되지 않는다.
때문에 이 패턴은 Java 8의 removeIf()
메서드로 바꿀 수 있다.
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));
replaceAll 메서드
List 인터페이스의 replaceAll()
메서드를 이용해 리스트의 각 요소를 새로운 요소로 바꿀 수 있다.
이전에 설명한 것처럼 컬렉션 객체를 Iterator 객체와 혼용하면 반복과 컬렉션 변경이 동시에 이루어지면서 쉽게 문제를 일으킨다.
Java 8의 기능을 이용하면 다음처럼 간단하게 구현할 수 있다.
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
맵 처리
Java 8에서는 Map 인터페이스에 몇 가지 디폴트 메서드를 추가했다.
forEach 메서드
Map 인터페이스는 BiConsumer(키와 값을 인수로 받음)를 인수로 받는 forEach 메서드를 지원하므로 코드를 조금 더 간단하게 구현할 수 있다.
ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " + age + " years old"));
정렬 메서드
다음 두 개의 새로운 유틸리티를 이용하면 맵의 항목을 값 또는 키를 기준으로 정렬할 수 있다.
- Entry.comparingByVaule
- Entry.comparingByKey
Map<String, String> favouriteMovies = ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James Bond"));
favouriteMovies
.entrySet()
.stream()
.sorted(Entry.comparingByKey())
.forEachOrdered(System.out::println); // 사람의 이름을 알파벳 순으로 스트림 요소를 처리한다.
HashMap 성능
Java 8에서는 HashMap의 내부 구조를 바꿔 성능을 개선했다.
기존에 맵 항목은 키로 생성한 해시코드로 접근할 수 있는 버켓에 저장했다.
많은 키가 같은 해쉬코드를 반환하는 상황이 되면 O(n)의 시간이 걸리는 LinkedList로 버킷을 반환해야 하므로 성능이 저하된다.
최근에는 버킷이 너무 커질 경우 이를 O(log(n))의 시간이 소요되는 정렬된 트리를 이용해 동적으로 치환해 충돌이 일어나는 요소 반환 성능을 개선했다. 하지만 키가 String, Number 클래스 같은 Comparable의 형태여야만 정렬된 트리가 지원된다.
getOrDefault 메서드
기존에는 찾으려는 키가 존재하지 않으면 널이 반환되므로 NullPointerException
을 방지하려면 요청 결과가 널인지 확인해야 한다. 기본값을 반환하는 방식으로 이 문제를 해결할 수 있다.
getOrDefault()
메서드를 이용하면 쉽게 이 문제를 해결할 수 있다. 첫 번째 인수로 키를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 두 번째 인수로 받은 기본값을 반환한다.
Map<String, String> favouriteMovies = ofEntries(entry("Raphael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James Bond"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix")); // James Bond 출력
System.out.println(favouriteMovies.getOrDefault("Thibaut", "Matrix")); // Matrix 출력
계산 패턴
맵에 키가 존재하는지 여부에 따라 어떤 동작을 실행하고 결과를 저장해야 하는 상황이 필요한 때가 있다.
다음의 세 가지 연산이 이런 상황에서 도움을 준다.
- computeIfAbsent : 제공된 키에 해당하는 값이 없으면(값이 없거나 널), 키를 이용해 새 값을 계산하고 맵에 추가한다.
- computeIfPresent : 제공된 키가 존재하면 새 값을 계산하고 맵에 추가한다.
- compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.
computeIfAbsent()
는 키가 존재하지 않으면 값을 계산해 맵에 추가하고 키가 존재하면 기존 값을 반환한다.
Map<String, List<String>> friendsToMovies = new HashMap<>();
friendsToMovies.computeIfAbsent("Raphael", name -> new ArrayList<>()).add("Star Wars");
computeIfPresent()
는 현재 키와 관련된 값이 맵에 존재하며 널이 아닐 때만 새 값을 계산한다.
삭제 패턴
Java 8에서는 키가 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공한다.
favouriteMovies.remove(key, value);
교체 패턴
- replaceAll : BiFucntion을 적용한 결과로 각 항목의 값을 교체한다. 이 메서드는 이전에 살펴본 List의 replaceAll과 비슷한 동작을 수행한다.
- Replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.\
Map<String, String> favouriteMovies = new HashMap<>(); //replaceAll을 적용할 것이므로 바꿀 수 있는 맵을 사용해야 한다.
favouriteMovies.put("Raphael", "Star Wars");
favouriteMovies.put("Olivia", "james bond");
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
System.out.println(favouriteMovies); // Olivia=JAMES BOND, Raphael=STAR WARS
합침
두 그룹의 연락처를 포함하는 두 개의 맵을 합친다고 가정하자. 다음처럼 putAll()을 사용할 수 있다.
Map<String, String> family = ofEntries(entry("Teo", "Star Wars"),
entry("Cristina", "James Bond"));
Map<String, String> friends = ofEntries(entry("Raphael", "Star Wars"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends); // friends의 모든 항목을 everyone으로 복사
System.out.println(everyone); // Cristina=James Bond, Raphael=Star Wars, Teo=Star Wars
중복된 키가 없다면 위 코드는 잘 동작한다.
값을 좀 더 유연하게 합쳐야 한다면 merge()
메서드를 이용할 수 있다. 이 메서드는 중복된 키를 어떻게 합칠지 결정하는 BiFunction을 인수로 받는다.
family와 friends 두 맵 모두에 Cristina가 다른 영화 값으로 존재한다고 가정하자.
Map<String, String> family = ofEntries(entry("Teo", "Star Wars"),
entry("Cristina", "James Bond"));
Map<String, String> friends = ofEntries(entry("Raphael", "Star Wars"),entry("Cristina", "Matrix"));
forEach()
와 merge()
메서드를 이용해 충돌을 해결할 수 있다.
Map<String, String> everyone = new HashMap<>(family);
friends.forEach((k, v) -> everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2)); // 중복된 키가 있으면 두 값을 연결
System.out.println(everyone); //Raphael=Star Wars, Cristina=James Bond & Matrix, Teo=Star Wars
ConcurrentHashMap
ConcurrentHashMap 클래스는 동시성 친화적이며 최신 기술을 반영한 HashMap 버전이다.
ConcurrentHashMap은 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 허용한다.
따라서 동기화된 Hashtable 버전에 비해 읽기 쓰기 연산 성능이 월등하다.
리듀스와 검색
- forEach : 각 (키,값) 쌍에 주어진 액션을 실행
- reduce : 모든 (키, 쌍) 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
- search : 널이 아닌 값을 반환할 때까지 각 (키, 값) 쌍에 함수를 적용
다음처럼 키에 함수 받기, 값, Map.Entry, (키, 값) 인수를 이용한 네 가지 연산 형태를 지원한다.
- 키 값으로 연산(forEach, reduce, search)
- 키로 연산(forEachKey, reduceKeys, searchKeys)
- 값으로 연산(forEachValue, reduceValues, searchValues)
- Map.Entry 객체로 연산(forEachEntry, reduceEntries, searchEntries)
이들 연산은 ConcurrentMap의 상태를 잠그지 않고 연산을 수행한다는 점을 주목하자.
따라서 이들 연산에 제공한 함수는 계산이 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
또한 이들 연산에 병렬성 기준값을 지정해야 한다.
맵의 크기가 주어진 기준값보다 작으면 순차적으로 연산을 실행한다. 기준값을 1로 지정하면 공통 스레드 풀을 이용해 병렬성을 극대화한다. Long.MAX_VALUE를 기준값으로 설정하면 한 개의 스레드로 연산을 실행한다. 여러분의 소프트웨어 아키텍처가 고급 수준의 자원 활용 최적화를 사용하고 있지 않다면 기준값 규칙을 따르는 것이 좋다.
계수
ConcurrentHashMap 클래스는 맵의 매핑 개수를 반환하는 mappingCount
메서드를 제공한다.
기존의 size 메서드 대신 새 코드에선 int를 반환하는 mappingCount
메서드를 사용하는 것이 좋다. 그래야 매핑의 개수가 int의 범위를 벗어너는 이후의 상황을 대처할 수 있기 때문이다.
집합뷰
ConccurentHashMap 클래스는 ConcurrentHashMap을 집합 뷰로 반환하는 keySet이라는 새 메서드를 제공한다. 맵을 바꾸면 집합도 바뀌고 반대로 집합을 바꾸면 맵도 영향을 받는다.
newKeySet이라는 새 메서드를 이용해 ConcurrentHashMap으로 유지되는 집합을 만들 수도 있다.
참고 자료 : https://ebook-product.kyobobook.co.kr/dig/epd/ebook/E000002942391
'백엔드 > Java' 카테고리의 다른 글
[Java] 병렬 스트림 (parallel stream) (0) | 2023.06.22 |
---|---|
[Java] 리플렉션(Reflection) (0) | 2023.05.25 |
[Java] 프록시 패턴(Proxy Pattern) (0) | 2023.05.24 |
[Java] JVM의 Class Loader (0) | 2023.05.23 |
[Java] 상속(Inheritance)과 합성(Composition) (1) | 2023.05.19 |