본문 바로가기
카테고리 없음

모던 자바 인 액션 스터디 6. 컬렉션 API 개선

by 손너잘 2021. 3. 25.

파즈

 

  1. Arrays.asList와List.of 의 차이점은 무엇일까요 ??
  2. map.put과map.replace두메서드 다 값 변경이 가능한데 그렇다면 왜 replace라는 메서드가 따로 있을까요 ??
  3. Map에는 computeIfAbsent이라는 메서드가 존재합니다. 만약 key가 존재하지 않는다면 키를 이용해서 새 값을 계산하고 맵을 추가하는 메서드입니다. 추가로, Map에는 putIfAbsent라는 메서드도 있습니다. 키가 존재 하지 않는다면 value를 바로 집어넣어주는 메서드입니다.
public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<>();
        map.put(1, 1);
        map.putIfAbsent(2, calculate(2));
        map.computeIfAbsent(2, key -> calculate(key));
    }
    public static int calculate(int n) {
        return n + 5;
}

  그렇다면 위와 같은 상황에서 두 메서드는 어떤 차이점이 있을까요??

  4.

Map<String, String> family = new HashMap<>(Map.ofEntries(
        Map.entry("Teo", "Star Wars"),
        Map.entry("Cristina", "James Bond")
));
family.merge("Teo", "asdf", (s, s2) -> null);
family.merge("Cristina", "JB2", (s, s2) -> s + " % " + s2);
family.merge("Bepoz", "Evans", (s, s2) -> s + "***" + s2);

나의 답

더보기
  1. 불변성의 차이. List.of 는 데이터의 변경을 지원한다. 따라서 asList의 경우 set, remove 등의 연산을 지원하지만 of는 해당 연산들을 지원하지 않는다. 내부적으로 asList는 인수로 들어온 가변인자를 그대로 참조하여 사용하고, of는 새로 ImmutableCollections를 사용하기 때문에 발생하는 차이이다. (asList는 이러한 이유로 add를 지원하지 않는다.)
  2. replace는 key가 존재하는 데이터에 대해서면 값을 변경한다.
  3. 모르겠음
  4. Cristina = James Bond & JB2, Bepoz = Evans  -> 약간 해봤는데 UnaryOperator의 반환이 null이면 삭제되는 걸 놓쳤었네요

 

파즈의 답

1번 문제 답안

  1. List.of 는 set으로 값 변경이 불가능하다
List<Integer> asList = Arrays.asList(1, 2, 3);
List<Integer> listOf = List.of(1, 2, 3);
asList.set(0, 10);
listOf.set(0, 10);		//UnsupportedOperationException

List.of는 set으로 값 변경을 시도하면 컴파일 에러가 발생하게 된다.2. List.of 는 null을 허용하지 않는다.

List<Integer> asList = Arrays.asList(1, 2, null);
List<Integer> listOf = List.of(1, 2, null);		//NPE

null을 받아들이는 Arryas.asList와 달리 List.of는 거부한다.3. List.of는 null 여부를 contains 확인도 못하게 한다.

List<Integer> asList = Arrays.asList(1, 2, 3);
List<Integer> listOf = List.of(1, 2, 3);
boolean asListResult = asList.contains(null);
boolean listOfResult = listOf.contains(null);		//NPE

4. Arrays.asList는 원본의 배열의 변화에 반응한다.

Integer[] arr = {1, 2, 3};
List<Integer> asList = Arrays.asList(arr);
List<Integer> listOf = List.of(arr);
arr[0] = 10;
System.out.println(asList);
System.out.println(listOf);
/*
[10, 2, 3]
[1, 2, 3]
 */

arr의 값이 변하자 asList의 값 또한 변한 것을 확인할 수가 있다.만약 new ArrayList<>(List.of / asList); 를 쓰게되는 경우에는, 어떤 것을 사용하는지는 취향차이라고 생각한다.(반박 받음)

2번 문제 답안
map.put(key, value) 는 해당 key 값에 value를 추가한다. 이미 존재하는 key 라면 value를 덮어씌운다.
map.replace(key, value) 는 해당 key 값을 value로 갈아치운다.
그러면 그냥 put을 사용하면 되는데 왜 굳이 replace라는 메서드가 존재하는 것일까 ??

default V replace(K key, V value) {
    V curValue;
    if (((curValue = get(key)) != null) || containsKey(key)) {
      curValue = put(key, value);
   }
    return curValue;
 }

 

