java 9 버전 부터 Map.of 메소드가 생겼습니다. 이것을 언제 쓸 법 한지 알아보고, 간단하게 내부를 보도록 하겠습니다.

 


 보시면, unmodifiable map을 리턴하게끔 되어 있습니다. 수정할 수 없는 맵을 리턴한다고 보면 맞겠네요. 예제 프로그램을 하나 보겠습니다.

 

 

 먼저 key와 value값이 (1, 1)인 쌍과 (2, 2)인 쌍을 넣은 다음에, m에 들어있는 내용들을 toString으로 출력해 보겠습니다.

 

 

 그러면, (2, 2)쌍과 (1, 1)쌍이 출력됩니다. 만약에 이 상황에서, 키를 추가한다고 하면 어떻게 될까요?

 

 

 예제는 이렇습니다. 일단, Map.of는 둘 이상의 (k, v)쌍을 호출하게 되면 ImmutableCollections 안에 있는 MapN<K, V> 객체를 만들어 내게 됩니다.

 

 

 이것은 AbstractImmutableMap<K, V>을 상속받게 됩니다. 이 클래스 내부를 보면, put 메서드가 있는데요.

 

 

 uoe()? UnsupportedOperationException을 떨구는 메소드입니다. 변하지 않는 map이라고 하였으니, 키가 추가되거나 제거되는 등의 변경 연산은 지원되지 않게 해야 겠지요. 실제로 지원되지 않는 연산이라는 예외가 떨어지게 됩니다.

 

 


 그러면 MapN을 알아봅시다. table하고 size가 final로 선언되어 있습니다. 이것이 끝입니다. 그렇다면 초기화가 될 때 일련의 작업이 일어난다는 것인데요.

 

 

 중간에 이상한 연산을 한 다음에, 새로운 table을 생성합니다. 왜 이런 이상한 연산을 하는지는 뒤에 probe 메서드를 설명할 때 다시 설명하겠습니다.

 

 

 중간에 보시면 probe가 있습니다. 이 메서드는 key k가 들어갈 위치를 찾습니다.

 

 

 동작 방식을 보면, table의 length만큼 돌려보면서, 빈 slot이 있으면 해당 위치에 대한 정보를 리턴하게 됩니다. 1325번째 줄에 나와 있고요. 만약에 빈 슬롯이 아니라면 두 가지 경우가 있습니다. 이미 키가 있는 경우. 이 때, 중복된 키가 있는지 검사하는 로직은 1327번째 줄에서 수행하게 됩니다.

 

 만약에 없다면, 1329번째 줄을 수행하게 되는데요. idx += 2를 보시면 됩니다. 구조를 잘 보시면 key와 value가 요렇게 들어감을 확인하실 수 있는데요.

 

 

 각각 테이블의 두 슬롯씩 차지하고 있음을 볼 수 있어요. 정리하면 초기화 과정에서 hashcode 값을 계산합니다. 얻은 hashcode 값을 통해서 들어갈 슬롯을 찾는데요. 해당 슬롯이 비어있지 않다면, 다음 슬롯을 탐색하는 선형 탐사법을 수행합니다. 이는 최악의 경우, 원소 개수가 n이라 하면 O(n)만큼 걸릴 수 있음을 의미합니다.

 

 


 immutable map의 특성과 언제 쓰일 지 생각해 보면 key 값을 찾는 데 주로 쓰일 것으로 보여요. 그러면 containsKey가 어떻게 제대로 동작할지도 보아야 합니다.

 

 

 size가 0보다 크면서, probe(o)가 0보다 크거나 같으면 참이라고 하네요? 다시 probe 메소드를 보겠습니다.

 

 

 그런데 1325번째 줄에 hashcode 값으로 간 버킷에 아무것도 없다면 - 값을 리턴합니다. 그런데 중요한 것은 이 경우에, 찾아야 하는 pk가 없다고 판단해 버려요.

 

 

 다시 key를 넣는 로직을 간단하게 봅시다. 어떤 키 c가, 들어가야 할 곳이 군청색 위치였다 해 볼게요. 그런데 이 위치에는 키가 이미 있었어요. 그렇기 때문에 계속 오른쪽으로 몇 칸씩 이동하면서 slot이 있는지 보는데요. 노란색으로 칠한 부분은 키가 있는 슬롯들입니다.

 

 노란색으로 칠한 부분도 이미 키가 있으니, 계속 건너 뛰다가 빈 슬롯인, c가 쓰여져 있는 곳에 도달하고 나서야 슬롯에 키 c를 넣습니다. 군청색으로 칠한 부분에 키가 없었다고 해 보겠습니다.

 

 

 그러면 군청색이 있는 부분에 키 c가 들어갈 수 있게 되는데요. 이 군청색으로 칠한 버킷 번호가 k라 해 보겠습니다. x를 넣어야 할 버킷을 판단하기 위한 함수를 f(x)라고 합시다. 이 때 f(x)는 버킷 번호를 의미합니다. 그렇다면 이건 뭘 의미할까요? f(?) = k인 ?가 없었다는 의미입니다. 그리고 이 ?에는 c 또한 포함되므로 c도 당연하게도 없었다는 의미가 됩니다.

 

 만약에 f(?) = k인 ?가 맵 내에 존재했다면 최소한 둘 중 하나였을 겁니다. 진짜 해당 버킷에 ?가 있었거나

 

 

 아니면 계속 충돌이 나서 다른 버킷에 존재했거나. 이 말인 즉슨 계속 이런 식으로 loop를 타고 가다가 null을 만나는 경우, 해당 키가 존재하지 않는 의미가 됩니다. 빈 슬롯에 c를 추가할 수 있다는 의미는, c가 없었다는 의미와 동일하기 때문입니다. 남은 의문 하나. probe 함수에서 while loop를 계속 돌고 있음을 볼 수 있는데요.

 

 

 이 while loop는 끝날 수 있을까요? 끝날 수 있습니다. 왜냐하면 입력으로 들어온 size에서 약 2배를 했기 때문에, 값이나 키가 들어가지 않는 부분에는 null 값이 채워지게 됩니다. 이 말인 즉슨 테이블의 크기가 N이고 들어간 데이터의 개수가 n일 때 N > n이라면 값이나 키가 들어가지 않는 부분이 최소 하나 이상 나오게 됩니다.

 

 즉 null을 하나 이상 만나게 되므로, 이 loop는 언젠가 끝이 나게 됩니다. 내부를 간단히 보았는데요. 적은 수의 (K, V) 쌍을 초기화 한 다음에 변경을 원하지 않는 경우에는 Map.of를 쓰면 맞겠다는 결론을 내릴 수 있어요. (k, v) 쌍이 10개를 넘어가는 경우, Map.of 말고 다른 방법으로 초기화 하시는 게 좋아 보입니다.

 


 이것은 여담이겠지만, value가 immutable 하지 않은 경우, 요런 식으로 키에 대응하는 데이터 값을 바꿔치기 할 수 있습니다.

 

 보시면, (1, (1, 2)), (2, (2, 4))를 넣은 상태였는데, 객체 d1의 속성 x를 3으로 바꿔치기 했어요.

 

 

 value 값이 바뀌었음을 확인할 수 있습니다. 조심해야 하는 부분입니다.