일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- java #중첩반복문 #구구단
- IntelliJ
- HashMap
- 믹스인
- 해쉬맵
- overring
- java #조건문 #if문
- 객체지향프로그램
- 메서드
- 자바 #java #상속
- 이펙티브자바
- 스프링 #Spring #spring #입문서
- 자바 #변수 #java
- java #객체지향 #자바
- java #조건문 #if조건문 #if
- 자바 #JAVA
- java #자료형
- java #continue #반복문
- Arrays.toString()
- EffectiveJava
- 람다
- Mixin
- java # for문 #반복문
- java
- java #자바 #상속
- 인텔리제이
- 자바
- Math.pow()
- java #자바 #상속 #오버라이딩
- Math.sqrt()
- Today
- Total
산으로 가자▲
해시맵(HashMap) 본문
해시맵(HashMap)
해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서 키는 맵에 오직 유일하게 있어야합니다. 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것입니다. 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)를 찾아야하기 때문입니다. 값은 중복된 값이어도 상관이 없습니다.
HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있습니다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전하지 않은 대신 속도가 빠르냐(HashMap)입니다. 여기서는 Thread-Safe하지 않은 HashMap사용법을 가장 아래에서 살펴보도록 하겠습니다. Hashtable의 개념과 사용법, Thread-Safe한 예제를 보려면 아래의 링크를 참고해주세요.
자신이 만든 객체도 키로 사용할 수 있는데, 그 사용법은 가장 아래쪽 부분에 설명되어있습니다.
map
Map 인터페이스를 구현한 HashMap은 키를 해싱하여 자료를 저장하고 꺼내오기 때문에 속도가 빠릅니다. 이번 포스팅에서는 사용방법 위주로 설명하도록 하겠습니다.
사용법
사용전에 HashMap과 Map은 java.util 안에 위치합니다. 두개를 import 하여 사용준비해주세요.
1. 키,값 저장(put), 읽기(get) 예
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] ar) {
Map<String,Integer> map=new HashMap(); //<키 자료형, 값 자료형>
map.put("A", 100);
map.put("B", 101);
map.put("C", 102);
map.put("C", 103); //중복된 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=101, C=103}
100
101
103
map을 그냥 println으로 출력하게 되면 중괄호('{ }')로 묶여서 키와 값들이 출력됩니다. 기본적으로 map의 put과 get이 아주 많이 사용됩니다. map을 사용하려면 반드시 알아야하는 메소드입니다. put을 키와 값을 map에 저장하는 메소드이며 get은 입력받은 key와 대응되는 값을 돌려줍니다. 만약 해당하는 key가 없다면 null을 넘겨주게 됩니다.
2. containsKey 사용예 (이미 HashMap에 키가 있으면 값을 덮어쓰지 않는 예)
public static void main(String[] ar){
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);
3. putAll 사용예 (Map에 다른 Map을 전부 포함)
public static void main(String[] ar) {
Map<String,Integer> map1=new HashMap();
Map<String,Integer> map2=new HashMap();
//map1 put
map1.put("map1-key1", 100);
map1.put("map1-key2", 200);
//map2 put
map2.put("map2-key3", 300);
map2.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", 1000);
//map2에는 영향 없음.
System.out.println("map2 includes map1:"+map2);
}
결과
map1:{map1-key1=100, map1-key2=200}
map2:{map2-key4=400, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
map2 includes map1:{map2-key4=400, map1-key1=100, map1-key2=200, map2-key3=300}
아예 map을 통째로 인자로 넘겨주고 싶다면 putAll 메소드를 사용하면 됩니다. 주의해야할 점은 반드시 키와 값의 자료형이 같은 map이어야한다는 점입니다. 다른 자료형의 키, 값은 받을 수 없습니다.
putAll 대신 생성자를 이용해서 생성과 동시에 map의 데이터를 전부 넘겨줄 수도 있습니다.
Map<String,Integer> map2=new HashMap(map1);
4. keySet 사용예 (모든 키를 순회하는 코드)
list처럼 증가하는 index를 사용할 방법이 없지만 keySet메소드를 이용하여 키를 Set으로 넘겨주어 Map에 존재하는 키를 모두 순회할 수 있습니다.
public static void main(String[] ar) {
Map<String,Integer> map=new HashMap();
map.put("key1",50);
map.put("key2",100);
map.put("key3",150);
map.put("key4",200);
System.out.println("All key-value pairs");
for(String key:map.keySet()) {
System.out.println("{"+key+","+map.get(key)+"}");
}
}
결과
All key-value pairs
{key1,50}
{key2,100}
{key3,150}
{key4,200}
5. Foreach() 메소드로 순환하기
Foreach() 메소드를 사용하기전, 람다식을 이해하고 있어야합니다. 하지만 사용법만 알아도 유용하게 사용할 수 있습니다. 아래의 사용법을 보면서 익혀보세요.
public static void main(String[] ar) {
Map<String,Integer> hm=new HashMap();
hm.put("http",80);
hm.put("ssh", 22);
hm.put("dns", 53);
hm.put("telnet",23);
hm.put("ftp", 21);
hm.forEach((key,value)->
{
System.out.println("{"+key+","+value+"}");
});
}
위에 보이는 -> 가 있는 라인이 람다식입니다. key와 value를 사용하며 -> 이후 동작을 구현해주면 됩니다.
결과
{ftp,21}
{telnet,23}
{dns,53}
{http,80}
{ssh,22}
6. 내가 만든 객체를 Key로 사용하기(나의 객체를 같은 키로 판단하는 방법)
public class Main {
public static void main(String[] ar) {
Person person1=new Person("reakwon","666666-7777777");
Person person2=new Person("putty","123456-1234567");
Person who=new Person("reakwon","666666-7777777");
Map<Person,Integer> map=new HashMap();
map.put(person1, 90);
map.put(person2, 80);
System.out.println("map includes "+who.getName()+"? "+map.containsKey(who));
map.put(who, 70);
System.out.println(map);
}
}
class Person{
private String name;
private String id;
public Person(String name,String id) {
this.name=name;
this.id=id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return this.name;
}
}
결과
map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}
위의 코드에서 약간 불편한 점을 눈치 채셨나요? 저는 Person의 name과 id가 같으면 같은 키로 보고 중복처리를 하지 않을 생각이었습니다. 즉, 위의 map에는 putty와 reakwon만 있는 것이 저의 의도였고, who와 person1은 같은 키로 map이 인식해줬으면 좋겠습니다. 어떻게 구현할까요?
● equals() 메소드 Overriding
Object 클래스의 equals는 서로 같은 객체인지 아닌지 판별해주는 메소드입니다. String이 바로 이 메소드를 오버라이딩했지요. 그래서 문자열이 같은 경우에 equals의 결과는 true입니다. 이처럼 equals를 통해서 같은지 아닌지를 판별해주면 됩니다. 그래서 아래의 코드를 Person에 추가하면 됩니다.
@Override
public boolean equals(Object o) {
if(o instanceof Person) {
Person p=(Person)o;
return this.id.equals(p.id) && this.name.equals(p.name);
}
return false;
}
결과
map includes reakwon? false
{putty=80, reakwon=70, reakwon=90}
분명 equals를 오버라이딩해서 같은 객체라고 명시했는대도 역시 false를 반환하고 있습니다. 어떻게 해결할 수 있을까요?
● hashCode() 메소드 Overriding
equals()와 쌍으로 기억해두셔야할 것은 바로 hashCode()입니다. hashCode는 각 객체가 갖는 유일한 값(Code)을 의미합니다. Object의 hashCode()는 원래 주소값에 의한 hashCode()로 각 객체가 전부 다른값을 가지고 있습니다. HashMap은 우선 hashCode를 비교하고 같을 때만 equals를 수행하여 정말 제대로 같은것인지 판별합니다. 그래서 HashMap은 애초에 hashCode()가 반환하는 값이 다르면 equals는 수행하지도 않습니다.
그래서 위 코드에서 같은 해시코드를 오버라이딩해서 name과 id에 따라 다른 hashCode를 갖게 만들어주어야합니다. 구현의 편의성을 위해서 String클래스를 사용합니다. String 클래스가 이미 그렇게 다 구현(문자열이 같으면 hashCode도 같고 equals도 true) 이 되어있습니다. 우리는 위의 예제에서 String을 키로 쓴적이 있었죠. String 클래스는 hashCode와 equals가 문자열이 같을때 같은 객체라고 판별할 수 있도록 두 메소드를 전부 재정의한 상태입니다.
그래서 우리는 아래의 코드를 위의 코드 대신 추가해주면 됩니다.
@Override
public int hashCode() {
return name.hashCode()+id.hashCode();
}
@Override
public boolean equals(Object o) {
return this.hashCode()==o.hashCode();
}
결과
map includes reakwon? true
{reakwon=70, putty=80}
위의 코드는 대체적으로 잘 동작하지만 만약 운이 굉장히 좋지 않을 경우 다른 객체로 의도했던 것이 같은 객체로 인식될 수 있습니다. String 클래스의 hashCode의 구현 방식때문이지요. 하지만 이해를 돕기 위해서 더 완벽한 구현은 하지 않았습니다. 귀찮기도 하구요.
알아두셔야할 점은 같은 객체를 판별하는 코드를 넣고자하여 equals를 재정의할때 반드시 hashCode도 다시 정의해주어야한다는 점을 기억하세요.
HashMap의 Thread-Unsafe 예
아래의 코드를 봅시다.
public class Main {
static Map<String,String> hashmap=new HashMap();
public static void main(String[] ar) throws InterruptedException {
Runnable runnable=new Thread() {
@Override
public void run(){
hashmap.put("seoul","02");
hashmap.put("kyeongkido", "031");
hashmap.put("busan", "051");
hashmap.put("daejeon","042");
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("jeju","064");
}
}
우선 Thread의 개념부터 잡아야합니다. 자바 쓰레드의 개념과 사용방법을 알아보려면 아래의 링크로 가서 학습해봅시다.
코드에 대한 결과는 예외를 던지며 프로그램이 종료됩니다. 아래와 같이 말이죠.
{seoul,02}
{daejeon,042}
{busan,051}
{jeju,064}
{kyeongkido,031}
Exception in thread "Thread-1" java.util.ConcurrentModificationException
at java.base/java.util.HashMap.forEach(HashMap.java:1428)
at aa.Main$1.run(Main.java:17)
at java.base/java.lang.Thread.run(Thread.java:831)
위 코드에 대한 의도는 thread라는 쓰레드에서 hashmap의 저장된 데이터 seoul ~ daejeon의 데이터를 모두 출력하는 의도입니다. 하지만 메인 쓰레드와 thread간의 HashMap에 동시에 접근했기 때문에 위 코드에 예외가 발생하게 된것입니다. 왜 동시에 접근했을까요?
메인 스레드는 thread의 run() 메소드가 forEach가 수행할 수 있도록 잠시 1초간 기다려줍니다. 그리고 thread는 1초마다 저장했던 데이터들을 출력합니다. 그런데 그때 메인스레드에서 put() 메소드로 데이터를 저장하는 상황이 발생했습니다. 그래서 두 쓰레드가 동시에 HashMap에 접근하게 된것이죠.
이처럼 HashMap은 쓰레드에 대해서 안전하지 않은 컬렉션입니다. 오직 단일 쓰레드에서만 사용하면 안전한 컬렉션이죠. 이를 보완한 컬렉션이 바로 Hashtable입니다. 위의 링크로 들어가서 Hashtable로 이 문제를 해결한 코드를 확인해보세요.
이상으로 HashMap에 대한 기본적인 개념과 사용법 위주로 알아보았습니다. 이 정도만 알고 있어도 HashMap을 잘 사용할 수 있을 것으로 생각합니다. 감사합니다.
'Java' 카테고리의 다른 글
String을 int로/ int를 String으로 변환 (문자열, 숫자 변환방법) (0) | 2022.10.12 |
---|---|
람다함수란? (1) | 2022.10.11 |
객체지향(OOP) (0) | 2022.10.08 |
쓰레드(Thread)란? (0) | 2022.10.08 |
자바 믹스인(mixins)이란? (0) | 2022.10.08 |