replace의 세부내용은 다음과 같다. null이 아닐 때만 put을 실행을 하고 있다.

 Map<String, Integer> map = new HashMap<>(Map.of("bepoz", 100));
    map.replace("giraffe", 150);
    System.out.println(map);
    map.put("giraffe", 150);
    System.out.println(map);
/*
{bepoz=100}
{bepoz=100, giraffe=150}

put은 해당 key 값이 없으면 그대로 추가해버리지만, replace는 해당 key 값이 존재할 때만 바꿔주므로 조금 더 안정성이 있다는 것을 확인할 수가 있다.

3번 문제 답안
putIfAbsent는 일단 calculate메서드를 실행하고 해당 결과를 파라미터로 보낸다.
반면, computeIfAbsent는 먼저 key 값의 존재 유무를 확인하고 없을 시에 calculate메서드가 실행이 된다.
만약, map.putIfAbsent(2,2), map.computeIfAbsent(2, key -> 2) 일 경우에는 차이가 없을 것이다.

 

4번 문제 답안
{Bepoz=Evans, Cristina=James Bond % JB2}

default V merge(K key, V value,
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
         remappingFunction.apply(oldValue, value);
    if (newValue == null) {
      remove(key);
   } else {
      put(key, newValue);
   }
    return newValue;
 }

newValue가 null 이면 remove 해버리기 때문에 Teo는 사라졌다.
Bepoz라는 기존 키가 없었기 때문에 바로 Evans로 추가되었다. Function을 거치지 않기에 *** 가 붙지 않았다.


중간곰

많이 바쁘시죠? 간단한 문제를 준비했습니다.
여유 있으신 분들만 풀어보세요~ 
방법이 여러 개 있습니다. stream은 쓰지 말고 만들어보세요.

 

8장 문제 
2022년에 카타르에서 월드컵이 열린다.
도박을 좋아하는 사람들이 우승할 것 같은 국가에게 자신의 돈을 걸고 있다.
Toto 클래스는 각각 사람들이 우승을 예상하는 나라코드(nation)와 거는 돈(money)로 이루어져 있다.
totos 리스트에는 사람들의 토토 내역이 들어 가있다.
totos를 순회하며 stats에 나라별로 걸린 금액의 총합을 계산해보자!

public class TotoApp {
    public static void main(String[] args) {
        List<Toto> totos = Arrays.asList(
                new Toto("br", 2000),
                new Toto("fr", 30000),
                new Toto("de", 20000),
                new Toto("fr", 1000),
                new Toto("kr", 15000),
                new Toto("jp", 10),
                new Toto("en", 1500),
                new Toto("kr", 7000),
                new Toto("kr", 4000),
                new Toto("br", 1000),
                new Toto("en", 1030),
                new Toto("br", 1000)
        );
        Map<String, Integer> stats = new HashMap<>();
        for (Toto toto : totos) {
            String nation = toto.getNation();
            int money = toto.getMoney();
            // 이곳에 코드를 넣으시오.
            // ...
            // ...
            // ...
        }
        System.out.println(stats);
        // 기대 결과
        // {br=4000, de=20000, jp=10, kr=26000, en=2530, fr=31000}
    }
    static class Toto {
        private String nation;
        private int money;
        private Toto(final String nation, final int money) {
            this.nation = nation;
            this.money = money;
        }
        public String getNation() {
            return nation;
        }
        public int getMoney() {
            return money;
        }
    }
}

나의 답

 

stats.merge(nation, money, Integer::sum);

웨지

8장 문제 대신에~~
ConcurrentHashMap의 동기화 처리 방식
Map interface를 구현한 3가지 구현체의 차이를 알아봅시다

  • HashMap : Thread 에 안전하지 않다.
  • Hashtable : Thread safe. 데이터 관련 함수에 synchronized 키워드가 선언 되어 있다.
  • ConcurrentHashMap : Thread safe, 어떻게 Thread-safe를 보장하는지 알아보자

synchronized 키워드가 100배 정도 성능에 안 좋은 영향을 준다고 하죠?
Hashtable과는 다르게 ConcurrentHashMap은 주요 메소드마다 synchronized키워드가 선언되어있지는 않습니다.
put할 때 두 가지로 동작하게 되는데요,

  1. 빈 해시 버킷에 노드를 삽입하는 경우, lock 을 사용하지 않고 Compare and Swap (비교 후 일치하면 변경)을 이용하여 새로운 노드를 해시 버킷에 삽입(원자성 보장)

(1) 무한 루프. table 은 내부적으로 관리하는 가변 배열이다.
(2) 새로운 노드를 삽입하기 위해, 해당 버킷 값을 가져와(tabAt 함수) 비어 있는지 확인한다.(== null)
(3) 다시 Node 를 담고 있는 volatile 변수에 접근하여 Node 와 기대값(null) 을 비교하여(casTabAt 함수) 같으면 새로운 Node 를 생성해 넣고, 아니면 (1)번으로 돌아간다(재시도).

2. 이미 노드가 존재 하는 경우는 synchronized (노드가 존재하는 해시 버킷 객체) 를 이용해 하나의 스레드만 접근할 수 있도록 제어한다.
서로 다른 스레드가 같은 해시 버킷에 접근할 때만 해당 블록이 잠기게 된다.

3줄 요약

  1. 자바 8 이전의 ConcurrentHashMap은 영역을 16개로 분리하여 각각의 영역을 잠그는 방식이었다.
  2. 자바 8 이후부터의 ConcurrentHashMap은 키가 존재할 때 put할 경우 각 테이블 버킷을 독립적으로 잠근다
  3. 값이 null인 경우에는 잠그지는 않고, CAS (체크 후 삽입)을 통해 삽입한다.

검프

8장 문제!!

  1. java.util.Collections의 정적 메소드를 통해 반환 받은 컬렉션 또는. 컬렉션을 반환하는 유팅성 클래스들의 정적 메소드들은 전부\ 요소를 추가하거나 삭제할 수 없게 되어있어요. 그 이유는 무엇일까요?
List<String> strings = Collections.emptyList();
Map<Integer, Integer> integers = Collections.emptyMap();
List<String> stringArray = Arrays.asList("hi", "bi", "me");
List<Integer> integers1 = List.of(0, 1, 2, 3); 
  1. 또한, 아래의 코드는 정상 작동할까요?
Set<String> set = new HashSet<>(Arrays.asList("hi", "ss", "me"));
set.add("hi");

찰리

8장 문제
pobi, cu, json이 참가한 레이싱 게임입니다.
movedCarNames 에는 움직인 순서대로 드라이버의 이름들이 들어있습니다.
경기 직전 레이싱 결과를 계산하는 로직을 보고만 pobi는 마음이 불편하다가
급기야 레이싱 도중 차를 멈추고 getOrDefault 말고 다른걸 써보는건 어떻겠냐고 피드백을 줍니다.Map의 계산 패턴 메서드 또는 merge를 사용해서 결과 계산 로직을 바꿔주세요!

 

Map의 계산 패턴 메서드 또는 merge를 사용해서 결과 계산 로직을 바꿔주세요!

for (String movedDriverName : movedDriverNames) {
    racingResult.put(movedName, (racingResult.getOrDefault(movedCarName, 0) + 1));
}

아래는 전체 코드입니다.

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WonkaQuestion {
    public static void main(String[] args) {
        List<String> movedDriverNames = Arrays.asList(
                "pobi",
                "json",
                "cu",
                "cu",
                "cu",
                "pobi",
                "pobi",
                "cu",
                "json",
                "json",
                "pobi"
        );
        Map<String, Integer> racingResult = new HashMap<>();
        for (String movedDriverName : movedDriverNames) {
            //racingResult.put(movedName, (racingResult.getOrDefault(movedCarName, 0) + 1));
            // Map의 계산패턴 메서드 또는 merge를 사용해서
            // 위에 주석 처리된 코드와 같은 결과를 보여주는 코드를 작성해주세요!
        }
        System.out.println(racingResult);
    }
}

나의 답

더보기
racingResult.merge(movedDriverName, 1, Integer::sum);

 

찰리의 답

racingResult.compute(movedCarName, (k, v) -> v + 1);
racingResult.merge(movedCarName, 1, (oldValue, newValue) -> oldValue + newValue);
racingResult.merge(movedCarName, 1, Integer::sum);


손너잘

8장.

사실 8장과 관련이 없을 수 있으나, 처음아는 사실이라서...

 

우리는 removeIf 라는 메서드를 배웠습니다. 맵에서 특정 element를 삭제할 때 아래와 같이 삭제할 수 있는데요.

testMap.entrySet().removeIf(e -> e.getKey().equlas("test"));

testMap.entrySet().removeIf(e -> e.getKey().equlas("test"));

어라? entrySet()을 데이터 셋을 불러왔는데, 해당 set을 삭제하니 map에서도 데이터가 삭제됩니다.... 왜 이런걸까요?

 

나의 답

댓글