Set<E> 인터페이스를 구현하는 컬렉션 클래스들

gustjtmd·2022년 1월 11일
0

Java

목록 보기
22/40

Set을 구현하는 클래스의 특성과 HashSet클래스

Set 인터페이스를 구현하는 제네릭 클래스의 특성 두 가지를 정리하면 다음과 같다
  • 저장 순서가 유지되지 않는다.
  • 데이터 중복 저장을 허용하지 않는다.
Set<String> set = new HashSet<>();
        set.add("Toy");
        set.add("Box");
        set.add("Robot");
        set.add("Box");
        System.out.println("인스턴스 수  : " +set.size());

        //반복자 이용한 전체 출력
        for(Iterator<String> itr = set.iterator(); itr.hasNext();)
            System.out.print(itr.next()+ '\t');
        System.out.println();

        //for -each문을 이용한 전체 출력
        for(String s : set)
            System.out.print(s + '\t');
        System.out.println();
 출력결과
 인스턴스 수  : 3
Box	Robot	Toy	
Box	Robot	Toy	

------------------------------------------------------------

출력 결과를 통해 동일한 인스턴스가 저장되지 않음을 알 수 있다.
그런데 동일한 데이터(인스턴스로) 판단하는 기준은 무엇일까? 다음 예제는 이 질문에 대해서
폭 넓은 생각을 하게된다.
class Num{
    private int num;
    public Num(int n){num = n;}

    @Override
    public String toString(){
        return String.valueOf(num);
    }
}

public class HashSetEqualityOne {
    public static void main(String[] args) {
        HashSet<Num> set = new HashSet<>();
        set.add(new Num(7799));
        set.add(new Num(9955));
        set.add(new Num(7799));
        System.out.println("인스턴스 수 : "+set.size());

        for(Num n : set)
            System.out.print(n.toString() + '\t');
        System.out.println();
    }
}
출력값
인스턴스 수 : 3
7799	7799	9955	
----------------------------------------------------------------
우리 관점에서 보면 다음과 같이 저장한 두 개의 Num 인스턴스는 동일한 인스턴스로 생각할수
있다. 지니고 있는 값이 같으니 말이다

그러나 실행 결과 이 둘이 다른 인스턴스로 간주됨을 보이고 있는데 이는 HashSet<E>가
판단하는 동일 인스턴스 기준은 Object클래스에 정의되어 있는 다음 두 메소드의 호출 결과를
근거로 하기 때문이다

public boolean equals(Object obj)
public int hashCode()
두 메소드가 어떻게 사용 되는지 이해하기 위해 해쉬 알고리즘에 간단한 이해가 필요하다.

해쉬 알고리즘과 hashcode메소드

적용 해쉬 알고리즘 : hashcode(){return num%3}
분류 대상 : 3, 5, 7, 12, 25, 31

이렇게 세 개의 부류로 나뉜 상태에서 정수 5의 존재 여부를 확인하는 가장 효율적인 방법을
생각해보자 모든 정수들이 3으로 나눈 나머지를 기준으로 나뉘어 있으니 우선 존재 여부
확인 대상인 정수 5를 3으로 나머지 연산을 하여 속하는 부류를 찾는것이 우선이다.

5 % 3 = 2

이로써 연산의 결과가 0과 1인 부류는 탐색 대상에서 제외되었다. 즉 탐생 대상이 줄은것이다!
그리고 이것이 해쉬 알고리즘을 사용하는 이유
정수 5의 존재 여부를 확인하는 과정을 정리하면 다음과 같은데 두 단계를 거쳐서 탐색을
진행하기 때문에 탐색 속도는 빠르다.

탐색 1단계 : 정수 5의 해쉬 값을 계산하여 탐색 부류를 결정
탐색 2단계 : 선택된 부류 내에 정수 5가 존재하는지 확인

그리고 위의 두 단계를 거쳐서 동일 인스턴스의 존재 여부를 확인하는 클래스가
HashSet<E>이다. 이 클래스의 탐색 과정은 다음과 같다.

탐색 1단계 : Object 클래스에 정의된 hashCOde 메소드의 반환 값을 기반으로 부류 결정.
탐색 2단계 : 선택된 부류 내에서 equals 메소드를 호출하여 등등 비교
 

앞서 보인 7799를 담고 있는 두 인스턴스가 서로 다른 인스턴스로 간주된 이유

