해시맵(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

와나진짜

,

컬렉션(Collection)

이름에서도 알 수 있듯이 자료들을 효율적으로 모을 수 있는 자료구조를 의미합니다. Collection의 상속구조는 아래의 그림과 같습니다.

 

주황색 상자는 인터페이스, 파란색 상자는 클래스를 의미하며 파란색 화살표는 extends, 녹색 화살표는 implements의 관계를 나타냅니다.

 

Collection이라는 인터페이스는 Collection 계층의 최상위 인터페이스입니다. Iterable이라는 인터페이스를 상속했다는 것을 알 수 있고, Map은 Collection 인터페이스를 상속하지는 않지만 자바의 JCF(Java Collections Framework)에 포함됩니다. 

 

 

Collection 인터페이스의 메소드들을 간단히 살펴보도록 하겠습니다. 별로 어려운 메소드는 없을 것 같습니다.

boolean add(E e)

요소를 추가합니다.

boolean addAll(Collection<? extends E> c)

Collection 타입의 매개변수에 있는 모든 요소를 추가합니다.

void clear()

Collection의 모든 요소를 지웁니다.

boolean contains(Object o)

인자 o가 이 Collection에 속한다면 true를 반환하고 없다면 false를 반환합니다.

boolean containsAll(Collection<?> c)

Collection c의 원소들이 이 Collection에 모두 존재한다면 true, 그렇지 않으면 false를 반환합니다.

boolean equals(Object o)

o와 같은 객체인지 아닌지 확인합니다.

int hashCode()

이 Collection의 해쉬코드를 반환합니다.

boolean isEmpty()

이 Collection이 비어있는지 확인합니다.

Iterator<E> iterator()

이 Collection의 반복자(Iterator)를 반환합니다. Set같은 Collection은 순서를 고려하지 않기때문에 iterator로 원소를 순회합니다. Iterator의 대표적인 메소드는 hasNext와 next가 있습니다.

boolean remove(Object o)

o와 같은 원소가 이 Collection에 존재한다면 삭제합니다.

boolean removeAll(Collection<?> c)

이 Collection에 Collection c를 모두 제거합니다.

boolean retainAll(Collection<?> c)

매개변수 c의 요소들만을 남겨둡니다.

int size()

원소의 사이즈를 반환합니다.

Object[] toArray()

이 Collection의 모든 요소를 포함하는 배열로 반환합니다.

<T> T[] toArray(T[] a)

지정한 타입의 배열로 변환합니다.

 

 

이제 Collection을 상속받는 List, Set, Queue와 Map에 관한 간단한 설명을 시작하도록 하겠습니다. 한 번더 이야기하자면 아래의 Collection은 전부 인터페이스로 스스로 객체생성이 불가능하다는 것을 알아두세요.

1. List

순서가 중요한 Collection입니다. 순서가 있는 데이터의 집합으로 데이터의 중복을 허용합니다. 기존의 배열과는 다르게 크기가 동적으로 변합니다.

 

파생클래스

Vector, ArrayList : 두 클래스는 같이 설명하겠습니다. 우선 기본적인 동작은 원소를 추가, 삭제하는 것은 둘 다 비슷합니다. 하지만 Vector는 동기화 처리가되어있습니다. 즉, 하나의 스레드만이 Vector에 요소를 추가하거나 삭제가 가능하지요. 스레드에 대해서 안전하지만 느리다는 단점이 있습니다. 하지만 ArrayList는 동기화가 되어있지 않습니다. 그렇기 스레드에 대해 안전하지 않습니다. 하지만 추가, 삭제가 빠르다는 장점이 있습니다.

단일쓰레드 환경에서 개발할때에는 ArrayList를 쓰는것이 바람직하다고 할 수 있습니다.

LinkedList : 자료구조를 배웠다면 이해가 수월할 것인데, 간단히 말하면 각각의 요소는 다음 요소를 가리키고 있어 추가, 삭제 연산이 빠릅니다.

 

 

2. Set

순서가 없는 데이터의 집합으로 데이터의 중복을 허용하지 않습니다. 우리가 중딩시절에 배운 집합을 떠올리면 될텐데요. 참고로 저는 집합이 나오고 수학을 포기했습니다.

 

파생클래스

HashSet : 내부적으로 해싱을 이용해서 구현된 클래스입니다. Set 파생클래스에서 가장 성능이 우수합니다.

TreeSet : 내부적으로 레드-블랙 트리 방식으로 구현된 클래스입니다. 레드- 블랙 트리는 이진 탐색 트리의 일종으로 log(n)의 속도로 삽입, 삭제, 검색이 가능합니다. HashSet보다는 성능이 느립니다.

 

3. Queue

아마 너무나도 잘 알고 있을 Queue는 기본적으로 선입 선출(First-In First-Out) 형식의 자료구조입니다. 먼저 들어온 원소가 먼저 나간다는 Collection입니다.

 

파생클래스

PriorityQueue : 들어온 순서가 아니라 우선순위 별로 Queue에서 원소를 꺼내옵니다. 우선순위는 Comparable 인터페이스로 정할 수 있습니다.

ArrayDeque : 보통의 큐와는 다르게 큐의 양쪽에서 원소를 꺼내올 수 있는 Collection입니다.

 

4. Map

Map은 키-데이터(Key-Value) 쌍으로 자료를 보관하고 있습니다. Map에서 순서는 고려되지 않는 편이며 키는 중복을 허용할 수 없습니다.

 

파생클래스

HashMap : 키-값의 쌍으로 값을 가져올 수 있는 대표적인 Map의 Collection입니다. 동기화를 보장하지 않습니다. 키-값으로 쉽게 데이터를 검색할 수 있습니다. 단일 스레드에서 개발한다면 HashMap을 사용합시다.

또한 HashMap은 특이하게도 키 또는 값에 null을 저장할 수 있다는 점입니다.

Hashtable : HashMap과 사용법이 거의 동일한데, 다른점은 동기화를 보장한다는 것입니다. 그렇기 때문에 HashMap보다는 무겁겠죠?

또 HashMap과는 다르게 키 또는 값에 null을 사용할 수 없습니다.

SortedMap : 이름에서도 알 수 있듯이 정렬이 된 Map구조입니다. 

 

다음 포스팅에는 파생클래스를 사용하는 방법에 대해 알아보도록 하겠습니다.

 

 

 

반응형
블로그 이미지

REAKWON

와나진짜

,