Object Class equals(), hashCode()


Object Class 의 equals() 와 hashCode()를 확인하기 위해 name 필드를 가지는 Car 클래스를 만들었다.

빌드 패턴을 이용한 생성자만 존재하며 equals()는 재정의 되지 않은 상태이다.

public class Car {
    private final String name;

    public static class Builder {
        private final String name;

        public Builder(String name) {
            this.name = name;
        }

        public Car build() {
            return new Car(this);
        }
    }

    private Car(Builder builder) {
        name = builder.name;
    }
}
Car k3 = new Car.Builder("k3").build(); // k3: Car@6996db8
Car k3_2 = new Car.Builder("k3").build(); // k3_2: Car@1963006a

System.out.println("k3: " + k3); // k3: Car@6996db8
System.out.println("k3_2: " + k3_2); // k3_2: Car@1963006a

Set<Car> set = new HashSet<>(Arrays.asList(k3, k3_2)); // set size : 2

System.out.println("set size : " + set.size()); // set size : 2
System.out.println("k3 eq k3_2 : " + k3.equals(k3_2)); // ke eq k3_2 : false
System.out.println("k3 hashcode : " + k3.hashCode()); // 110718392
System.out.println("k3_2 hashcode : " + k3_2.hashCode()); // 425918570

k3 라는 name을 가지는 인스턴스 객체를 2개 생성했으며, equals() 와 hashCode()를 확인해봤다.

Car 클래스 내에서 재정의 하지 않았기 때문에 Object 의 equals와 hashCode를 사용하게 된다.

public class Object {
	public boolean equals(Object obj) {
        return (this == obj);
    }
}

equals 에서는 객체의 주소를 비교하게 된다. k3라는 생성된 객체들은 힙메모리내에서 다른 주소값을 가지고 있기때문에, equals 에서 false를 반환하게 되는 것이다.

public class Object {
	public native int hashCode();
}

 

hashCode가 다른 이유도 Object 클래스의 hashCode()는 주소값을 변환하여 int 타입으로 반환하기 때문에 주소가 다른 객체이므로 hashCode가 다르다.

 

override equals()


이제 직접 Car 클래스에서 equals() 메서드를 재정의 하겠습니다.

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;

    return name.equals(((Car)obj).name);
}

Car 클래스의 equals 메서드는 String 타입의 equals 를 사용하도록 재정의하였다. String 클래스의 equals 메서드는 주소값을 비교하지 않고, 문자열을 비교하기 때문에 같은 name으로 생성된 두 객체는 서로 다른 힙메모리에서 다른 주소값을 갖지만 위와 같은 이유로 true를 반환한다. 

Car k3 = new Car.Builder("k3").build(); // Car@15db9742
Car k3_2 = new Car.Builder("k3").build(); // Car@6d06d69c

Set<Car> set = new HashSet<>(Arrays.asList(k3, k3_2)); 

System.out.println("set size : " + set.size()); // size : 2
System.out.println("k3 eq k3_2 :" + k3.equals(k3_2)); // true
System.out.println("k3 hashcode :" + k3.hashCode()); // 366712642 
System.out.println("k3_2 hashcode :" + k3_2.hashCode()); // 1829164700

재정의 하고 코드를 진행하면 equals 메서드에서 true를 반환한다. 하지만 Set 자료구조의 사이즈에서는 여전히 2를 반환한다. Set 자료구조는 값이 중복될 수 없다. 

euuals 메서드를 재정의해서 name 필드가 같으면 같은 객체라고 정의하였기 때문에, 1개의 객체가 저장되는 것으로 생각했다, 하지만 HashSet 자료구조에서는 2개의 객체가 저장이 되었다. 

 

Hash 를 사용하는 자바 자료구조는 (HashMap, HashSet, HashTable 등) 객체가 논리적으로 같은지 비교할 때 아래와 같다.

Car 클래스는 equals 메서드만 재정의하였고, hashCode() 메서드는 재정의하지 않았기 때문에, Object 클래스의 hashCode() 메서드를 사용하게 된다. 주소값을 반환하기 때문에 같은 name 이여도 객체의 주소가 다르기 때문에 hashCode 값 역시 서로 다르다.

@Override
public int hashCode() {
    return name != null ? name.hashCode() : 0;
}

이제 name 필드의 값이 같으면 hashCode도 모두 동일하다.

Car k3 = new Car.Builder("k3").build(); //Car@d28
Car k3_2 = new Car.Builder("k3").build(); //Car@d28

Set<Car> set = new HashSet<>(Arrays.asList(k3, k3_2));

System.out.println("set size : " + set.size()); // set size : 1
System.out.println("k3 eq k3_2 :" + k3.equals(k3_2)); // true
System.out.println("k3 hashcode :" + k3.hashCode());  // 3368
System.out.println("k3_2 hashcode :" + k3_2.hashCode()); // 3368

재정의 하고 진행하면 Set size 가 1인걸 확인할 수 있다.

 

위와 같은 내용 떄문에 equals를 재정의하려거든 hashCode도 재정의 하라는(아이템 11) 이펙티브 자바에서도 명시하고 있다.