Object 클래스에 정의되어 있는 hashCode equals 메소드는 다음과 같이 정리되어
있다.
Object 클래스의 hashCode 메소드는 인스턴스가 저장된 주소값을 기반으로 반환값이
만들어지도록 되어있다.
"인스턴스가 다르면 Object 클래스의 hashCode메소드는 다른 값을 반환한다."
"인스턴스가 다르면 Object 클래스의 equals 메소드느 false를 반환한다."
Object 클래스의 hashCode와 equals는 저장하고 있는 값을 기준으로 인스턴스의
등등 여부를 따지지 않는다 그래서 7799를 담고있는 두 인스턴스는 서로 다른 인스턴스로
간주 되었다.
따라서 값을 기준으로 여부를 따지도록 하려면  
@Override
public boolean equals(Object obj) 오버라이딩 해야한다.
class Num{
    private int num;
    public Num2(int n) {num = n;}

    @Override
    public String toString(){
        return String.valueOf(num);
    }

    @Override
    public int hashCode(){
        return num % 3;     //num의 값이 같으면 부류도 같다.
    }

    @Override
    public boolean equals(Object obj){  //num값이 같으면 true 반환
        if(num ==((Num)obj).num)
            return true;
        else
            return false;
    }
}
public class HashSetEqualityTwo {
    public static void main(String[] args) {
        HashSet<Num2> set = new HashSet<>();
        set.add(new Num(7799));
        set.add(new Num(9955));
        set.add(new Num(7799));
        System.out.println("인스턴스 수 : " +set.size());

        for(Num2 n : set)
            System.out.print(n.toString() +'\t');
        System.out.println();
    }
}
결과값 : 
인스턴스 수 : 2
9955	7799	

hashcode 메소드의 다양한 정의

다음과 같이 둘 이상의 값을 지닌 클래스의 경우 내용 비교를 위한 hashCode와 equas메소드는
어떻게 정의하는게 좋을까? 인스턴스가 지닌 모든 값이 동일할때 동일 인스턴스로 간주하도록
정의하려면??

class Car{
	private String model;
    private String color;
    public Car(String m, String c){
    	model = m;
        color = c;
    }
    @Override
    public String toString(){return model =" : " color;}
    
    @Override
    public int hashCode(){
    	return (model.hashCode() + color.hashCode()) /2;
    }
}
--------------------------------------------------------------
두 참조변수는 String 인스턴스를 참조한다. String 클래스의 hashCode와 equals메소드는
내용 비교를 하도록 적절히 오버라이딩 되어있다. 따라서 위에서 보이는 방법을 고려해 볼 수 있다.
class Car{
    private String model;
    private String color;

    public Car(String m, String c){
        model = m;
        color = c;
    }
    @Override
    public String toString(){
        return model + " : " + color;
    }

    @Override
    public int hashCode(){
        return (model.hashCode() + color.hashCode()) / 2;
    }
    @Override
    public boolean equals(Object obj){
        String m = ((Car)obj).model;
        String c = ((Car)obj).color;

        if(model.equals(m) && color.equals(c))
            return true;
        else
            return false;
    }
}
public class HowHashCode {
    public static void main(String[] args) {
        HashSet<Car> set = new HashSet<>();
        set.add(new Car("HY_MD_301", "RED"));
        set.add(new Car("HY_MD_301", "BLACK"));
        set.add(new Car("HY_MD_302", "RED"));
        set.add(new Car("HY_MD_302","WHITE"));
        set.add(new Car("HY_MD_301","BLACK"));
        System.out.println("인스턴스 수 : "+ set.size());

        for(Car car : set)
            System.out.println(car.toString() + '\t');
    }
}
결과 : 
인스턴스 수 : 4
HY_MD_301 : RED	
HY_MD_302 : RED	
HY_MD_301 : BLACK	
HY_MD_302 : WHITE	
그런데 클래스를 정의할때마다 hashCode 메소드를 정의하는 것은 번거로운 일이다.
특히 해쉬 알고리즘의 성능적 측면까지 고려하면서 모든 클래스를 정의하기란 쉬운일이 아니다.
따라서 자바는 다음 메소드를 제공하고 있다.

public static int hash(Ojbect....value)
-> java.util.Ojbects에 정의된 메소드, 전달된 인자 기반의 해쉬값 반환

위 메소드의 매개변수 선언에는 '가변 인자 선언'이 포함되어 있는데 이는 전달되는 인자의 수를
메소드 호출시마다 달리할수 있는 선언이다.(추후에 설명)
그리고 이 hash 메소드를 이용하여 위 예제의 hashCode 메소드를 다음과 같이 오버라이딩 
할 수 있다.

@Override
public int hashCode(){
	return Ojbects.hash(model, color) 
    //전달인자 model, color 기반 해쉬값 반환.
}
이렇듯 hash 메소드는 하나 이상의 인자를 조합하여 하나의 해쉬값을 만들어 반환.
따라서 특별한 경우가 아니라면 직접 해쉬 알고리즘 만들지 않고 이 메소드에 의존하자.
profile
반갑습니다

0개의 댓글