본문 바로가기
Dev Log/Java

Modern Java In Action 정리 - 8장 컬렉션 API 개선

by 삽질하는큐 2020. 6. 6.

Collection 팩토리 메소드

가변 리스트 만들기

List<String> friends1 = new ArrayList<>();
friends1.add("Raphael");
friends1.add("Olivia");
friends1.add("Thibaut");

 

고정 크기의 리스트 만들기

List<String> friends2 = Arrays.asList("Raphael", "Olivia"); // fixed size
friends2.set(0, "Richard");
// friends.add("Thibaut"); // throws UnsupportedOperationException

 

한 발짝 더 나아가 불변 리스트 만들기 (Java 9 부터 지원)

List<String> friends3 = List.of("Raphael", "Olivia", "Thibaut"); // immutable list
// friends3.add("Chih-Chun"); // UnsupportedOperationException
// friends3.set(0, "Park"); // UnsupportedOperationException

 

List 인터페이스를 보면 List.of가 많이 구현되어있는데, 아래처럼 E의 인수가 없을때 부터 여러개가 들어오는 것에 따라서 오버로딩 되어서 구현되어있다, 0개~10개까지 구현이 되어있다. 가변인수로 받게 되면 내부적으로 추가 배열을 할당해서 리스트로 감싸서 비용이 발생하기 때문에 10개까지는 조금 더 성능을 내기 위해서 이와 같이 되어있다고 한다.

static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
....

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

참고로 Set.of(), Map.of()도 0~10개가 동일하게 구현되어 되어있다.

 

 

불변 집합 만들기

Set<String> friendsSet = Set.of("Raphael", "Olivia", "Thibaut");

// 중복 요소로 인해 throws IllegalArgumentException
// Set<String> friendsSet2 = Set.of("Raphael", "Olivia", "Olivia"); 

 

불변 맵 만들기

import static java.util.Map.entry;

Map<String, Integer> ageOfFriends = Map.of("Raphael", 30, "Olivia", 25, "Thibaut", 26);
System.out.println(ageOfFriends);

Map<String, Integer> ageOfFriends2 = Map.ofEntries(
    entry("Raphael", 30), 
    entry("Olivia", 25),
    entry("Thibaut", 26)
);

 

 

removeIf

리스트에서 필터링을 할 때 filter()를 쓸 수도 있지만, removeIf()를 쓸 수도 있다. 일반 for문으로 짰을 때는 다음과 같은데, ConcurrentModificationException이 일어날 수 있다.

List<Transaction> transactions = new ArrayList<>();
transactions.add(new Transaction(new Trader("A", "a"), 100, 1000));
transactions.add(new Transaction(new Trader("B", "b"), 200, 2000));

for (Transaction transaction: transactions) {
    if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        transactions.remove(transaction); // ConcurrentModificationException
    }
}

왜냐면 iterator가 돌면서 next()를 해야되는데 지워버렸을 수 있으니까

그래서 iterator.hasNext()를 체킹하고 넘어가야 에러가 나지 않지만 코드가 장황해진다.

Iterator<Transaction> iterator = transactions.iterator();
while(iterator.hasNext()) {
    Transaction transaction = iterator.next();
    if (Character.isDigit(transaction.getReferenceCode().charAt(0))) {
        iterator.remove();
    }
}

removeIf()를 쓴다면

transactions.removeIf(transaction -> Character.isDigit(transaction.getReferenceCode().charAt(0)));

한 줄로 정리가 된다.

 

 

replaceAll

문자열 리스트의 각 앞자리만 대문자로 바꿔야 한다면

List<String> referenceCodes = Arrays.asList("a7sj13d", "bidj3fd", "psk39kcd");

referenceCodes.stream()
                .map(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1))
                .collect(Collectors.toList())
                .forEach(System.out::println);

이렇게 하면 요구사항에 만족할 수 있지만, 새로운 문자열 컬렉션을 만들게 된다.

기존의 컬렉션을 활용해서 바로 바로 바꾸고 싶다면

referenceCodes.replaceAll(code -> Character.toUpperCase(code.charAt(0)) + code.substring(1));
System.out.println(referenceCodes);

 

 

맵 처리

forEach

Java 8 이전에는 map을 iterating 하려면 entrySet을 활용해야 했다.

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

Java 8 부터는 BiConsumer를 인수로 받는 forEach를 쓸 수 있게 되었다.

ageOfFriends.forEach((friend, age) -> System.out.println(friend + " is " + age + " years old"));

 

정렬

Entry.comparingByValue

Entry.comparingByKey

말그대로 key 순서대로 혹은 value 순서대로 정렬할 수 있도록 하는 유틸리티이다.

Map<String, String> favoriteMoviews = Map.ofEntries(
    entry("Cristina", "Matrix"),
    entry("Raphael", "Star Wars"),
    entry("Olivia", "James Bond")
);

favoriteMoviews.entrySet()
    .stream()
    .sorted(Map.Entry.comparingByKey())
    .forEachOrdered(System.out::println);
    
/* 실행 결과
    Cristina=Matrix
    Olivia=James Bond
    Raphael=Star Wars
*/

 

getOrDefault

nvl 같은거라고 보면 된다. null일 경우 디폴트 값을 리턴. 첫 번째 인수가 key, 두 번째 인수가 defaultValue

System.out.println(favoriteMoviews.getOrDefault("Olivia", "Matrix")); // James Bond
System.out.println(favoriteMoviews.getOrDefault("Thibaut", "Matrix")); // Matrix

 

remove

이전에는 map이 key를 갖고 있는지 없는지 체크하면서 value를 꺼내는 작업을 했다면, Java 8부터는 remove가 key가 없을 경우에는 아무 연산을 하지 않고, key가 있다면 삭제를 하도록 개선되었다.

favoriteMovies.remove(key, value);

 

merge

Cristina를 합치게 되면 Matrix로 덮어씌워진다.

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);

System.out.println(everyone);
// {Cristina=Matrix, Raphael=Star Wars, Teo=Star Wars}

 

충돌이 나는 부분을 유연하게 대처하려면 merge를 쓰면 된다. 코드는 다소 어렵다.

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}

 

실제로 이러한 코드를 언제 짜볼까 싶지만, 컬렉션 API들은 일단 알고봐야 실무에서 다시 꺼내 볼 수 있는 것 같다.

 

 

책에서 나오는 ConcurrentHashMap 예제는 컴파일이 안 되었고, 내용도 다소 빈약해서 나중에 따로 다뤄봐야겠다.