Java

Java의 equals()와 hashCode()

무무11 2025. 1. 3. 22:21

나는 코틀린을 먼저 익히고 자바로 넘어온 독특한 경우일 것이다. 코틀린을 배울 때 data class를 활용하면 좋은 것이 equlas()와 hashCode()같은 함수들을 자동으로 오버라이드해 만들어주기 때문에 편리하다라는 내용을 분명 보고 넘어갔지만, 정확히 어떤 내용인지는 자세히 알지 못한채로 넘어갈 수 밖에 없었다. 자바를 배우면서 이 부분들은 모두 해소되었는데 그 내용을 여기 정리해보았다.

 

1. Object 클래스

먼저 equals()와 hashCode()에 대해 이야기하려면 'Object' 클래스부터 이야기해야한다. 자바의 모든 클래스의 최상위 부모 클래스는 바로 'Object' 클래스이다.

 

모든 클래스가 'Object' 클래스를 상속받는 이유는 공통 기능 제공, 다형성의 기본 구현 때문이다.

 

먼저 공통 기능 제공에 대해 살펴보면 다음과 같다. 객체의 정보를 제공하고, 이 객체가 다른 객체와 같은지 비교하고, 객체가 어떤 클래스로 만들어졌는지 확인하는 기능은 모든 객체에게 필요한 기본 기능이다. 이런 기능을 객체를 만들 때 마다 항상 새로운 메서드를 정의해서 만들면 번거롭고 어려울 것이다. 그리고 만든다고 해도 개발자마다 서로 다른 이름의 메서드를 만들어 일관성이 떨어질 수도 있다.

 

따라서 'Object' 클래스는 모든 객체에 필요한 공통 기능을 제공한다. 그리고 최상위 부모 클래스이기 때문에 모든 객체는 공통 기능을 편리하게 상속 받을 수 있다.

 

객체의 문자 정보를 제공하는 'toString()', 객체의 동등함을 비교하는 'equals()', 객체의 클래스 정보를 제공하는 'getClass()' 등 기타 여러가지 기능들(객체를 식별할 수 있는 정수값을 제공하는 'hashCode()')을 제공한다.

 

다형성의 기본 구현에 대해 살펴보면 다음과 같다. 부모 클래스 타입 변수는 자식 클래스 인스턴스를 담을 수 있다. 'Object'는 모든 클래스의 부모 클래스이기 때문에 모든 객체를 참조할 수 있다. 'Object' 클래스는 다형성을 지원하는 기본적인 메커니즘을 제공한다. 모든 자바 객체는 'Object' 타입으로 처리될 수 있고, 이는 다양한 타입의 객체를 통합적으로 처리할 수 있게 해준다. 다시 말해, 'Object'는 모든 객체를 다 담을 수 있다.

 

* 참고

equals()와 hashCode()에 대해 주로 다루는 글이기 때문에 여기서 toString()에 대해서 짧게 정리해두려고한다.

 

'Object' 클래스의 toString() 메서드는 아래와 같다.

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

기본적으로 패키지를 포함한 객체의 이름과 객체의 참조값(해시코드)를 16진수로 제공한다.

 

다른 형태로 객체의 문자 정보를 출력하고 싶다면 직접 toString() 메서드를 오버라이드해서 원하는 형태로 출력되도록 코드를 작성하면된다.

 

2. 동일성과 동등성 그리고 equals()

두 객체가 같다라는 표현은 2가지로 나눌 수 있다.

 

- 동일성(Identity) : '==' 연산자를 사용해서 두 객체의 참조가 동일한 객체를 가리키고 있는지 확인한다

- 동등성(Equality) : equals() 메서드를 사용하여 두 객체가 논리적으로 동등한지 확인한다

 

동일성은 물리적으로 같은 메모리에 있는 객체 인스턴스인지 참조값을 확인하는 것이고, 동등성은 논리적으로 같은지 확인하는 것이다.

 

먼저 Object 클래스가 제공하는 equals() 메서드가 어떤지 확인해보자

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

