DJDU 2022. 11. 3. 20:03

HashSet - 순서X, 중복X

  • Set인터페이스를 구현한 대표적인 컬렉션 클래스
  • 순서를 유지하려면, LinkedHashSet클래스를 사용하면 된다.

TreeSet

  • 범위 검색과 정렬에 유리한 컬렉션 클래스
  • HashSet보다 데이터 구차, 삭제에 시간이 더 걸림

HashSet의 주요 메서드

더보기

생성자

추가, 삭제

검색

기타

예제 11-9

코드

더보기

저장 순서를 유지하지 않기 때문에 어느 것이 "1"이고 어느 것이 Integer(1)인지 알 수 없다.

import java.util.*;

public class Ex11_9 {
    public static void main(String[] args) {
        Object[] objArr = {"1",new Integer(1),"2","2","3","3","4","4","4"};
        Set set = new HashSet();

        for(int i=0; i < objArr.length; i++) {
            set.add(objArr[i]);	// HashSet에 objArr의 요소들을 저장한다.
        }
        // HashSet에 저장된 요소들을 출력한다.
        System.out.println(set);  // [1, 1, 2, 3, 4]


set.add(objArr[i]) 직접 출력해보기 테스트

        for(int i=0; i < objArr.length; i++) {
//             set.add(objArr[i]);	// HashSet에 objArr의 요소들을 저장한다.
            System.out.println(objArr[i] + "=" + set.add(objArr[i]));
        }

결과

1=true
1=true
2=true
2=false
3=true
3=false
4=true
4=false
4=false

예제 11-10

코드

더보기

정렬하지 않고 출력 테스트

class Ex11_10 {
    public static void main(String[] args) {
        Set set = new HashSet();
        // set의 크기가 6보다 작은 동안 1~45사이의 난수를 저장
        for (int i = 0; set.size() < 6 ; i++) {
            int num = (int)(Math.random()*45) + 1;
//          set.add(new Integer(num));
            set.add(num);
        }
        System.out.println(set);
//        List list = new LinkedList(set); // LinkedList(Collection c)
//        Collections.sort(list);          // Collections.sort(List list)
//        System.out.println(list);
    }
}

결과

[2, 3, 24, 25, 43, 14]  // 실행할 때 마다 결과값이 다를 수 있음

sort()의 매개변수로는 List만 올 수 있기 때문에, Set은 올 수 없다. 정렬은 '순서 유지'가 되어야 한다. 그래서 Set은 정렬할 수 없다. 그래서 Set을 List로 옮기고, List를 정렬해야 한다. 그게 아래 코드이다.

  1. Set의 모든 요소를 List에 저장
  2. list를 정렬
  3. list를 출력

대부분의 Collection클래스들은 생성자에 Collection을 받는 생성자가 있다. 그래서 다른 컬렉션으로 쉽게 바꿀 수 있는 방법들을 제공한다.

예제 11-11

  • HashSet은 객체를 저장하기 전에 기존에 같은 객체가 있는지 확인
  • 같은 객체가 없으면 저장하고, 있으면 저장하지 않는다.
  • boolean add(Object o)는 저장할 객체의 equals()와 hashCode()를 호출
  • equals()와 hashCode()가 오버라이딩되어있어야 함
  • 코드
더보기

HashSet은 객체를 저장하기 전에 기존에 같은 객체가 있는지 확인

같은 객체가 없으면 저장하고, 있으면 저장하지 않는다.

Set은 순서를 유지하지 않고, 중복을 허용하지 않는다. HashSet과 같이 Set인터페이스를 구현한 클래스는 객체를 저장할 때, 기존에 같은 객체가 있는지 '중복'을 확인해야 한다. 무조건 저장할 수 없다.

| 참고 | List는 중복을 허용하기 때문에, 무조건 저장한다.

boolean add(Object o)는 저장할 객체의 equals()와 hashCode()를 호출
equals()와 hashCode()가 오버라이딩되어있어야 함

add메서드는 저장하기 전에, equals메서드와 hashCode를 이용해서 중복 여부를 확인한다.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return name +":"+ age;
    }
}

