해시맵 (HashMap)
- 해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)이다.
- 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조이다.
- 여기서 키는 맵에 오직 유일하게 있어야 한다.
- 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것이다.
- 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)을 찾아야하기 때문이다.
- 값은 중복되어도 상관이 없다.
HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전 하지 않은 대신 속도가 빠르냐(HashMap)이다.
Map 인터페이스를 구현한 HashMap은 키를 해싱하여 자료를 저장하고 꺼내오기 때문에 속도가 빠르다.
사용법
사용전에 HashMap과 Map은 java.util안에 위치한다. 두개를 import 하여 사용준비한다.
키, 값 저장(put), 읽기(get) 예
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap(); // <키 자료형, 값 자료형>
map.put("A", 100);
map.put("B", 200);
map.put("C", 300);
map.put("C", 400); // 중복된 key가 들어갈때는 이전 키, 값을 지금의 것으로 업데이트
System.out.println(map);
System.out.println(map.get("A"));
System.out.println(map.get("B"));
System.out.println(map.get("C"));
}
}
// 결과
{A=100, B=200, C=400}
100
200
400
map을 그냥 println으로 출력하게 되면 중괄호('{ }')로 묶여서 키와 값들이 출력된다. 기본적으로 map의 put과 get이 아주 많이 사용된다. map을 사용하려면 반드시 알아야하는 메소드로, put은 키와 값을 map에 저장하는 메소드이며 get은 입력받은 key와 대응되는 값을 돌려준다. 만약 해당하는 key가 없다면 null을 넘겨주게 된다.
containsKey 사용 예 (이미 HashMap에 키가 있으면 값을 덮어쓰지 않는 예)
public static void main(String[] args) {
Map<String, Integer> map = new HashMap();
map.put("key1", 100);
map.put("key2", 200);
if(!map.containsKey("key2")) // 키가 들어있는지 확인. 있으면 덮어쓰지 않는다.
map.put("key2", 300);
System.out.println(map);
System.out.println(map.get("key1"));
System.out.println(map.get("key2"));
}
// 결과
{key1=100, key2=200}
100
200
containsKey메소드로 키가 존재하는지 존재하지 않는지 알 수 있다.이것과 비슷한 메소드로는 containsValue가 있다. 이것은 반대로 값이 존재하는지 알아보는 메소드이다. 존재시 true, 없을때는 false를 반환한다.
위의 if문과 put메소드를 한꺼번에 처리할 수 있는 메소드가 존재하는데 두라인을 아래와 같이 바꿔써도 같은 동작을 한다.
// if(!map.containsKey("key2"))
// map.put("key2", 300);
// 위 두줄과 아래 한줄은 같은 동작을 한다.
map.putIfAbsent("key2", 300);
putAll 사용 예 (Map에 다른 Map을 전부 포함)
public static void main(String[] args) {
Map<String, Integer> map1 = new HashMap();
Map<String, Integer> map2 = new HashMap();
// map1 put
map1.put("map1-key1", 100);
map1.put("map1-key2", 200);
// map1 put
map1.put("map2-key3", 300);
map1.put("map2-key4", 400);
System.out.println("map1:" + map1);
System.out.println("map2:" + map2);
// map2에 map1을 합침
map2.putAll(map1);
System.out.println("map2 includes map1:" + map2);
// map1의 키, 값 변경
map1.put("map1-key1", 500);
// map2에는 영향 없음
System.out.println("map2 includes map1:" + map2);
}
아예 map을 통째로 인자로 넘겨주고 싶다면 putAll 메소드를 사용하면 된다. 주의해야할 점은 반드시 키와 값의 자료형이 같은 map이어야한다는 점이다. 다른 자료형의 키, 값은 받을 수 없다.
putAll 대신 생성자를 이용해서 생성과 동시에 map의 데이터를 전부 넘겨줄 수도 있다.
Map<String, Integer> map2 = new HashMap(map1);
keySet 사용 예 (모든 키를 순회하는 코드)
list처럼 증가하는 index를 사용할 방법은 없지만 keySet메소드를 이용하여 키를 Set으로 넘겨주어 Map에 존재하는 키를 모두 순회할 수 있다.
public static void main(String[] args) {
Map<String, Integer> map = new HashMap();
map.put("key1", 100);
map.put("key2", 200);
map.put("key3", 300);
map.put("key4", 400);
System.out.println("All key-value pairs");
for(String key:map.KeySet()) {
System.out.println("{" + key + "," + map.get(key) +"}");
}
}
Foreach() 메소드로 순환하기
Foreach() 메소드를 사용하기전, 람다식을 이해하고 있어야 한다. 하지만 사용법만 알아도 유용하게 사용할 수 있다.
public static void main(String[] args) {
Map<String, Integer> map = new HashMap();
map.put("http", 80);
map.put("ssh", 22);
map.put("dns", 53);
map.put("telnet", 23);
map.put("ftp", 21);
map.forEach((key, value) ->
{
Systrm.out.println("{" + key + "," + value + "}");
});
}
위에 보이는 ->가 있는 라인이 람다식이다. key와 value를 사용하며 -> 이후 동작을 구현해주면 된다.
// 출력 결과
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
equals() 메소드 Overriding
Object 클래스의 equals는 서로 같은 객체인지 판별해주는 메소드이다. 그래서 문자열이 같은 경우에 equals의 결과는 true이다. equals를 통해서 같은지 아닌지를 판별해주면 된다.
hashCode() 메소드 Overriding
equals()와 쌍으로 기억해야할 것은 hashCode()이다. hashCode는 각 객체가 갖는 유일한 값(Code)을 의미한다. Object의 hashCode()는 원래 주소값에 의한 hashCode()로 각 객체가 전부 다른값을 가지고 있다. HashMap은 우선 hashCode를 비교하고 같을 때만 equals를 수행하여 정말 제대로 같은것인지 판별한다. 그래서 HashMap은 애초에 hashCode()가 반환하는 값이 다르면 equals는 수행하지도 않는다.
같은 객체를 판별하는 코드를 넣고자하여 equals를 재정의할때 반드시 hashCode도 다시 정의해주어야한다.
※ 해시(hash)란 다양한 길이를 가진 데이터를 고정된 길이를 가진 데이터로 매핑(mapping)한 값이다. 이를 이용해 특정한 배열의 인덱스나 위치나 위치를 입력하고자 하는 데이터의 값을 이용해 저장하거나 찾을 수 있다.
HashMap의 Thread-Unsafe 예
(Thread의 개념을 알아야 한다.)
public class Main {
static Map<String, String> hashmap = new HashMap();
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Thread() {
@Override
public void run() {
hashmap.put("september", "09");
hashmap.put("october", "10");
hashmap.put("november", "11");
hashmap.put("december", "12");
hashmap.forEach((key, value) ->
{
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("{" + key + "," + value + "}");
});
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(1000);
hashmap.put("may", "5");
}
}
코드에 대한 결과는 예외를 던지며 프로그램이 종료된다.
위 코드에 대한 의도는 thread라는 쓰레드에서 hashmap에 저장된 데이터 september ~ december의 데이터를 모두 출력하는 의도이다. 하지만 메인 쓰레드와 thread간의 HashMap에 동시에 접근했기 때문에 위 코드에 예외가 발생하게 된것이다.
메인 스레드는 thread의 run() 메소드가 forEach가 수행할 수 있도록 잠시 1초간 기다려준다. 그리고 thread는 1초마다 저장했던 데이터들을 출력한다. 그런데 그때 메인스레드에서 put() 메소드로 데이터를 저장하는 상황이 발생했다. 그래서 두 쓰레드가 동시에 HashMap에 접근하게 된것이다.
이처럼 HashMap은 쓰레드에 대해서 안전하지 않은 컬렉션이다. 오직 단일 쓰레드에서만 사용하면 안전한 컬렉션이다.
이를 보완한 컬렉션이 바로 Hashtable이다.
'Language > Java' 카테고리의 다른 글
[Java] 다차원 배열 (multi-dimensional array) (0) | 2022.08.11 |
---|---|
[Java] 해시테이블 (Hashtable) (0) | 2022.08.11 |
[Java] 실습 class AppleOrange (0) | 2022.08.10 |
[Java] compareTo 메소드 (0) | 2022.08.09 |
[Java] 실습 CardDeckExam (0) | 2022.08.09 |