Modern Java in Action - Ch.8

참고: 책 - Modern Java in Action

책 Modern Java in Action을 읽고 정리합니다. 이번 포스트에서는 Ch 8.1 ~ Ch 8.3의 내용을 읽고 정리합니다.

Ch 8. 컬렉션 API 개선
8.1 컬렉션 팩토리
- 8.1.1 리스트 팩토리
- 8.1.2 집합 팩토리
- 8.1.3 맵 팩토리
8.2 리스트와 집합 처리
- 8.2.1 removeIf 메서드
- 8.2.2 replaceAll 메서드
8.3 맵 처리
- 8.3.1 forEach 메서드
- 8.3.2 정렬 메서드
- 8.3.3 getOrDefault 메서드
- 8.3.4 계산 패턴
- 8.3.5 삭제 패턴
- 8.3.6 교체 패턴
- 8.3.7 합침

8.1 컬렉션 팩토리

Java 9에서는 작은 컬렉션 객체를 쉽게 만들 수 있는 몇 가지 방법을 제공합니다.

적은 요소를 포함하는 리스트

ex. 휴가를 함께 보내려는 친구 이름을 포함하는 그룹을 만드는 예제입니다.

//Before
List<String> friends = new ArrayList<>();
friends.add("Rachel");
friends.add("Poogle");
friends.add("Suhyun");
//After
List<String> friends = Arrays.asList("Rachel", "Poogle", "Suhyun");

//갱신 - 가능
friends.set(0, "Richard");

//UnsupportedOperationException 발생
friends.add("Solar");



8.1.1 리스트 팩토리 - List.of

image


오버로딩 vs 가변 인수

List 인터페이스를 살펴보면 List.of의 다양한 오버로드 버전이 있다는 사실을 알 수 있습니다.

image

static <E> List<E> of(E... elements)

다중요소를 받을 수 있도록 가변인수를 사용할 수도 있는데 내부적으로 가변 인수 버전은 추가 배열을 할당해서 리스트로 감싸게 됩니다. 따라서 배열을 할당하고 초기화하며 나중에 가비지 컬렉션을 하는 비용을 지불해야 합니다. 이 때 고정된 숫자의 요소(최대 10개까지)를 API로 정의하므로 이런 비용을 제거할 수 있습니다.

image

List.of()로 10개 이상의 요소를 가진 리스트를 만들게 될 때는 위와 같이 가변 인수를 이용하는 메소드가 사용됨을 알 수 있습니다. Set.of()Map.of()에서도 이와 같은 패턴이 등장함을 확인할 수 있습니다.



8.1.2 집합 팩토리

Set<String> friends = Set.of("Rachel", "Poogle", "Suhyun");


8.1.3 맵 팩토리

Map<String, Integer> ageOfFriends = Map.of("Rachel", 20, "Poogle", 26, "Suhyun", 28);
import static java.util.Map.entry;

Map<String, Integer> ageOfFriends = Map.ofEntries(entry("Rachel", 20), entry("Poogle", 26), entry("Suhyun", 28));




8.2 리스트와 집합 처리

Java 8에서는 List, Set 인터페이스에 다음과 같은 메서드를 추가했습니다.


이들 메서드는 호출한 컬렉션 자체를 바꿉니다. 새로운 결과를 만드는 스트림 동작과 달리 이들 메서드는 기존 컬렉션을 바꿉니다.



8.2.1 removeIf 메서드

ex. 숫자로 시작되는 참조 코드를 가진 트랜젝션을 삭제하는 예제입니다.

for (Transaction transaction : transactions) {
    if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        transactions.remove(transaction);
    }
} 
for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext(); ) {
    Transaction transaction = iterator.next();
    if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        transactions.remove(transaction); // 반복하면서 별도의 두 객체를 통해 컬렉션을 바꾸고 있는 문제
    }
} 
for (Iterator<Transaction> iterator = transactions.iterator(); iterator.hasNext(); ) {
    Transaction transaction = iterator.next();
    if(Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        iterator.remove(transaction); // transactions -> iterator
    }
} 
transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));


8.2.2 replaceAll 메서드

referenceCodes.stream()
              .map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1))
              .collect(Collectors.toList())
              .forEach(System.out::println);
for(ListIterator<String> iterator = referenceCodes.listIterator; iterator.hasNext(); ) {
    String code = iterator.next();
    iterator.set(Character.toUpperCase(code.charAt(0)) + code.substring(1));
}
referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));




8.3 맵 처리

8.3.1 forEach 메서드

for(Map.Entry<String, Integer> entry : ageOfFriends.entrySet()) {
    String friend = entry.getKey();
    Integer age = entry.getValue();
    System.out.println(friend + " is " + age + " years old");
}
ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " + age + " years old"));


