백엔드 개발자라면 대답해야 할 100가지 질문

4. equals()와 hashcode()를 같이 재정의해야 하는 이유

ignuy 2023. 7. 20.

객체의 동등성과 동일성에 대해서 정리할 필요가 있다.

동일성 : 같은 주소값을 가짐
동등성 : 객체 내부에서 값을 비교 (hashCode와 equals를 재정의하여 비교)

우리가 == 과 equals()의 차이점을 String Type의 변수에서 확인했듯이 ==은 동일성을 판단하는 연산자였고 equals()는 동등성을 판단하는 메서드였다.

equals()

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

위 equals 메서드는 Object 클래스 내부에서 선언된 equals이다. this와 파라미터로 받은 obj의 주소값을 비교하고 그 결과 논리값을 반환한다. 보통 새로운 클래스를 만들게 되면 이 equals()를 재정의하여 객체 사이의 동등성을 비교할 수 있도록 새로운 형태의 equals() 로직을 만들어야 한다.

보통은 아래와 같은 순서를 지키도록 재정의하고 조건들을 지나가며 논리값을 반환한다.

순서 1. 두 객체가 동일한 참조 값을 가지는지 확인합니다. 둘 다 동일한 참조 값을 가지는 경우 데이터도 동일하므로 true를 반환하도록 합니다.
순서 2. 매개변수로 전달받은 객체가 null이거나 두 객체가 동일한 타입이 아니라면, false를 반환하도록 합니다.
순서 3. 매개변수로 전달받은 매개변수를 현재 클래스 타입으로 변환합니다.
순서 4. 모든 필드의 값을 비교합니다. 기본 타입이라면 == 연산자를 사용하고 참조 타입인 경우 Objects 클래스의 equals() 메서드를 사용할 수 있습니다.

class Lecture {
	String name;
	String professor;
	
	Lecture (String name, String professor) {
		this.name =name;
		this.professor= professor;
	}
	
	@Override
	public boolean equals(Object obj) {
	    if (this == obj)
	        return true;
	
	    if (obj == null || getClass() != obj.getClass())
	        return false;
	
	    Lecture other = (Lecture) obj;
	    return name.equals(other.name) && professor.equals(other.professor);
	}
}

public class Main {

	public static void main(String[] args) throws Exception {
		
		Lecture l1 = new Lecture("자료구조", "박현성");
		Lecture l2 = new Lecture("자료구조", "박현성");
		
    System.out.println("l1.equals(l2) = " + l1.equals(l2));
    System.out.println("l1.hashCode() = " + l1.hashCode());
    System.out.println("l2.hashCode() = " + l2.hashCode());
	}
}
/**
* l1.equals(l2) = true
* l1.hashCode() = 705927765
* l2.hashCode() = 366712642
*/

이제 동등성을 비교하기 위해 값이 같은 객체는 eqauls()가 true를 반환하도록 해주었지만 문제가 하나 더 남아있다. 바로 hashCode이다.

hashCode()

public native int hashCode();

위는 Object 클래스에 정의되어 있는 hashCode 메서드이다. native 키워드는 보통 C나 C++과 같이 자바가 아닌 언어로 구현한 후 자바에서 사용하려고 할 때 이용하는 키워드니 참고 바란다.

위 메서드는 객체가 가지는 객체를 식별하는 하나의 고유 정수값인 hashcode를 반환한다. Object의 hashCode() 메소드는 객체의 메모리 번지를 이용해서 hashcode를 만들어 리턴하기 때문에 객체마다 고유의 다른 값을 가져야 한다.

/**
* l1.equals(l2) = true
* l1.hashCode() = 705927765
* l2.hashCode() = 366712642
*/

따라서 같은 값을 가지는 객체가 동일성을 지니게 만들고 싶다면 두 객체는 같은 hashcode를 가질 수 있도록 hashCode() 메서드를 재정의 해주어야 한다.

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

/**
* l1.equals(l2) = true
* l1.hashCode() = 1389809412
* l2.hashCode() = 1389809412
*/

여기까지 보통 클래스를 새로 만들게 될 때 equals()와 hashCode()를 꼭 재정의해서 사용하는 과정을 보여주었다.

equals()와 hashCode()를 같이 재정의해야 하는 이유

equals()만 재정의하지 않으면 hashcode()가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수 있지만 객체가 자신과 같은 값을 가지고 있는지 확인할 수 없기 때문에 원하는 객체를 찾을 수 없다.

hashCode()만 재정의하지 않으면 같은 값이라도 hashcode값이 달라지게 된다. hash 값을 사용하는 Collection(HashMap, HashSet, HashTable)은 객체가 논리적으로 같은지 비교할 때 equals()뿐 아니라 hashCode()를 이용하여 동등비교를 하므로 필수적으로 동등성을 확인하기 위한 재정의가 필요하다..

이러한 이유로 객체의 정확한 동등, 동일 비교를 위해서는 Object의 equals()와 hashCode()는 반드시 재정의 후 사용해야 한다.

+ 추가) 다른 객체임에도 hashCode()가 같을 수 있다.

두 객체를 equals() 메서드로 비교하여 true가 나온다면 hashCode()는 일반적으로 true를 반환한다. 하지만, HashTable이나 HashSet, HashMap 등 hash를 사용하는 라이브러리 인터페이스를 사용하는 환경에서 다른 객체가 동일한 hash값을 가지는 hash 충돌이 일어난다면 다른 객체 임에도 hashCode() 값이 같을 수도 있게 된다.

댓글