CHAP08 - Modern-Java-in-Action/Online-Study GitHub Wiki
- 작은 리스트, 집합, 맵을 쉽게 만들 수 있도록 개선된 컬렉션 팩토리
- 리스트와 집합에서 요소를 삭제하거나 바꾸는 관용 패턴 적용 방법
- 맵 작업과 관련해 추가된 새로운 편리 기능
-
팩토리 메서드 : 객체 생성 코드를 별도 클래스/메서드로 분리하여 객체 생성의 변화에 대비하는 데 유용하다.
-
new Integer(0)
대신 사용하는Intege.valueOf(0)
- Design Pattern 팩토리 메서드 패턴이란 - Heee's Development Blog (gmlwjd9405.github.io)
-
-
객체를 final 키워드로 선언해도, 내부 데이터는 바꿀 수 있다.
final Person p = new Person("Bob", 30); //p = new Person("John", 25); p.name = "John"; p.age = 25; private static class Person{ public String name; public int age; Person(String name, int age){ this.name = name; this.age = age; } }
- 자바 9는 적은 원소를 포함하며 바꿀 수 없는 리스트, 집합, 맵을 쉽게 만들 수 있도록 List.of, Set.of, Map.of, Map.ofEntries 등 컬렉션 팩토리를 지원한다.
- List 인터페이스에서 removeIf, replaceAll, sort 세 가지 디폴트 메서드를 지원한다.
- Set 인터페이스는 removeIf 디폴트 메서드를 지원한다.
- Map 인터페이스는 자주 사용하는 패턴과 버그를 방지할 수 있도록 다양한 디폴트 메서드를 지원한다.
- ConcurrentHashMap은 Map에서 상속받은 새 디폴트 메서드를 지원함과 동시에 스레드 안전성도 제공한다.
구분 | 메서드 | 설명 |
---|---|---|
공통 | List.of, Set.of, Map.of | 바꿀 수 없는 리스트/집합/맵 객체를 생성한다. |
리스트/집합 | removeIf | 프리디케이트를 만족하는 요소를 제거 |
replaceAll | UnaryOperator 함수로 요소를 바꿈 | |
sort | 정렬 | |
맵 | forEach | Biconsumer를 넘겨 키와 값을 반복하여 처리 |
sorted | 정렬 | |
getOrDefault | 찾으려는 키가 존재하지 않을때 반환할 기본값을 지정 | |
computeIfAbsent | 제공된 키에 해당하는 값이 없으면, 키로 새 값을 계산하고 맵에 추가 | |
computeIfPresent | 제공된 키에 해당하는 값이 있으면, 키를 이용해 새 값을 계산하고 변경 | |
compute | 제공된 키로 새 값을 계산하고 저장 | |
remove | 키/값을 인자로 받아 해당되는 데이터를 삭제 | |
replaceAll | BiFunction을 적용한 결과로 각 항목의 값을 교체 | |
replace | 키가 존재하면 맵의 값을 바꿈 | |
merge | 키, 값, 키 중복시 처리할 BiFunctino을 인자로 넘긺 | |
ConcurrentHashMap | forEach | 키/값 쌍에 주어진 액션을 실행 |
reduce | 모든 키/값 쌍을 제공된 리듀스 함수를 이용해 합침 | |
search | null 아닌 값을 반환할 때까지 키/값에 함수를 적용 | |
mappingCount | long을 반환(기존 size는 int) | |
keySet | 키값을 집합으로 변환, 집합/맵은 서로 변경사항이 동기화됨 |
maven 프로젝트에서 pom.xml에 아래와 같이 의존성을 추가한다.
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.17.4</version>
</dependency>
Faker 객체를 생성해 임의의 데이터를 생성할 수 있다.
Faker faker = new Faker();
documents = new HashMap<>();
for(int i=0; i<10; i++){
documents.put(faker.dune().character(), faker.dune().planet());
}
documents.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(System.out::println);
documents.forEach((name, address)->System.out.println(name+" : "+address));
System.out.println(documents.getOrDefault("Chani", "NOT_FOUND_CHANI"));
지원하는 데이터셋 : 주소, 이름, 항공기, 코인, 고양이, 색 등 82종
DiUS/java-faker: Brings the popular ruby faker gem to Java (github.com)
java faker 로 테스트 데이타 만들기 (lesstif.com)
](https://www.lesstif.com/java/java-faker-48988763.html)
HashMap<String, String> documents = new HashMap<>();
Faker faker = new Faker();
for(int i=0; i<100000; i++){
documents.put(faker.name().fullName(), faker.address().fullAddress());
}
//documents.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEachOrdered(System.out::println);
//documents.forEach((name, address)->System.out.println(name+" : "+address));
//System.out.println(documents.getOrDefault("Chani", "NOT_FOUND"));
//documents.entrySet().stream().parallel().sorted(Map.Entry.comparingByKey()).limit(10).forEachOrdered(System.out::println);
System.out.println("# filter startWith A, limit 10");
documents.entrySet().stream().filter(e -> e.getKey().startsWith("A")).limit(10).forEach(System.out::println);
System.out.println("\n# distinct, limit 10");
documents.entrySet().stream().distinct().limit(10).forEach(System.out::println);
System.out.println("\n# map address -> length, limit 10");
documents.entrySet().stream().map(e -> e.getValue().length()).limit(10).forEach(System.out::println);
System.out.println("\n# collect, joining string");
System.out.println(documents.keySet().stream().limit(10).collect(joining(" ")));
System.out.println("\n# collect, reducing for joining string");
System.out.println(documents.keySet().stream().limit(10).collect(reducing("", (o, v)->o+","+v)));
System.out.println("\n# reduce for joining string");
System.out.println(documents.keySet().stream().limit(10).reduce("", (o, v)->o+","+v));
System.out.println("\n# replaceAll name toUpperCase, limit 10");
documents.replaceAll((k,v)->k.toUpperCase());
documents.entrySet().stream().limit(10).forEach(System.out::println);
- Arrays.asList로 만든 리스트는 데이터가 final 배열에 담겨 있다.
- 데이터를 추가할 수 없고, 내부 데이터는 바꿀 수 있다.
기존 자바에서 아래 코드는 UnsupportedException
을 뱉는다.
List<String> friends2 = Arrays.asList("Rael", "Oli");
friends2.set(0, "Rich");
friends2.add("Thiba"); //예외발생
왜냐하면 Arrays.asList
는 자체 정의한 private ArrayList를 반환하기 때문이다. 고정크기 final 배열을 만들어 초기화한 후에는 새 배열로 바꾸지 못하게 했고, 내부 데이터만 변경할 수 있다. 그래서 AbstractList 추상클래스는 set, add 모두 UnsupportedException
예외를 던지는데, 그중 set 메서드만 재정의해두었다.
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
}
- List.of로 만든 리스트도 데이터가 final 배열에 담겨 있다.
- 데이터를 추가/수정/삭제하는 모든 메서드가 예외를 던진다.
- 생성자에서 10개 이하 인자는 고정크기 배열로 데이터를 받는다. 인자 개수가 그 이상인 경우 배열크기를 늘려가며 데이터를 받고, 가비지 컬렉션 비용이 추가로 들어간다.
다음 코드에서도 add/set 메서드 호출시 UnsupportedException
을 뱉는다.
List<String> friends = List.of("Rael", "Oli", "Thiba");
friends.add("Taey"); //예외발생
friends.set(0, "Taey"); //예외발생
List.of
는 ImmutableCollections.ListN<>()
를 반환한다. ListN은 AbstractImmutableList을 상속하는 정적 클래스이고, AbstractImmutableList는 아래와 같이 final 배열로 데이터를 담고, 리스트를 수정하는 모든 메서드에서 예외를 던지도록 설계되어 있다.
static final class ListN<E> extends AbstractImmutableList<E>
implements Serializable {
static final List<?> EMPTY_LIST = new ListN<>();
@Stable
private final E[] elements;
}
static abstract class AbstractImmutableList<E> extends AbstractImmutableCollection<E>
implements List<E>, RandomAccess {
// all mutating methods throw UnsupportedOperationException
// uoe()는 UnsupportedException을 던지는 메서드
@Override public void add(int index, E element) { throw uoe(); }
@Override public boolean addAll(int index, Collection<? extends E> c) { throw uoe(); }
@Override public E remove(int index) { throw uoe(); }
@Override public void replaceAll(UnaryOperator<E> operator) { throw uoe(); }
@Override public E set(int index, E element) { throw uoe(); }
@Override public void sort(Comparator<? super E> c) { throw uoe(); }
}
- Set.of로 바꿀 수 없는 집합을 만들 수 있다.
- 중복된 데이터를 넘기면 예외를 던진다.
Set<String> friends = Set.of("Rael", "Oli", "Thiba");
Set<String> friends2 = Set.of("Rael", "Oli", "Thiba", "Rael"); //예외발생
//Stream을 사용해 유일한 값을 추출할 수는 있지만, 고정크기로 가벼운 집합을 만들려는 의도에서 한참 벗어나 버린다.
Set<String> friends3 = List.of("Rael", "Oli", "Thiba", "Rael").stream().distinct().collect(toSet());
- Map.of로 바꿀 수 없는 맵을 만들 수 있다.
- entry는 Map.Entry 객체를 만드는 팩토리 메서드이며, 내부 데이터를 변경할 수 없다.
Map<String, Integer> ageOfFriends = Map.of("Rael", 30, "Oli", 25, "Thiba", 26);
Map<String, Integer> ageOfFriends2 = Map.ofEntries(
entry("Rael", 30),
entry("Oli", 25),
entry("Thiba", 26)
);
다음 코드의 실행결과는?
List<String> actors = List.of("Keanu", "Jessica");
actors.set(0, "Brad");
System.out.println(actors);
- removeIf : 프리디케이트를 만족하는 요소를 제거, List나 Set을 구현하거나 그 구현을 상속받은 모든 클래스에서 이용.
removeIf를 사용하지 않을 때
컬렉션에서 특정 객체를 골라서 삭제하려면? for-each는 내부적으로 Iterator를 사용한다. 리스트를 순회하는 데 Iterator를 사용하고, 리스트에서 데이터를 삭제하는 데 컬렉션으로 접근하면 두 객체간 동기화가 이뤄지지 않아 오류가 발생한다. 그래서 아래 코드는 ConcurrentModificationException
을 던진다.
TestData td = new TestData();
for(Person p : td.people){
if(p.age > 50) td.people.remove(p);
}
위 코드는 아래와 같이 해석된다.
for(Iterator<Peorson> itr = td.people.iterator(); iterator.hasNext(); ){
Person p = itr.next();
/* 생략 */
}
의도한 대로 작동시키려면 아래처럼 명시적으로 Iterator를 사용하고 remove 메서드를 호출해야 한다.
for(Iterator<Peorson> itr = td.people.iterator(); iterator.hasNext(); ){
Person p = itr.next();
if(p.age > 50) itr.remove();
}
removeIf를 사용할 때
프리디케이트를 인수로 넘겨 간단히 처리할 수 있다.
td.people.removeIf(p -> p.age>50);
- replaceAll : 리스트에서 이용할 수 있는 기능으로 UnaryOperator 함수를 이용해 요소를 바꿈.
replaceAll을 사용하지 않을 때
아래와 같이 스트림을 사용해 매핑할 수 있지만 새로운 문자열 리스트를 생성한다.
TestData td = new TestData();
td.people.stream()
.map(p -> p.name.toLowerCase())
.collect(Collectors.toList())
.forEach(System.out::println);
기존 컬렉션을 바꾸려면 아래와 같이 Iterator의 set() 메서드를 사용해야한다.
for(ListIterator<Person> itr = td.people.listIterator(); itr.hasNext(); ){
Person p = itr.next();
itr.set(changeName(p));
}
replaceAll을 사용할 때
td.people.replaceAll(ListSetProcessor::changeName);
td.people.sort((o1, o2) -> Integer.compare(o1.age, o2.age));
아래 코드를 짧게 줄이려면?
Map<String, Integer> movies = new HashMap<>();
movies.put("JamesBond", 20);
movies.put("Matrix", 15);
movies.put("Harry Potter", 5);
Iterator<Map.Entry<String, Integer>> iterator = movies.entrySet().iterator();
while(iterator.hasNext()){
Map.Entry<String, Integer> entry = iterator.next();
if(entry.getValue() < 10){
iterator.remove();
}
}
정답
movies.entrySet().removeIf(e -> e.getValue()<10);
맵에서 키와 값을 반복하여 확인하는 작업은 for문과 entrySet을 사용한다.
Map<String, Integer> ageOfFriends = new HashMap<>();
ageOfFriends.put("김이름", 30);
ageOfFriends.put("이익명", 40);
ageOfFriends.put("박닉네임", 50);
for(Map.Entry<String, Integer> entry : ageOfFriends.entrySet()){
String name = entry.getKey();
Integer age = entry.getValue();
System.out.println(name + " is " + age + " years old.");
}
이제는 forEach에 키와 값을 인수로 받는 BiConsumer를 넘겨 처리할 수 있다.
ageOfFriends.forEach((name, age)->System.out.println(name+" is "+age+" years old."));
HashMap을 정렬할 때, EntrySet을 별도 Collection 객체로 만들어서 Collections.sort로 정렬하지 않아도 된다. 아래와 같이 sorted 메서드를 사용하면 된다.
Map<String, String> favouriteMoview = Map.ofEntries(
entry("Rahpael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James bond"));
favouriteMoview
.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
//.forEach(System.out::println);
.forEachOrdered(System.out::println);
자바8에서 HashMap은 많은 키가 같은 해시값을 반환하는 경우, O(logn) 시간이 소요되는 정렬된 트리를 이용해 동적으로 치환하여 성능을 향상시켰다.
기존에는 찾으려는 키가 존재하지 않으면 null이 반환되므로 NullPointerException이 발생할 수 있었다. getOrDefault로 기본값을 지정해줌으로써 이 문제를 해결할 수 있다.
Map<String, String> favouriteMovies = Map.ofEntries(
entry("Rahpael", "Star Wars"),
entry("Cristina", "Matrix"));
System.out.println(favouriteMovies.getOrDefault("Olivia", "Matrix"));
System.out.println(favouriteMovies.getOrDefault("Timothy", "Dune"));
단, 키는 존재하고 값은 null인 경우 위 코드로도 NullPointerException을 마주할 수 있다.
- computeIfAbsent : 제공된 키에 해당하는 값이 없으면, 키를 이용해 새 값을 계산하고 맵에 추가
- computeIfPresent : 제공된 키에 해당하는 값이 있으면, 키를 이용해 새 값을 계산하고 값을 변경
- compute : 제공된 키로 새 값을 계산하고 맵에 저장한다.
파일 한줄한줄 해시값을 계산해 저장하는 컬렉션. computeIfAbsent로 이미 읽어온 줄인지 확인하고 해시값 계산해 추가.
try (Stream<String> lines = Files.lines(Paths.get("D:\\DailyLogs\\README.md"), Charset.defaultCharset())){
Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
this.messageDigest = messageDigest;
lines.forEach(line -> dataToHash.computeIfAbsent(line, this::calculateDigest));
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
}
친구이름/영화목록 컬렉션. 친구 이름을 검색해 없으면 새 영화목록을 생성해 추가.
Map<String, List<String>> friendsToMovies = Map.ofEntries();
String friend = "Raphael";
List<String> movies = friendsToMovies.get(friend);
if(movies==null){
movies = new ArrayList<>();
friendsToMovies.put(friend, movies);
}
movies.add("Star Wars");
Map<String, List<String>> friendsToMovies2 = Map.ofEntries();
friendsToMovies2.computeIfAbsent("Rahpael", name -> new ArrayList<>())
.add("Star Wars");
기존에는 아래와 같이 키를 인자로 받는 remove 메서드를 사용했는데, 그전에 해당 키가 컬렉션에 존재하는지 그리고 삭제하려는 데이터의 키/값과 비교해야 했다.
Map<String, String> favouriteMovies = new HashMap<>(Map.ofEntries(
entry("Rahpael", "Star Wars"),
entry("Cristina", "Matrix"),
entry("Olivia", "James bond")));
String key = "Raphael";
String value = "Jack Reacher 2";
if(favouriteMovies.containsKey(key) && Objects.equals(favouriteMovies.get(key), value)){
favouriteMovies.remove(key);
}
이제 키/값을 인자로 넘겨 삭제할 수 있다.
favouriteMovies.remove(key, value);
- replaceAll : BiFunction을 적용한 결과로 각 항목의 값을 교체한다. 이 메서드는 이전에 살펴본 List의 replaceAll가 비슷한 동작을 한다.
- replace : 키가 존재하면 맵의 값을 바꾼다. 키가 특정 값으로 매핑되었을 때만 값을 교체하는 오버로드 버전도 있다.
favouriteMovies.replaceAll((friend, movie) -> movie.toUpperCase());
두 맵을 합칠 때 중복된 키/값이 있다면 의도하지 않은 오류가 발생할 수 있다.
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"));
Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends);
BiFunction을 인자로 넘겨 중복된 키를 어떻게 합칠지 미리 정할 수 있다.
Map<String, String> everyone2 = new HashMap<>(family);
friends.forEach((k,v)->everyone2.merge(k,v,(movie1, movie2)->movie1+" & "+movie2));
또는 아래와 같이 초기값을 정하고 값을 갱신해나갈 수도 있다.
moviesToCount.merge(movieName, 1L, (k, v)->v+1L);
- 내부 자료구조의 특정 부분만 잠궈 동시 추가, 갱신 작업을 할 수 있다.
- 동기화된 Hashtable에 비해 읽기 쓰기 연산 성능이 월등하다. (참고로 HashMap은 비동기로 동작)
-
연산종류
-
forEach 키/값 쌍에 주어진 액션을 실행
-
redue 모든 키/값 쌍을 제공된 리듀스 함수를 이용해 결과로 합침
-
search 널이 아닌 값을 반환할 때까지 키/값에 함수를 적용
-
-
연산방법
- 키, 값으로 : forEach, reduce, search
- 키로 : forEachKey, reduceKeys, searchKeys
- 값으로 : forEachValue, reduceValues, searchValues
- Map.Entry 객체로 : forEachEntry, reduceEntries, searchEntries
위 연산들은 ConcurrentHashMap의 상태를 잠그지 않는다. 이들 연산에 제공한 함수는 진행되는 동안 바뀔 수 있는 객체, 값, 순서 등에 의존하지 않아야 한다.
아울러 첫번째 인수로 병렬성 기준값(threshold)을 정해줘야 하는데, 기준값이 1이면 병렬성을 극대화하고 Long.MAX_VALUE이면 한 개 스레드로 연산을 실행한다.
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
long parallelismThreshod = 1;
Optional<Long> maxValue = Optional.ofNullable(map.reduceValues(parallelismThreshod, Long::max));
필요시 기본값 전용 연산을 사용하자. (reduceValuesToInt, reduceKeysToLong 등)
- mappingCount : long을 반환함
- size : int를 반환
concurrentHashMap 크기가 int값을 넘어갈 때를 대비할 수 있다.
- keySet : 키값을 집합으로 볼 수 있다. 집합/맵 변경사항이 상호간 적용된다. 단, 집합에 값을 추가하려면 keySet 생성시 인자로 기본값을 넘겨주어야 한다.
- newKeySet : 새 맵 객체와 집합뷰를 동시에 생성한다. 기본값을 TRUE로 지정해준다.
Set<String> set = map.keySet(0L);
System.out.println(map);
System.out.println(set);
set.add("BYE");
System.out.println(map);
System.out.println(set);
map.replace("A", 100L);
map.put("HELLO", 1000L);
System.out.println(map);
System.out.println(set);
keySet은 CollectinView를 상속하는 keySetView를 반환한다.
-
CollectionView 메서드
- clear() size() isEmpty() iterator(), containse(), remove(), toArray(), toString(), containsAll(), removeAll(), retainAll()
-
keySetView 메서드
-
keySetView 생성자에서 디폴트 값을 인자로 받는다. 만약 객체 생성시 디폴트 값을 정해주지 않으면 아래와 같이 예외를 발생시킨다.
-
public boolean add(K e) { V v; if ((v = value) == null) throw new UnsupportedOperationException(); return map.putVal(e, v, true) == null; }
-
System.out.println("#1 ==========================================");
ConcurrentHashMap.KeySetView<String, Long> set = map.keySet(0L);
System.out.println(map);
System.out.println(set);
System.out.println(set.getMappedValue());
System.out.println("#2 ==========================================");
ConcurrentHashMap.KeySetView<String, Long> kset = map.keySet();
System.out.println(kset.getMappedValue());
System.out.println("#3 ==========================================");
set.add("BYE");
System.out.println(map);
System.out.println(set);
System.out.println("#4 ==========================================");
map.replace("A", 100L);
map.put("HELLO", 1000L);
System.out.println(map);
System.out.println(set);
System.out.println("#5 ==========================================");
ConcurrentHashMap.KeySetView<String, Boolean> keySet = ConcurrentHashMap.newKeySet();
keySet.add("HELLO");
System.out.println(keySet);
System.out.println("#6 ==========================================");
ConcurrentHashMap.KeySetView<String, Boolean> keySet2 = ConcurrentHashMap.newKeySet(10);
keySet.add("HELLO");
System.out.println(keySet);
System.out.println(keySet.size());