HashMap, HashSet과 Iterator
HashMap은 Map 인터페이스를 구현한 컬렉션 클래스로 Map의 특징인 키와 값을 묶어서 하나의 데이터(entry)로 저장하며 해싱을 사용하기 때문에 많은 양의 데이터를 검색하는데 있어서 뛰어난 성능을 보인다. HashMap은 키와 값을 각각 Object 타입으로 저장하고 키와 값을 저장하기 위해 Map의 내부 인터페이스인 Map.Entry 인터페이스를 구현한다. 이는 키와 값이 서로 연관되었기 때문에 이들을 별개로 다루지 않고 Entry라는 타입으로 묶어서 취급하는 것이다.
Object[] key;
Object[] value;
Entry[] table;
class Entry {
Object key;
Object value;
}
HashSet은 Set 인터페이스를 구현한 가장 대표적인 컬렉션 클래스이며 Set 인터페이스의 특징대로 중복된 요소를 저장하지 않고 저장 순서를 유지하지 않는다. 만약 저장 순서를 유지하고자 한다면 LinkedHashSet을 사용해야 한다.
List 인터페이스를 구현하는 컬렉션들은 인덱스를 통해 값에 접근할 수 있었다. 하지만 HashSet의 경우 저장 순서가 없으므로 인덱스로 값을 읽어 올 수 없다. 대신 Iterator 인터페이스를 이용하면 된다.
컬렉션 프레임워크에서는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화하였다. List와 Set 인터페이스의 부모 인터페이스인 Collection 인터페이스는 Iterable 인터페이스를 상속 받고있으며 이 Iterable 인터페이스는 Iterator를 생성하도록 하고 있다. Iterator, ListIterator, Enumeration 모두 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다.
Collection 인터페이스에는 Iterable 인터페이스에 정의된 ‘Iterator를 구현한 클래스의 인스턴스’를 반환하는 iterator()가 구현되어 있다.
public interface Iterable<T> {
Iterator<T> iterator();
...
}
public interface Iterator<E> {
boolean hasNext();
E next();
...
}
public interface Collection<E> extends Iterable<E> {
...
Iterator<E> iterator();
...
}
public interface Set<E> extends Collection<E> {
...
Iterator<E> iterator();
...
}
List나 Set 인터페이스를 구현하는 컬렉션 클래스는 iterator()가 각 컬렉션의 특징에 알맞게 작성되어 있다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
...
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
...
}
...
}
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
...
public Iterator<E> iterator() {
return map.keySet().iterator();
}
...
}
컬렉션 클래스에 대해 iterator()를 호출하여 Iterator를 얻은 다음 반복문을 사용해서 컬렉션 클래스의 요소들을 읽어 올 수 있다.
public static void main(String[] args) {
Object[] objects = {"1", new Integer(1), "2", "2", "3", "3", "3", "4", "4"};
HashSet set = new HashSet<>(Arrays.asList(objects));
Iterator it = set.iterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
// 실행 결과
// 1 1 2 3 4
}
실행 결과 1이 두 번 출력된 것을 알 수 있다. 하나는 String 인스턴스이고 다른 하나는 Integer 인스턴스로 서로 다른 객체이기 때문에 중복으로 간주되지 않은 것이다.
Map 인터페이스를 구현한 컬렉션 클래스는 키와 값을 쌍(pair)으로 저장하고 있기 때문에 iterator를 직접 호출할 수 없고, 대신 keySet()이나 entrySet()과 같은 메서드를 통해 키와 값을 각각 따로 Set의 형태로 얻어온 후에 다시 iterator()를 호출해야 Iterator를 얻을 수 있다.
HashMap map = new HashMap();
...
Iterator it = map.entrySet().iterator();
Customer 클래스는 이름(name)과 휴대전화 번호 끝 4자리(phoneNum)를 멤버 변수로 갖는다. 이름과 휴대전화 번호가 같으면 같은 사람으로 인식해 중복으로 고객 리스트를 작성하지 않으려고 한다.
import java.util.*;
public class Customer {
String name;
String phoneNum;
public Customer(String name, String phoneNum) {
this.name = name;
this.phoneNum = phoneNum;
}
@Override
public String toString() {
return "[" + name + " - " + phoneNum + "]";
}
public static void main(String[] args) {
Customer[] customers = {
new Customer("David", "1111"),
new Customer("David", "1111"),
new Customer("Alice", "2222"),
new Customer("Kelly", "6777")
};
HashSet customerList = new HashSet<>(Arrays.asList(customers));
System.out.println(customerList);
}
}
하지만 실행 결과, 이름과 번호가 같음에도 서로 다른 사람으로 인식하여 David가 2번 저장되었다. 이런 경우 Customer 클래스는 equals()와 hashCode()를 오버라이딩해야 한다. HashSet에서 새로운 요소를 추가하기 전에 기존에 저장된 요소와 같은 것인지 판별하기 위해 추가하려는 요소의 equals()와 hashCode()를 호출하기 때문이다.
import java.util.*;
public class Customer {
...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return name.equals(customer.name) && phoneNum.equals(customer.phoneNum);
}
@Override
public int hashCode() {
return Objects.hash(name, phoneNum);
}
...
}
HashMap과 HashSet에 공통적으로 ‘hash’라는 단어가 들어가는 이유는 바로 중복되는 값을 찾기 위해 해싱 알고리즘을 사용하기 때문이다. Set에서는 같은 객체인지 판별하기 위해, Map에서는 같은 키인지 판별하기 위해 hashCode()를 사용한다.