기본으로 제공하는 equals()는 '=='으로 동일성 비교를 제공한다.

 

따라서 클래스를 새로 만들고 equals() 메서드를 오버라이드해서 새로 정의하지 않으면, 똑같은 변수로 초기화를 한 상태라도 equals() 메서드로 동등성을 비교해도 실제로는 동일성 비교를 하기 때문에 false가 반환될 것이다. 따라서 equals() 메서드를 이용해 동등성 비교를 하기 위해 오버라이드를 해야한다.

 

하지만 실제로 정확한 equals() 메서드를 구현하는 것은 쉽지 않다. 몇가지 지켜야할 규칙들이 있다.

 

- 반사성(Reflexivity) : 객체는 자기 자신과 동등해야 한다. (a.equals(a)는 항상 true)

- 대칭성(Symmetry) : 두 객체가 서로에 대해 동등하다고 판단하면, 이는 양방향으로 동등해야 한다. (a.equals(b)가 true이면, b.equals(a)도 true)

- 추이성(Transitivity) : 만약 한 객체가 두 번째 객체와 동등하고, 두 번째 객체가 세 번째 객체와 동등하다면, 첫 번째 객체는 세 번째 객체와도 동등해야 한다.

- 일관성(Consistency) : 두 객체의 상태가 변경되지 않는 한, equals() 메서드는 항상 동일한 값을 반환해야 한다.

- null에 대한 비교 : 모든 객체는 null과 비교했을 때, false를 반환해야 한다.

 

위와 같은 모든 규칙을 지켜서 직접 equals() 메서드를 구현하는 것은 쉽지 않기 때문에 보통 IDE에서 만들어주는 equals() 메서드를 사용한다. 아래는 Intellij에서 생성해주는 equals() 메서드이다. Student 클래스는 id 필드 하나만 가지는 클래스이다.

@Override
public boolean equals(Object o) {
	if (this == o) return true;
	if (o == null || getClass() != o.getClass()) return false;
	Student student = (Student) o;
	return Objects.equals(id, user.id);
}

 

3. hashCode()

해시 인덱스를 사용하는 해시 자료구조는 데이터 추가, 검색, 삭제의 성능이 매우 빠르기 때문에 많이 사용된다.(HashMap, HashSet 등) 그런데 해시 자료 구조를 사용하려면 정수로 된 숫자 값인 해시 코드가 필요하다.

 

직접 정의한 클래스의 타입을 해시 자료 구조에 저장하려면 객체가 숫자 해시 코드를 제공할 수 있어야 한다.

 

자바는 모든 객체가 자신만의 해시 코드를 표현할 수 있는'Object'  있는 hashCode() 메서드를 제공한다.

public class Object {
	public int hashCode();
}

보통은 이 메서드를 그대로 사용하기 보다는 오버라이딩해서 사용한다. Object의 이 메서드는 객체의 참조값을 기반으로 해시 코드를 생성한다. 객체의 인스턴스가 다르면 해시 코드도 다르다.

 

만약 Person이라는 클래스를 만들었고 필드는 age 밖에 없어서 age가 같으면 같은 Person이라고 할 수 있다고 하자. 이런 경우라면 age를 기반으로 hashCode()를 재정의 해야한다. Objects.hashCode()를 이용해서 쉽게 해시 코드를 생성할 수 있다. (IDE의 기능을 활용하면 쉽게 생성할 수 있다.)

import java.util.Objects;

public class Person {

    public int age;

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(age);
    }
}

해시 자료 구조를 사용하려면 hashCode()도 중요하지만, 해시 인덱스가 충돌할 경우도 대비해 equals()도 반드시 재정의해야한다. 해시 충돌이 발생한 경우 인덱스에 있는 데이터들을 하나하나 비교해야하기 하는데 이 때 equals()를 이용하기 때문에 반드시 재정의해야한다.

 

이 내용을 자세히 이해하려면 해시와 해시충돌에 대해 알고있어야 하는데 공부해보면 위 내용을 어렵지 않게 이해할 수 있을 것이다.