본문 바로가기

Backend/Java

[JAVA] HashSet, HashMap, Hashtable 그리고 Iterator

 

 


HashSet, HashMap, Hashtable과 이들의 값을 출력하는 Iterator


 

 

1. HashSet


Set(HashSet) 는 데이터의 중복을 허용하지 않고 저장되는 클래스다. 중복된 값을 제거하면서 데이터를 저장하기 위해 사용된다.

 

import java.util.HashSet;

 

 

여기서 다룰 HashSet부터 뒤이어 등장할 HashMap, Hashtable.. Hash는 '특정 데이터를 고정된 값으로 반환한다.' 는 의미를 지닌 자료구조다. hash 연산은 클래스 내에서 유일한 값을 얻어낼 수 있는 고유 알고리즘 연산으로 이해하면 된다.

Set의 종류인 HashSet의 경우 저장할 때 hash 연산 결과로 저장하고, 검색 또한 hash 연산결과로 검색한다. 보통 빠른 검색이 가능하기 때문에 검색을 위해 사용된다. 라고 일단은 알고 있자.

 

 

ArrayList<Integer> a = new ArrayList<Integer>();
HashSet<Integer> h = new HashSet<Integer>();

 

ArrayList와 HashSet 객체를 선언하여 중복된 값을 허용하지 않는다는 특징을 예시를 통해 알아보자.

 

 

// 중복허용이 되기 때문에 1 2 2 모두 리스트에 저장
a.add(1);
a.add(2);
a.add(2);

// 중복허용이 안 되기 때문에 마지막 2는 무시됨
h.add(1);
h.add(2);
h.add(2);

 

hashSet 또한 add() 메소드를 이용해 값을 저장할 수 있다. 각각의 객체에 1, 2, 2 값을 넣어주자.

 

 

System.out.println("ArrayList : ");
for(Integer k : a) System.out.printf("%d ", k);

System.out.println("\nHashSet : ");
for(Integer k : h) System.out.printf("%d ", k);

 

같은 값을 대입했음에도 불구하고 HashSet에는 중복된 2 값 하나가 들어가지 않은 것을 알 수 있다.

 

 

 

2. Iterator


HashSet 을 가지고 로또 번호 발생 코드를 작성하면서 동시에 내부 값을 순회하는 Iteraotr(방문자) 방법에 대해서 알아볼 것이다.

 

// 객체 선언 시 앞쪽 자료형에 제네릭이 명확히 작성되었다면 뒤쪽은 빈칸으로 써도 무방
HashSet<Integer> lotto = new HashSet<>();
while(lotto.size() < 6)
    lotto.add((int)(Math.random() * 45) +1);

 

우선, Math.random() 메소드를 통해 6개의 로또 번호를 생성한다.

 

해당 코드를 상세히 분석해보자면...

  1. lotto.size() 의 크기가 6 이하라면(6이 되기 전까지) while문을 돌린다.
  2. lotto.add() 는 (괄호)안의 값을 HashSet 자료형 객체 lotto에 대입한다.
  3. Math.random() * 45 으로 0~44 까지의 값을 랜덤하게 받는다.
  4. 로또는 1~45 까지라서 랜덤값에 +1 을 해준다.
  5. random으로 리턴되는 값은 실수이기 때문에 (캐스팅연산)을 통해 int 형으로 바꾸어준다.

 

// 값 출력
System.out.println("생성 확인 출력");
for(Integer k : lotto)
    System.out.printf("%d ", k);

 

 

앞서 계속해서 for( (자료형) (리스트값을 담을 변수) : (리스트 변수) ) 의 형태로 출력을 하곤 했는데 이제는 Iterator 라는 새로운 방식을 다뤄보겠다. 해당 방법은 List, HashSet 을 포함한 값이 여러개인 자료들에 사용할 수 있다.

 

Iterator<Integer> iter1 = lotto.iterator();

 

HashSet 으로 선언한 lotto값에 접근할 수 있는 권한을 Iteraotr 객체에 위와 같이 저장한다. 이때 어떤 자료형으로 받을 것인가에 대한 명시(제네릭)가 필요하다. 위 예시에서 생성된 Iterator 타입의 iter1 변수는 대상이 되는 리스트의 위치정보를 저장하고 있는 참조변수로, next()에 의해 다음 참조값들로 교체되면서 데이터에 접근한다.

 

 