8.3.2 정렬 메서드

Map<String, String> favoriteMovies = Map.ofEntries(entry("Rachel", "About Time"), 
                                                    entry("Poogle", "Decision to Leave"),
                                                    entry("Suhyun", "Top Gun"));

favoriteMovies.entrySet()
              .stream()
              .sorted(Entry.comparingByKey())
              .forEachOrdered(System.out::println);

결과

Poogle=Decision to Leave
Rachel=About Time
Suhyun=Top Gun


8.3.3 getOrDefault 메서드

기존에는 찾으려는 키가 존재하지 않으면 null이 반환되므로 NullPointerException을 방지하려면 요청 결과가 null인지 확인해야 합니다. 이 문제는 기본값을 반환하는 방식으로 해결할 수 있습니다. getOrDefault 메서드는 첫 번째 인수로 Key를, 두 번째 인수로 기본값을 받으며 맵에 키가 존재하지 않으면 (key가 존재하더라도 값이 null인 상황에서는 null을 반환할 수 있음) 두 번째 인수로 받은 기본값을 반환합니다.

Map<String, String> favoriteMovies = Map.ofEntries(entry("Rachel", "About Time"), 
                                                    entry("Poogle", "Decision to Leave"));
System.out.println(favoriteMovies.getOrDefault("Rachel", "Get Out")); //About Time 출력
System.out.println(favoriteMovies.getOrDefault("Suhyun", "Get Out")); //Get Out 출력


8.3.4 계산 패턴

맵에 키가 존재하는지 여부에 따라 동작을 실행하고 결과를 저장해야 하는 상황이 필요한 때가 있습니다. (ex. 키를 이용해 값비싼 동작을 실행해서 얻은 결과를 캐시, 키가 존재하면 결과를 다시 계산할 필요가 없음) 다음의 세 가지 연산이 이런 상황에서 도움을 줍니다.

ex. 파일 집합의 각 행을 파싱해 SHA-256을 계산하는 예제입니다.

Map<String, byte[]> dataToHash = new HashMap<>();
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");

//데이터 반복하면서 결과를 캐시, line: 키 & 키가 존재하지 않으면 동작 실행
lines.forEach(line -> dataToHash.computeIfAbsent(line, this::calculateDigest));

//헬퍼가 제공된 키의 해시를 계산
private byte[] calculateDigest(String key) {
    return messageDigest.digest(key.getBytes(StandardCharsets.UTF_8));
}

ex. Rachel에게 줄 영화 목록을 만드는 예제

String friend = "Rachel";
List<String> movies = friendsToMovies.get(friend);
if(movies == null) {
    movies = new ArrayList<>();
    friendsToMovies.put(friend, movies);
}
movies.add("Star Wars");

System.out.println(friendsToMovies);
friendsToMovies.computeIfAbsent("Rachel", name -> new ArrayList<>())
                .add("Star Wars");


8.3.5 삭제 패턴

Java 8에서는 키가 특정한 값과 연관되었을 때만 항목을 제거하는 오버로드 버전 메서드를 제공합니다.

String key = "Rachel";
String value = "Jack Reacher 2";
if (favoriteMovies.containsKey(key) && Object.equals(favoriteMovies.get(key), value)) {
    favoirteMovies.remove(key);
} else {
    return false;
}

다음처럼 코드를 간결하게 구현할 수 있습니다.

favoriteMovies.remove(key, value);


8.3.6 교체 패턴



8.3.7 합침

ex. 두 그룹의 연락처를 포함하는 두 개의 맵을 합치는 예제입니다.

Map<String, String> family = Map.ofEntries(
    entry("Teo", "Star Wars"),
    entry("Cristina", "James Bond"));
Map<String, String> friends = Map.ofEntries(entry("Raphael", "Star Wars"));

Map<String, String> everyone = new HashMap<>(family);
everyone.putAll(friends); //friends의 모든 항목을 everyone으로 복사
System.out.println(everyone);
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);
friends.forEach((k, v) -> everyone.merge(k, v, (movie1, movie2) -> movie1 + " & " + movie2)); //중복된 키가 있으면 두 값을 연결
System.out.println(everyone);


ex. 영화를 몇 회 시청했는지 기록하는 맵 예제

Map<String, Long> moviesToCount = new HashMap<>();
String movieName = "AboutTime";
long count = moviesToCount.get(movieName);
if(count == null) {
    moviesToCount.put(movieName, 1);
} else {
    moviesToCount.put(movieName, count + 1);
}
moviesToCount.merge(movieName, 1L, (key, count) -> count + 1L);