해시맵(HashMap)

해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서 키는 맵에 오직 유일하게 있어야합니다. 즉, 같은 맵에 두 개 이상의 키가 존재하면 안된다는 것입니다. 이름 그대로 열쇠이기 때문에 그 열쇠로 짝인 값(Value)를 찾아야하기 때문입니다. 값은 중복된 값이어도 상관이 없습니다. 

HashMap과 사용법이 거의 동일한 컬렉션(Collection)에는 Hashtable이 있습니다. 두 클래스의 차이점은 Thread 관점에서 안전하냐(Hashtable), 안전하지 않은 대신 속도가 빠르냐(HashMap)입니다. 여기서는 Thread-Safe하지 않은 HashMap사용법을 가장 아래에서 살펴보도록 하겠습니다. Hashtable의 개념과 사용법, Thread-Safe한 예제를 보려면 아래의 링크를 참고해주세요.

reakwon.tistory.com/152

 

자바 해시테이블(Hashtable) 개념과 사용법, HashMap과의 차이점을 코드로 이해할 수 있는 예제

해시테이블(Hashtable) 해시테이블은 해시맵(HashMap)과 함께 Map을 구현한 키와 값 쌍을 데이터로 저장한 컬렉션(Collection)입니다. 여기서 키는 유일해야만 합니다. 해시테이블은 이 키를 가지고 키에

reakwon.tistory.com

자신이 만든 객체도 키로 사용할 수 있는데, 그 사용법은 가장 아래쪽 부분에 설명되어있습니다.

 

 

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의 개념부터 잡아야합니다. 자바 쓰레드의 개념과 사용방법을 알아보려면 아래의 링크로 가서 학습해봅시다.

reakwon.tistory.com/84

 

[JAVA/자바] 쓰레드(Thread) 다루기( Thread상속, Runnable 구현, join)

쓰레드(Thread) 쓰레드(Thread)는 간단히 정의하면 하나의 프로세스(실행중인 프로그램이라고 합니다.)에서 독립접으로 실행되는 하나의 일, 또는 작업의 단위를 말합니다. 뭐, 더 간단히 말해 쓰레

reakwon.tistory.com

 

코드에 대한 결과는 예외를 던지며 프로그램이 종료됩니다. 아래와 같이 말이죠.

{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을 잘 사용할 수 있을 것으로 생각합니다. 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

HashSet

자바 Collection 중 Set의 대표적인 HashSet 클래스를 다루어 보도록 하겠습니다. HashSet은 Set의 파생클래스로 Set은 기본적으로 집합으로 중복된 원소를 허용하지 않습니다. HashSet은 순서 역시 고려가 되지 않습니다. 그렇다면 다음의 예제로 HashSet의 기본적인 동작을 살펴보도록 하겠습니다.

 

public static void main(String[] args){
	Set hashSet=new HashSet();
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* 위와 같은 데이터들을 다시 add */
	hashSet.add("F");
	hashSet.add("B");
	hashSet.add("D");
	hashSet.add("A");
	hashSet.add("C");
	
	/* HasSet의 "C"라는 원소 삭제 */
	hashSet.remove("C");
	
	/* HashSet 모든 원소 출력 */
	System.out.println("HashSet 원소 출력");
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		System.out.print(it.next()+" ");
	}
	
	/* HashSet의 모든 원소를 ArrayList로 전달 */
	List arrayList=new ArrayList();
	arrayList.addAll(hashSet);
	
	/* ArrayList의 모든 원소 출력 */
	System.out.println();
	System.out.println("ArrayList 원소 출력");
	for(int i=0;i<arrayList.size();i++){
		System.out.print(arrayList.get(i)+" ");
	}
}
 

 

우선 HashSet에 "F", "B", "D", "A", "C"라는 문자열을 차례대로 넣었습니다. 그리고 나서 다시 같은 데이터들을 넣게 되죠.  Set에는 중복을 허용하지 않는다고 하였으니 Set에는 현재 { "F", "B", "D", "A", "C" }가 존재합니다.

 

그 후에 "C"라는 문자열을 지우는 군요. 뭐 쉽습니다. "C"라는 원소를 삭제했으니 { "F", "B", "D", "A"}만이 남는것을 알 수 있겠군요. HashSet을 출력하는 부분을 보고 확인해보세요.

아, HashSet에는 순서가 없으므로 Iterator를 사용해서 집합안의 원소를 출력하고 있습니다. Iterator의 메소드는 3개가 다인데요.

hasNext() : 다음 원소가 남아있는지 여부를 알아냅니다. 다음 원소가 남았다면 true를 반환합니다.

next() : 다음 원소를 가져옵니다. 

remove() : 현재 반복자가 가리키고 있는 원소를 삭제합니다.

 

장난삼아서 ArrayList에 HashSet의 원소를 모두 전달해서 확인해봤습니다. 아래는 그 결과입니다.

