오늘은 Map에 대해서 알아보자!
Map은 한 쌍으로 묶인 데이터를 사용할 때 사용하게 되는데, 이번 포스팅을 통해 자세히 정리해보도록 하자!
Map 이란?
맵(Map)은 Key를 Value에 매핑하는 객체이다. 조금 더 쉽게 말해보자면, 각각의 대응 관계를 쉽게 표현하는 자료형이다.
예를 들어 "이름" : "기영이"의 경우, Key에 "이름"이, Value(값)에 "기영이"가 들어가는 것이다.
박응용님의 점프 투 자바에서는 Map을 사전과 같다고 표현한다. 한 단어(Key)에 뜻(Value)이 부합되는 것 처럼, Map은 Key와 Value를 한 쌍으로 갖는 자료형이기 때문이다. 자바의 공식 문서에서도 Map 인터페이스는 추상 클래스인 Dictionary class를 대신한다고 한다.
HashMap 이란?
HashMap은 Map 인터페이스를 구현한 대표적인 Map 컬렉션이다.
Map 인터페이스를 상속하고 있기에 Map의 성질을 그대로 가지고 있다. 키와 값은 모두 객체이고, 값은 중복을 허용하지만 키는 중복을 허용하지 않는다 (중복된 키로 값을 저장하면 기존의 값은 없어지고 새로운 값으로 대치됨). HashMap은 이름 그대로 해싱(Hashing)을 사용하기 때문에 많은 양의 데이터를 검색하는데 있어서 뛰어난 성능을 보인다.
해싱 (Hashing)
키 값을 해시 함수(Hash Function) 라는 수식에 대입시켜 계산한 후, 결과를 주소로 사용하여 바로 값에 접근하는 방법
해시 함수 (Hash Function)
임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 변환시켜주는 함수
흠 그런데 여기서 해싱이라는 게 잘 이해가 되지 않았다. 알듯말듯... 다른 자료구조에서도 해시에 대해서는 계속 나왔는데 오늘은 갑자기 깊히 알아보고 싶어졌다.
해싱 (Hashing)
먼저 데이터가 있고, 이 데이터를 해시 함수를 거쳐서 해시 테이블로 정리를 하고 싶다고 생각해보자. 그럼 데이터를 어떻게 해시 테이블에 넣어야 나중에 보기가 편할까? 들어온 순서대로 데이터를 저장하는 게 아니라, 똑같은 데이터가 올 때마다 똑같이 분류하는 규칙이 있다면 더 효율적으로 데이터를 관리할 수 있을 것이다. 그리고 이러한 규칙성을 가지고 있는 곳이 바로 해시 함수이다. 해시 함수에 대해서는 업비트의 해시 함수는 무엇인가?라는 포스팅을 통해 쉽게 이해할 수 있었다. 업비트는 임의 길이의 데이터를 고정된 길이의 데이터로 변환시키는 해시 함수를 암호화에 사용하고 있는데, 위에 언급한 것 처럼 똑같은 데이터가 올 때마다 똑같이 분류한다!
해시 함수를 거친 데이터는 해시 테이블에 저장되고 해시 테이블의 구조는 다음 사진과 같다. 예를 들어, 123이라는 데이터가 f(x) = x/100 이라는 해시함수를 거쳐 결과로 1이 나왔다. 그렇다면 여기서 1은 버켓이고, 버켓 안에 들어가 있는 값인 123을 엔트리라고 한다.
그리고 이 모든 프로세스를 '해싱' 이라고 한다.
그리고 해싱의 특징은 자원을 사용하여 속도를 높힌다는 것이다.
그럼 자원을 사용하여 속도를 어떻게 높힐 수 있다는 것일까??
위 이미지는 해싱의 예제이다. 123, 234, 345, 456의 데이터를 f(x) = x/100 이라는 해시함수를 거쳐 해시 테이블에 저장했다.
만약 345 라는 값이 해시 테이블에 존재하는지를 확인하고 싶다고 가정해보자. 그러면 345라는 데이터를 바로 해시 함수에 집어넣고, 그 해시 함수는 3이라는 인덱스를 뽑아낼 것이다. 그럼 여기서 모든 인덱스를 검사할 필요 없이, 추출된 3이라는 인덱스에 345 값이 존재하는지 확인하면 된다! 바로 이것이 해시의 장점인 데이터 접근으로 인한 빠른 속도이다.
서로 다른 두개의 데이터가 동일한 인덱스를 가지는 경우 (예: 123, 150), 충돌이 발생할 수 있다. 충돌을 해결하는 방법도 존재하지만 일단 이번 포스팅은 Map에 대한 포스팅이니 넘어가도록 하겠다!
그럼 다시 본론으로 돌아가서 HashMap에 대해서 얘기해보자.
위 그림과 같이 HashMap은 내부에 '키'와 '값'을 저장하는 자료 구조를 가지고 있다.
HashMap은 순서에 상관없이 해시 함수를 통해 '키'와 '값'이 저장되는 위치를 결정하고 사용자는 그 위치를 알 수 없다.
HashMap 선언 방법
HashMap<String,String> map1 = new HashMap<>();
HashMap<String,String> map2 = new HashMap<String,String>(); // 타입 지정
HashMap<String,String> map3 = new HashMap<>(10); //초기 용량 세팅
HashMap<String,String> map4 = new HashMap<>(map1); //map1의 모든 값을 가진 HashMap생성
HashMap<String,String> map5 = new HashMap<>(10, 0.7f);//초기 capacity, load factor지정
HashMap<String,String> map6 = new HashMap<String,String>(){{ //초기값 세팅
put("a","b");
}};
HashMap은 저장공간을 초과하여 값이 들어오면 List처럼 저장공간을 추가로 늘린다. 하지만 List처럼 저장공간을 한 칸씩 늘리지 않고 약 두배로 늘린다고 한다. 그리고 이러한 과정에서 과부하가 많이 발생한다. 따라서 초기에 지정할 데이터의 개수를 알고 있다면 미리 초기 용량을 지정하는 것이 좋다.
Java 9 부터는 두 가지 방법으로 수정할 수 없는 맵을 초기화 할 수 있다.
Map.of
10개 이하의 키와 값 쌍을 가진 작은 맵을 만들때에는 이 메소드가 유용하다.
- 10개가 넘어가면 컴파일 에러가 발생
- immutable한 map을 반환
- 중복 키가 있는 키-값 쌍을 추가하려고 하면 IllegalArgumentException 이 발생
- null 키나 값 을 추가하려고 하면 NullPointerException이 발생
Map.ofEntries
10개 이상의 키와 값 쌍을 가진 맵을 만들때에는 Map.ofEntries 를 사용한다.
- 키와 값을 감쌀 추가 객체 할당 필요
- null 키나 값을 추가하려고 하면 NullPointerException이 발생
- 반환된 항목에서 Entry.setValue()를 호출하면 UnsupportedOperationException이 발생
HashMap 값 추가
put(key,value): HashMap의 값 추가
HashMap<Integer,String> map = new HashMap<>();
map.put(1,"기영이"); //값 추가
map.put(2,"큰누나");
map.put(3,"작은누나");
만약 HashMap 추가 시, 입력한 키 값이 이미 HashMap에 존재한다면 기존 값은 새로 입력되는 값으로 대치된다.
HashMap 값 삭제
remove(key): 해당 key 값으로 Map 요소 삭제
clear(): 데이터 모두 삭제
HashMap<Integer,String> map = new HashMap<Integer,String>(){{ //초기값 세팅
put(1,"기영이");
put(2,"큰누나");
put(3,"작은누나");
}};
map.remove(2); //key값 2 제거
map.clear(); //모든 값 제거
HashMap 값 출력
HashMap<Integer,String> map = new HashMap<Integer,String>(){{ // 초기값 세팅
put(1,"기영이");
put(2,"큰누나");
put(3,"작은누나");
}};
System.out.println(map); // 전체 출력 : {1=기영이, 2=큰누나, 3=작은누나}
System.out.println(map.get(1)); // key 1의 value 값 : 기영이
//entrySet() 활용
for (Entry<Integer, String> entry : map.entrySet()) {
System.out.println("서열 " + entry.getKey() + "위 : " + entry.getValue());
}
//서열 1위 : 기영이
//서열 2위 : 큰누나
//서열 3위 : 작은누나
// JAVA 8부터 BiConsumer(키와 값을 인수로 받는)를 인수로 받는 forEach메서드를 지원
map.forEach((rank, name) ->
System.out.println("서열 " + rank + "위 : " + name));
//서열 1위 : 기영이
//서열 2위 : 큰누나
//서열 3위 : 작은누나
//KeySet() 활용
for(Integer i : map.keySet()){ //저장된 key값 확인
System.out.println("서열 " + i + "위 : " + map.get(i));
}
//서열 1위 : 기영이
//서열 2위 : 큰누나
//서열 3위 : 작은누나
entrySet()은 주로 key와 value 모두가 필요할 경우 사용하며
keySet()은 key 값만 필요할 경우 사용하고, get(key) 로 value 또한 확인할 수 있다. 하지만 이러한 과정에서 시간이 많이 소요되기 때문에 (key를 찾고 -> 그 키로 또 value를 찾음), 많은 양의 데이터를 가져와야한다면 entrySet()이 성능 면에서 더 좋다.
사실 요새 모던 인 자바 액션 책 스터디를 하면서 Java 8 이후 새롭게 추가된 기능들을 공부하고 있다.
그리고 아무 생각 없이 사용하던 컬렉션 프레임워크에 유용한 메서드들이 추가된 것을 보면서 그동안 코딩테스트는 Java 8, Java 11을 골랐으면서 막상 존재하는 메소드를 사용하지 않은 것에 대한 후회가 들었다. 최근에 눈여겨 보고 있는 스타트업에서는 이미 Java 17을 사용중이던데, 새로운 것을 배우고 직접 사용하는 것에 대한 중요성을 느끼고 있다. 공부한 내용들을 꼭 실전에 쓸 수 있도록 많이 연습해야겠다!
[참고]
'𝑷𝒓𝒐𝒈𝒓𝒂𝒎𝒎𝒊𝒏𝒈 > 𝐽𝐴𝑉𝐴' 카테고리의 다른 글
[JAVA] 레코드(record) 클래스 - jdk 14 (0) | 2023.01.20 |
---|---|
[JAVA / 모던 자바 인 액션] JAVA 8 부터의 컬렉션 API 개선 (0) | 2023.01.12 |
[JAVA / 모던 자바 인 액션] 람다 표현식 (0) | 2022.12.31 |
[JAVA] Optional 개념 및 사용법 (0) | 2022.12.16 |
[JAVA] Stream API 살펴보기 - findFirst() vs findAny() + 병렬 처리 (0) | 2022.12.01 |