while(iter1.hasNext())
    System.out.printf("%d ", iter1.next());

 

값을 순회하기 위해서 위와 같이 while 문을 작성한다.

여기서 .hasNext()는 방문할 다음 같이 있다면 true, 없다면 false를 리턴하는 메소드로, iter1 으로 받은 값의 존재 여부에 따라 while 조건이 맞춰지게 된다. 출력을 위해 데이터 값을 리턴하기 위해서는 .next() 메소드를 사용한다.

 

이렇게 Iterator로 모든 데이터를 한 번씩 방문했다면, 다시 읽어들이기 위해서는 Iterator 를 재설정 해야한다.

 

iter1 = lotto.iterator(); // iter1 재설정
while(iter1.hasNext())
    System.out.printf("%d ", iter1.next());

 

iter1 을 재설정해도 당연히 값은 똑같이 나온다.

 

 

참고로 뒤에 HashMap과 Hashtable에서 Iterator와 비슷하게 값을 하나씩 참조하는 Enumeration 에 대해서 예시로 언급한다. 기능은 비슷하나 메소드 형태가 다르다. 놀라지 말자.

 

 

 

3. HashMap, Hashtable


HashMap과 Hashtable은 데이터베이스 내부의 키 값을 검색하기 위해 만들어진 알고리즘을 기반으로 작성된 클래스이다. 마찬가지로 검색을 위해 사용되며 특이하게 Key와 Value 값을 저장할 수 있다. Key에 따른 Value를 지정해주는 것이다. Key를 통해 Value를 불러오기 때문에 Key값은 중복을 허용하지 않고, Value값 중복을 허용한다.

 

이들은 ArrayList와 함께 웹프로그래밍에서 핵심 자료구조로 활용된다.

 

Hashtable<String, Integer> ht = new Hashtable<>();
HashMap<Integer, String> hm = new HashMap<>();

 

선언의 형태는 여타 new 연산자를 통해 객체를 생성하는 방식과 동일하다. 한가지 유의해야 할 점은 Key 값과 Value 값의 자료형을 제네릭으로 각각 지정해주어야 한다.

 

예시에서는 Hashtable의 Key값을 String, Value값은 Integer형으로 받았고,

HashMap의 Key값은 Integer, Value값은 String형으로 받았다.

 

 

ht.put("One", 1);
ht.put("Two", 2);
ht.put("Three", 3);

hm.put(1, "One");
hm.put(2, "Two");
hm.put(3, "Three");

 

HashMap과 Hashtable은 put() 메소드를 이용해 Key값과 Value값을 대입한다. Key값이 먼저, 그 뒤가 Value값이며 ,(콤마)로 구분한다. 당연하지만 자료형에 맞춰서 대입해주자.

 

 

System.out.println("Hashtable의 size : " + ht.size());
System.out.println("HashMap의 size : " + hm.size());

 

put() 메소드를 이용해 대입해준 값만큼 객체의 사이즈가 정해지며, size는 size() 메소드를 통해 알 수 있다.

 

 


앞서 value값은 중복을 허용하지만, key값은 중복을 허용하지 않는다고 했다. 만약 중복된 key값이 대입되면 어떻게 될까?

결론부터 말하자면 key값이 새로 대입된 값으로 대체된다.

 

아래 예시를 통해 살펴보자.

// Key값은 중복되지 않고, 중복된 Value값으로 데이터를 추가
ht.put("Six", 1);
hm.put(6, "One");

 

먼저 중복되지 않는 key값을 대입하고 key-value가 잘 보이도록 출력했다.

 

// 동일한 Key값으로 다른 Value가 입력되는 경우
ht.put("Six", 6);
hm.put(6, "Six");

 

이후 동일한 key값의 key-value를 대입했더니, 새로운 값으로 대체된 모습을 확인할 수 있다.

 

참고로 Hashtable과 HashMap은 저장된 자료들이 key값으로 구분될 뿐 순서가 없어서 무순위로 출력된다. 출력 결과에 대해서는 깊게 생각하지 않아도 된다.


 

 

