TIL

  1. equals()
  2. hashCode()

 


 

equals()

/**
* Object 클래스에서의 equals()
*/
public boolean equals(Object obj) {
    return (this == obj); // 객체 주소값 비교
}

Object 클래스의 equals() 메소드는 객체 자신(this)의 주소값과 매개변수 객체의 주소값을 비교해서 같으면 true, 다르면 false를 반환한다.

 

 

/**
* String 클래스에서 오버라이딩한 equals()
*/
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

String 클래스에서 오버라이딩한 equals() 메소드는 문자값 그 자체를 비교한다.

 

 

/**
* 사용자 정의 클래스에서 오버라이딩한 equals()
*/
public class Person {
    String name;

    // Person 객체의 name 필드가 동등한지 비교하기위해 오버라이딩
    public boolean equals(Object o) {
        // this와 매개변수 객체 주소값이 같을 경우 true
        if (this == o) return true;

        // 매개변수 객체가 Person 타입과 호환되지 않으면 false
        if (!(o instanceof Person)) return false;

        // 매개변수 객체를 다운캐스팅 (name 변수가 Object에는 없고 Person에는 있으니까) 
        Person person = (Person) o;

        // this의 name 필드와 매개변수 객체의 name 필드를 비교
        return Objects.equals(this.name, person.name);
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        System.out.println(p1.equals(p2)); // true
    }
}

그렇다면 내가 만든 Person 클래스로 객체를 생성하고 그 객체들을 비교할 땐, 어떻게 해야할까?
컴퓨터 입장에서 보면 p1, p2는 heap 영역에 전혀 다른 주소값을 갖고 있는, 서로 다른 객체라고 보겠지만
현실 세계 관점에서 보면 두 객체는 name 속성이 같은 데이터라고 볼 수도 있다.
이렇게 객체의 인스턴스 변수값으로 객체를 비교 하고싶다면 equals() 메소드를 오버라이딩하면 된다.

 

그런데 한 가지 유념해야할 것이 있다.

 

equals 메소드를 오버라이딩할 때는 hashCode() 메소드도 함께 오버라이딩 해야한다. 위 코드처럼 equals() 메소드를 오버라이딩 해서 name 필드가 '홍길동'인 객체는 서로 같다고 판단할 수 있겠지만 서로 가리키고 있는 객체의 주소값은 완전 다르다. equals() 결과가 true임에도 불구하고 hashCode()의 값은 서로 다른 것이다. 이렇게 되면 Java Collection Framework 사용 시, 문제가 발생한다.

 

따라서 equals() 메소드의 결과가 true인 두 객체는 해시코드도 반드시 같아야 한다.

 

 

 

hashCode()

public native int hashCode();

hashCode() 메소드는 객체에 대한 해시코드 값을 리턴한다. 해시코드란 객체의 주소값으로 만든 고유한 숫자값을 말한다. 해싱 알고리즘을 통해 객체의 주소를 int로 변환해서 반환한다.
위 코드에서 선언 된 native라는 키워드는 native method라는 뜻이다. 즉, OS가 가지고 있는 메소드라는 뜻이다. 주로 C언어로 작성된 경우가 많다. 코드를 보면 메소드 구현부가 없는데 이미 작성 된 메소드를 호출하는 것이라 내용이 없는 것이다. 또 우리는 그 안에 작성된 코드를 볼 수 없다.

 

 

class Person {
    String name;

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

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        // 객체마다 서로 다른 해시코드를 가지고 있다.
        System.out.println(p1.hashCode()); // 622488023
        System.out.println(p2.hashCode()); // 1933863327
    }
}

해시코드는 객체의 주소를 가지고 만들기 때문에 객체마다 서로 다른 값을 갖는다. 그래서 해시코드를 객체의 지문이라고도 표현한다. 앞에서 equals() 메소드를 오버라이딩 하면 hashCode() 메소드도 오버라이딩 해야한다고 했다.

 

그 이유를 더 자세히 알아보면,

 

 

public class Test {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        // 해시코드 서로 다름
        System.out.println(p1.hashCode()); // 460141958
        System.out.println(p2.hashCode()); // 1163157884

        // equals() 메소드를 오버라이딩 했다고 가정함
        // equas() 값은 true이고, 해시코드 값은 서로 다름
        System.out.println(p1.equals(p2)); // true

        Set<Person> people = new HashSet<>();
        people.add(p1);
        people.add(p2);

        System.out.println(cars.size()); // 2
    }
}

p1.equals(p2) 결과가 true이기 때문에 중복 데이터를 허용하지않는 Set에 두 객체를 넣었을 때, 한 개의 데이터만 저장될 것이라고 예상했다. 그런데 peoplesize() 결과값이 2이다. 이는 두 객체를 equals() 메소드를 통해 논리적으로 같다고 정의했지만 해시코드가 다르기 때문에 발생한 문제이다.

 

 

 

hashCode()와 equals() 동작순서

hash 값을 사용하는 Collection(HashMap, HashSet, HashTable)은 객체가 논리적으로 같은지 비교할 때, 이와 같은 과정을 거친다.

 

  1. 컬렉션에 데이터가 추가되면 해당 데이터의 hashCode() 리턴값을 해당 컬렉션에서 가지고 있는지 찾아본다.
  2. 해시코드가 같으면 다음으로 equals() 리턴값을 비교한다.
  3. equals()가 true를 리턴하면 논리적으로 서로 같은 객체라고 판단한다.

 

위에 있는 코드와 비교해보면 p1객체와 p2객체는 hashCode() 리턴값이 다르기 때문에 첫 단계에서부터 서로 다른 객체라고 판단해버린다.

따라서 이런 오류를 막기위해 hashCode() 메소드도 오버라이딩해서 서로 같은 해시코드를 갖도록 만들어야 한다.

 

 

class Person {
    String name;

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

    /**
    * 해시코드 재정의
    */
    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
    
    @Override
    public boolean equals() {
    	// 오버라이딩 했다고 가정
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동");

        // 해시코드가 서로 같다.
        System.out.println(p1.hashCode()); // 54150093
        System.out.println(p2.hashCode()); // 54150093

        System.out.println(p1.equals(p2)); // true

        Set<Person> people = new HashSet<>();
        people.add(p1);
        people.add(p2);

        System.out.println(cars.size()); // 1
    }
}

Person 클래스의 name 필드를 가지고 해시코드를 생성하도록 hashCode() 메소드를 오버라이딩 한다. 이제 두 객체의 해시코드가 같은 것을 볼 수 있다. Set 컬렉션도 p1, p2 객체를 중복된 데이터로 판단해서 한 개의 데이터만 저장한다.

 

 

 

📚 참고
자바의 신
자바 equals / hashCode 오버라이딩 - 완벽 이해하기
[자바의 정석 - 기초편] ch9-1~3 Object클래스와 equals()
[자바의 정석 - 기초편] ch9-4~6 hashCode(), toString()