9) 컬렉션 프레임워크5 - Hash와 Iterator

dev-mage·2022년 11월 28일
0

Hello Java World!

목록 보기
29/32
post-thumbnail

HashMap, HashSet과 Iterator

HashMap

HashMap은 Map 인터페이스를 구현한 컬렉션 클래스로 Map의 특징인 키와 값을 묶어서 하나의 데이터(entry)로 저장하며 해싱을 사용하기 때문에 많은 양의 데이터를 검색하는데 있어서 뛰어난 성능을 보인다. HashMap은 키와 값을 각각 Object 타입으로 저장하고 키와 값을 저장하기 위해 Map의 내부 인터페이스인 Map.Entry 인터페이스를 구현한다. 이는 키와 값이 서로 연관되었기 때문에 이들을 별개로 다루지 않고 Entry라는 타입으로 묶어서 취급하는 것이다.

  • 비객체지향적인 코드
Object[] key;
Object[] value;
  • 객체지향적인 코드
Entry[] table;
class Entry {
		Object key;
		Object value;
}

HashSet

HashSet은 Set 인터페이스를 구현한 가장 대표적인 컬렉션 클래스이며 Set 인터페이스의 특징대로 중복된 요소를 저장하지 않고 저장 순서를 유지하지 않는다. 만약 저장 순서를 유지하고자 한다면 LinkedHashSet을 사용해야 한다.

List 인터페이스를 구현하는 컬렉션들은 인덱스를 통해 값에 접근할 수 있었다. 하지만 HashSet의 경우 저장 순서가 없으므로 인덱스로 값을 읽어 올 수 없다. 대신 Iterator 인터페이스를 이용하면 된다.

컬렉션에 저장된 요소 읽어 오기 - Iterator, ListIterator, Enumeration

컬렉션 프레임워크에서는 컬렉션에 저장된 요소들을 읽어오는 방법을 표준화하였다. List와 Set 인터페이스의 부모 인터페이스인 Collection 인터페이스는 Iterable 인터페이스를 상속 받고있으며 이 Iterable 인터페이스는 Iterator를 생성하도록 하고 있다. Iterator, ListIterator, Enumeration 모두 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스이다.

  • Iterator: 컬렉션에 저장된 요소를 접근하는데 사용되는 인터페이스.
  • ListIterator: Iterator에 양방향 조회 기능을 추가한 인터페이스(List를 구현한 경우만 사용 가능).
  • Enumeration: Iterator의 구버전 → Iterable 인터페이스 구현 X

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과 Iterator

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);
    }
		...
}
  • 실행 결과

hash

HashMap과 HashSet에 공통적으로 ‘hash’라는 단어가 들어가는 이유는 바로 중복되는 값을 찾기 위해 해싱 알고리즘을 사용하기 때문이다. Set에서는 같은 객체인지 판별하기 위해, Map에서는 같은 키인지 판별하기 위해 hashCode()를 사용한다.


Reference

  • 자바의 정석 CHAPTER 11

0개의 댓글