name, age 즉 이름과 나이를 iv로 갖는 Person클래스가 있을 때, Person객체를 HashSet에 boolean add(Object o)를 사용하여 추가하려 한다. 그럴 때 Person클래스에서 equals()와 hashCode()를 호출한다.

하지만 지금은 equals()와 hashCode()가 Person클래스에 없다. 사실은, 두 메서드는 모든 클래스의 최고 조상인 Object클래스에서 상속 받았기 때문에 Person클래스는 두 메서드를 가지고 있다.

그런데 문제는, equals()와 hashCode()가 오버라이딩되어있지 않으면 HashSet이 제대로 동작하지 않는다. 두 메서드를 오버라이딩해야 객체의 중복을 확인할 수 있다. 사실 equals메서드만 오버라이딩하면 되는데 hashCode메서드도 같이 오버라이딩하는 것이 정석이다. HashSet과 같이 Hash가 붙은 클래스들은 hashCode메서드를 내부적으로 이용한다. 필수는 아니지만 그렇게 하는 것이 좋다.

이제 equals()와 hashCode()를 어떻게 오버라이딩하는지 알아보자.

public boolean equals(Object obj) {
    if(!(obj instanceof Person)) return false;  // 참조변수 형변환 이전 형변환 가능여부 확인
    
    Person tmp = (Person)obj;  // 참조변수 형변환
    
    return name.equals(tmp.name) && age==tmp.age;  // 객체 자신(this)과 매개변수로 지정된 객체(obj)를 비교
}

public int hashCode() {
    return (name+age).hashCode();
}

equals메서드의 경우 iv값들(name, age)을 비교하도록 오버라이딩한다. hashCode()의 경우 String의 hashCode()를 호출하도록 오버라이딩한다. name이 String이기 때문에 int인 age를 더하면 String이 된다. 이건 예전 버전이다.

요즘에는 Objects.hashCode(name + age); 이런 식으로 오버라이딩한다. 원래는 우리가 직접 만들어줘야 하는데 Objects클래스를 제공하여 좀 더 편리하게 작성하도록 도와준다.

Ex11_11

equals()와 hashCode()를 오버라이딩해야 HashSet이 바르게 동작한다.

[abc, David:10, David:10]

Ex11_11.java를 실행하면, David:10이 중복되어 HashSet에 저장되고 있다. 중복 제거가 제대로 되지 않고 있다. 이는 equals()와 hashCode()를 오버라이딩하지 않았기 때문이다.

IDE 기능을 활용하여 equals()와 hashCode()를 오버라이딩해보자.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return name +":"+ age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

그대로 쓰지 말고 변경한다.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String toString() {
        return name +":"+ age;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Person)) return false;
        
        Person p = (Person)obj;
        // 나 자신(this)의 이름과 나이를 p와 비교
        return this.name.equals(p.name) && this.age==p.age
    }

    @Override
    public int hashCode() {
        // int hash(Object... values); // 가변인자
        return Objects.hash(name, age);
    }
}

그 다음 실행을 해본다.

결과

[David:10, abc]

"David:10"이 한 번만 출력되는 것을 보아 중복 처리가 제대로 된 것을 확인할 수 있다.

예제 11-12

더보기

풀이 2

// 풀이 2 - 각 코드 별로 주석처리를 해주고 하나씩만 출력 가능
        // setA.retainAll(setB); // 교집합. 공통된 요소만 남기고 삭제
        // System.out.println("A ∩ B = " + setA);
        // setA.addAll(setB);    // 합집합. SetB의 모든 요소를 추가(중복 제외)
        // System.out.println("A U B = " + setA);
        // setA.removeAll(setB); // 차집합. SetB와 공통 요소를 제거
        // System.out.println("A - B = " + setA);

그런데 setA를 변경하지 않고 합집합, 교집합, 차집합 등의 결과를 보고 싶다면 풀이 2보다는 풀이 1이 적합하다.