HashSet 원소 출력
A B D F 
ArrayList 원소 출력
A B D F 

 

이제 우리는 단순히 String 타입의 자료형이 아닌 우리가 정의한 클래스의 객체를 HashSet의 원소에 넣고 싶습니다. 그렇다면 어떻게 중복된 원소인지 아닌지를 확인할 수 있을까요?

위에서 String 객체를 HashSet은 어떻게 중복된 원소라고 감지했을까요? HashSet이 알아서 판단해 줄까요?

 

우리가 다음과 같은 Person 클래스가 있다고 합시다.

 

class Person{
	String name; //이름
	int residentNumber; //주민번호
}

 

사람이 같은지 여부를 판별하는 것은 주민등록번호를 보고 알 수 있지요? 허나 Set에 단순히 이 Person 클래스를 넘겨준다면 중복 여부를 판별하지 못할 겁니다. Set이 Person 객체의 어떤 메소드를 호출해서 같은지 말지를 판변할 수 있으니까요. 그 메소드가 바로 equals와 hashCode입니다.

 

1. equals(Object o)

만약 두 객체(현재 이 객체와 인자로 넘어온 객체 o)가 같다는 것을 알려주려면 equals 메소드에서 true를 반환해주어야합니다.

 

2. hashCode()

만약 equals에서 두 객체가 같다라고 true를 반환했다면 hashCode는 두 객체에서 항상 같은 값을 반환해야합니다. 만일 equals에서 false를 반환하여 같지 않다고 반환했다면 hashCode 역시 다르게 반환하는 것이 좋습니다.

 

위의 조건을 고려해서 구현한 Person 클래스를 다시 봅시다.

class Person{
	String name; //이름
	int residentNumber; //주민번호
	
	public Person(String name,int residentNumber){
		this.name=name;
		this.residentNumber=residentNumber;
	}
	@Override
	public int hashCode(){
		/* Objects.hash 메소드로 residentNumber의 해쉬값 반환 */
		return Objects.hash(residentNumber);
	}
	
	@Override
	public boolean equals(Object o){
		/* 주민 번호가 같은 Person은 true 반환 */
		Person p=(Person)o;
		return p.residentNumber==this.residentNumber;
	}
}

 

equals와 hashCode를 Override한 코드를 확인해봅시다.

equals에서는 단순히 residentNumber만 비교해서 같다면 true, 다르다면 false입니다.

hashCode에서는 Objects.hash 메소드로 residentNumber의 해쉬값을 반환합니다. 이 hash는 residentNumber가 다르다면 항상 다른 값이 반환됩니다.

 

 

이제 Set에 원소를 넣고 확인해보도록 하지요.

 

public static void main(String[] args){
	
	Set hashSet=new HashSet();
	hashSet.add(new Person("reakwon",111111));
	hashSet.add(new Person("KDC",222222));
	hashSet.add(new Person("KSG",333333));
	hashSet.add(new Person("reakwon",111112));
	hashSet.add(new Person("MJW",111111));
	
	Iterator it=hashSet.iterator();
	while(it.hasNext()){
		Person p=it.next();
		System.out.println(p.name+"/"+p.residentNumber);
	}
}

 

동명이인이 있군요. 주민 번호 111111인 reakwon과 주민 번호 111112인 reakwon인 객체들이네요. 우리는 이름이 같은것은 같은 객체로 보지 않습니다. 주민 번호가 같은 객체만 중복된 원소로 보는 것이죠.

그래서 이 두 객체는 집합에 추가가 됩니다.

 

하지만 이름이 주민번호가 111111의 "MJW"라는 객체와 주민번호 111111의 "reakwon"라는 객체는 같습니다. 주민번호가 같기때문입니다. 그래서 "MJW"라는 Person객체는 집합이 추가하지 않습니다.

 

그 집합의 모든 원소를 출력한 결과는 아래와 같습니다.

KSG/333333 
reakwon/111112 
reakwon/111111 
KDC/222222

 

이제 우리가 만든 객체들도 HashSet에 추가할 수 있습니다.

 

HashSet을 알아보았으니 다음은 HashMap에 대해서 알아볼 차례겠군요. 아래의 링크로 들어가서 개념과 사용법을 익혀보도록 합시다.

reakwon.tistory.com/151

 

[자바] 해시맵(HashMap)의 개념과 사용 예, 기본 메소드와 같은 키인지 판별하는 방법

해시맵(HashMap) 해시맵은 이름 그대로 해싱(Hashing)된 맵(Map)입니다. 여기서 맵(Map)부터 짚고 넘어가야겠죠? 맵이라는 것은 키(Key)와 값(Value) 두 쌍으로 데이터를 보관하는 자료구조입니다. 여기서

reakwon.tistory.com

반응형
블로그 이미지

REAKWON

와나진짜

,