// get 메소드에 인덱스(0,1,2...) 대신 Key값을 전달하여 Value값을 리턴 받음
Integer i1 = ht.get("One");
Integer i2 = ht.get("Two");
Integer i3 = ht.get("Three");

String s1 = hm.get(1);
String s2 = hm.get(2);
String s3 = hm.get(3);

System.out.println("Hashtable의 value들 : " + i1 + " " + i2 + " " + i3);
System.out.println("HashMap의 value들 : " + s1 + " " + s2 + " " + s3);

 

Value값을 가져오기 위해서는 get() 메소드를 사용한다. 이때, get의 매개변수로 Key값을 작성한다. key에 대응하는 value값을 추출해야 하기 때문에 value 추출을 위해서 key값 입력은 필수이다.

 

 

그렇다면 key값은 어떻게 출력할까?

두가지 방법을 알아보자.

 

1. Key값들을 리스트 형식으로 얻어서 저장

Enumeration<String> e1 = ht.keys();

 

Iterator와 비슷한 기능을 하는 Enumeration을 이용해 hashtable의 key값을 추출한다. key값은 keys() 메소드를 통해 얻는다.

 

while(e1.hasMoreElements()) {
    String k = e1.nextElement(); // 현재 위치의 키값 하나를 추출
    
    int v = ht.get(k); // key에 대응하는 value
    System.out.printf("key(%s) = Value(%d)\n", k, v);
}

 

hasMoreElements() 메소드를 통해 값이 존재하는지 여부를 판단하고 (존재하면 true, 없으면 false)

nextElement() 메소드로 현재 위치의 키값 하나를 추출한다.

 

위 예시에서는 key에 대응하는 value값도 추출하기 위해 임의로 변수 v를 선언했다.

 

 

2. keySet 으로 키 값 모으기 (조금 더 간단)

// hm.keySet() : 키값들만 모아서 set 리스트로 생성
for(Integer k : hm.keySet()) {
    String v = hm.get(k); // key에 대응하는 value 추출
    System.out.printf("key(%s) = Value(%s)\n", k, v);
}

 

hashMap(Hashtable) 객체에 keySet() 메소드를 이용하면 key값을 set 리스트로 모을 수 있다. (key값은 중복이 되지 않기 때문에 set리스트로 받아도 무관하다.) 이를 Iterator를 이용해 for문을 돌려 위와 같이 출력할 수 있다.

 

 

 

4. HashMap의 확장성


HashMap을 이용하면 확장성이 향상된다.

 

아래와 같이 기억하기 쉬운 key값으로 저장한다면, 여러 자료를 혼용해서 저장하고 쉽게 꺼내어 쓸 수 있다.

HashMap<String, Object> hmap = new HashMap<>();
hmap.put("intVar", 10);
hmap.put("doubleVar", 10.123);
hmap.put("stringVar", "abcd");

String[] s = {"korea", "usa", "canada"};
hmap.put("stringArrVar", s); // 배열도 저장 가능

 

Value 값을 Object로 받는다면 자료형에 상관없이 저장받을 수 있다.

 

Integer intVar = (Integer)hmap.get("intVar");
Double doubleVar = (Double)hmap.get("doubleVar");
String strVar = (String)hmap.get("stringVar");
String[] strArrVar = (String[])hmap.get("stringArrVar");

System.out.println("intvar : " + intVar);
System.out.println("intvar : " + doubleVar);
System.out.println("intvar : " + strVar);
System.out.println("intvar : " + strArrVar);

strArrVar 배열은 참조변수라서 참조값이 출력됐다.

 

배열을 포함한 각종 리스트의 형태도 value 값에 저장할 수 있다.

 

ArrayList<String> al = new ArrayList<String>();
HashMap<Integer, ArrayList<String>> hm3 = new HashMap<>();

al.add("abcd");
al.add("bced");
hm3.put(1, al);

 

조금 복잡한 형태지만..

  1. ArrayList 자료형의 al 객체를 생성하고
  2. value값에 ArrayList<String> (al을 담을 수 있는 환경) 을 제네릭으로 지정한 뒤
  3. add() 를 통해 al 에 값을 넣고
  4. 값이 들어간 ArrayList를 HashMap의 key : 1 - value : al 로 넣을 수 있다.