영호

eqauls, hashCode(feat. 동일성과 동등성) 본문

Language/JAVA

eqauls, hashCode(feat. 동일성과 동등성)

0h0 2023. 3. 4. 19:31

동일성

두 객체가 물리적으로 같은 주소에 저장되어 있는지에 대한 성질로 ==연산자를 통해 동일성을 비교할 수 있습니다.

동등성

두 객체의 필드값이 같은지에 대한 성질입니다. 두 객체의 필드 값이 같다면 두 객체는 논리적으로 동등하다고 할 수 있고, equals()를 통해 비교 가능합니다.

객체 비교 방법

우리는 객체가 같은지 비교할 때 equals()를 사용한다. 그 이유는 ==은 객체의 주소를 비교해 두 객체가 동일한지 물리적 동일성을 비교한다. equals()는 객체의 주소가 아닌 내부 필드값들이 같은지 논리적 동등성을 비교한다.

이제부터 equals를 쓰기 위해 equals, hashCode에 대해 알아보겠습니다. equals와 hashCode를 재정의 하면 contains에서도 이점을 볼 수 있습니다.

equals()

eqauls()는 위에서 적었듯이 두 객체의 논리적 동등성을 비교합니다. 즉, new를 통해 2개의 인스턴스를 생성할 때, 필드를 동일한 값으로 설정하고 equals()를 사용하면 true가 반환됩니다. 그러나 저희의 의도대로 equals()가 동작하려면 오버라이딩이 필요합니다. 오버라이딩은 인텔리제이에서 제공해 주기 때문에 이것을 사용하겠습니다.

우선 Human클래스를 만들고 인스턴스 변수로 String name, String Address를 선언해 줍니다.

public class Human {
    private final String name;
    private final String address;

    Human(final String name, final String address) {
        this.name = name;
        this.address = address;
    }
}

그리고 2개의 Human인스턴스를 생성하고 equals()로 비교를 하면 false가 반환됩니다.

final Human human1 = new Human("잠실", "레오");
final Human human2 = new Human("잠실", "레오");

System.out.println(human1.equals(human2)); //false

Object의 eqauls메서드를 보면 아래와 같습니다.

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

이처럼 물리적 동일성을 비교하고 있기 때문에 equals() 메서드를 저희의 의도에 맞게 논리적 동등성을 비교하도록 오버라이딩할 필요가 있습니다. 인텔리제이가 생성해주는 equals를 사용하면 아래와 같은 코드가 나옵니다.

@Override
public boolean equals(final Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    final Human human = (Human) o;
    return Objects.equals(name, human.name) && Objects.equals(address, human.address);
}
  1. 만약 두 인스턴스의 물리적으로 동일하면 true를 반환합니다.
  2. parameter로 들어온 인스턴스가 null이거나, 두 인스턴스의 타입이 다르면 false를 반환합니다.
  3. 타입도 같고, null도 아니라면 Human타입으로 캐스팅합니다.
  4. 이후 논리적 동등성을 비교하길 원하는 인스턴스 변수 각각에 대해 equals로 논리적 동등성을 비교하고 이 결과를 반환합니다.
    • 만약 인스턴스 변수에 별도로 만든 객체가 있다면 해당 객체에도 equals, hashCode오버라이딩이 필요합니다.

이렇게 오버라이딩을 마치고 다시 equals비교를 해보면 true가 반환됩니다.

System.out.println(human1.equals(human2)); //true

hashCode()

hashCode()는 Object클래스에 정의되어 있는 메서드로, 객체가 저장된 주소값을 int로 변환하여 반환합니다. 즉, 이 메서드를 이용해 물리적 동일성을 비교할 수 있습니다.

위에서 생성했던 human1, human2의 hashCode를 출력해 보면 아래와 같습니다.

final Human human1 = new Human("잠실", "레오");
        final Human human2 = new Human("잠실", "레오");

        System.out.println(human1.equals(human2));
        System.out.println(human1.hashCode()); //1579572132
        System.out.println(human2.hashCode()); //359023572

new를 통해 새로운 인스턴스를 생성했기 때문에 서로 다른 주소에 저장되어 있는 걸 볼 수 있습니다.

 

Object명세 중 'equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.'라는 내용이 있습니다.

그러나, equals를 통해 human1과 human2를 비교하면 같다고 나오지만 hashCode는 다르게 나옵니다. hashCode가 다르게 나오는 이유는 equals와 마찬가지로 Object클래스의 hashCode()는 기본적으로 주소를 가지고 hashCode를 만들어내기 때문에 hashCode()도 오버라이딩을 통해 인스턴스 변수의 논리적 동등성을 비교하도록 해야 합니다.

이 역시 인텔리제이에서 생성해 주는 코드를 가져오겠습니다.

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

이제 다시 human1, human2의 hashCode값을 확인하면 동일한 것을 볼 수 있습니다.

System.out.println(human1.hashCode()); //52169753
System.out.println(human2.hashCode()); //52169753

hashCode()를 오버라이딩 안 했을 때 문제점

만약 Human객체들을 관리할 때 이름, 주소 모두 중복되지 않는 객체들만 저장하고 싶어서 HashSet을 사용할 때 문제가 발생합니다.

hashSet의 add과정을 간략하게 아래와 같습니다.

  • 저장하려는 객체의 hash값을 hashCode로 알아낸다.
  • 이미 저장된 객체들의 hash값 중 같은 것이 있는지 살펴본다.
  • hash값이 같다면 equals를 통해 비교한다.
  • equals도 true라면 같은 객체로 판별한다.

이처럼 hash값을 사용하는 컬렉션들은 모두 hashCode를 이용해 hash값을 얻은 뒤, 이를 토대로 기능을 수행합니다. 그래서 만약 hashCode를 오버라이딩 하지 않고 아래와 같은 코드를 실행하면 원하는 결과가 나오지 않습니다.

Set<Human> humanHashSet = new HashSet<>();
humanHashSet.add(human2);
humanHashSet.add(human1);
System.out.println(humanHashSet.size()); // 2
humanHashSet.forEach(a -> System.out.println(a.hashCode()));
// 359023572
// 1579572132

hashCode만 오버라이딩 한 경우

hashCode만 오버라이딩하고 위 코드를 실행하면 아래와 같은 결과가 나옵니다.

Set<Human> humanHashSet = new HashSet<>();
humanHashSet.add(human2);
humanHashSet.add(human1);
System.out.println(humanHashSet.size()); // 2
humanHashSet.forEach(a -> System.out.println(a.hashCode()));
// 52169753
// 52169753

equals를 재정의 하지 않아 add과정 중 마지막에 다른 객체로 판별해 hashSet에 추가되어 hashSet의 size가 2가 됩니다.

정리

만약 객체의 인스턴스 간 필드 값이 같은지에 따라 같은 객체로 봐야 하는 상황이 있다면 equals(), hashCode()를 모두 재정의 해야 합니다. 그리고, 이를 통해 contains이점도 볼 수 있기 때문에 equals()와 hashCode() 모두 재정의 하는 것이 좋다!

Comments