Java

자바 Object 클래스의 equals, hashCode, toString 메서드

마닐라 2022. 3. 16. 17:35

Object 클래스란?

Object 클래스는 java.lang 패키지에 속해 있는 모든 자바 클래스의 최고 조상 클래스입니다.

java.lang 패키지란 자바에서 가장 기본적인 동작을 수행하는 클래스들의 집합입니다.

따라서 자바에서는 java.lang의 패키지의 클래스들은 import 문 없이 클래스 이름만으로 바로 사용할 수 있도록 합니다.

 

Object 클래스는 필드가 없으며 총 11개의 메서드로 이루어져 있습니다.

그 중 equals, hashCode, toString 메서드에 대해 알아보려합니다.

 

equals 메서드

Object의 equals() 메서드는 아래와 같이 기본적으로 해당 인스턴스를 매개변수로 전달받는 참조 변수와 비교하여 그 결과를 반환합니다.

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

 

'==' 연산자를 사용하기 때문에 해당 메서드는 서로의 주솟값이 같아야 같은 인스턴스로 판단합니다.

equals 메서드를 사용하려면 재정의를 해야한다는 말을 많이 듣습니다.

재정의를 해야하는 이유에 대해 알아보겠습니다.

 

먼저 문자열 연산의 경우는 어떻게 되는지 생각해보겠습니다.

public static void main(String[] args) throws IOException {
    String s1 = "스트링";
    String s2 = "스트링";
    String s3 = new String("스트링");
    String s4 = new String("스트링");
    System.out.println(s1 == s2);       // true
    System.out.println(s1 == s3);       // false
    System.out.println(s3 == s4);       // false
    System.out.println(s1.equals(s2));  // true
    System.out.println(s1.equals(s3));  // true
    System.out.println(s3.equals(s4));  // true
}

 

먼저 '==' 연산자에 대해서 알아보겠습니다.

'==' 연산자는 서로의 주솟값이 같아야 true를 반환합니다.

3개의 연산 결과값이 왜 저렇게 나오는지 확인하려면 JVM 메모리 구조에 대한 학습이 필요합니다.

s1 ~ s4 모두 String 인스턴스를 생성한다는 공통점이 있습니다.

생성된 인스턴스는 JVM 메모리의 heap 영역이라는 곳에 저장이 됩니다.

new 키워드로 생성하지 않은 String은 heap 영역의 String Constant Pool에 따로 저장이 됩니다.

String Constant Pool은 동일한 리터럴 값을 갖는 인스턴스에 대해 재사용할 수 있는 공간이라고 생각하면 좋을 것 같습니다.

 

s1 ~ s4가 heap 영역에 저장되는 과정을 알아보겠습니다.

1. s1의 리터럴 값에 대해서 heap 영역의 String Constant Pool에 주솟값이 새롭게 저장됩니다.

2. s2의 리터럴 값은 heap 영역의 String Constant Pool에 이미 저장된 리터럴이므로 해당 주솟값을 참조합니다.

따라서 여기까지 진행된다면 총 1개의 String 객체가 생성된 것을 확인할 수 있습니다.

3. s3의 리터럴 값은 heap 영역의 String Constant Pool 바깥에 주솟값이 새롭게 저장됩니다.

4. s4의 리터럴 값은 heap 영역의 String Constant Pool 바깥에 주솟값이 새롭게 저장됩니다.

 

이렇게 해서 JVM의 heap 메모리 영역에 총 3개의 String 인스턴스가 생성되었습니다.

따라서 true, false, false의 결괏값이 나오게 됩니다.

 

그러면 본론으로 돌아와서 Object클래스의 equals 메서드도 서로의 주솟값을 통해 같은 인스턴스를 판단한다고 했는데 어떻게 모두 true라는 결괏값이 나오게 되었을까요?

 

그것은 String 클래스에서 equals 메서드를 재정의하였기 때문입니다.

아래의 코드는 String 클래스에서 재정의한 equals 메서드의 내용입니다.

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String aString = (String)anObject;
        if (coder() == aString.coder()) {
            return isLatin1() ? StringLatin1.equals(value, aString.value)
                              : StringUTF16.equals(value, aString.value);
        }
    }
    return false;
}

 

여기서 신경쓸 것은 첫번째 if문은 Object 클래스와 같은 내용이지만 두번째 if문에서 추가적인 처리를 해준다는 것을 볼 수 있습니다.

세부 내용은 더 복잡하지만 두번째 if문의 내용을 글로만 간단하게 표현하면 '같은 문자열일 때는 true를 반환한다.' 는 것입니다.

이로써 저희는 String 클래스를 사용하여 문자열 비교를 할 때 간편하게 재정의된 equals 메서드를 사용하여 비교할 수 있는 것입니다.

 

우리는 프로그래밍을 하다 보면 클래스를 만들기도 하고 만든 클래스에 대한 비교가 필요할 때가 있습니다.

아래와 같이 사람 클래스를 만들어서 비교를 진행해보겠습니다.

public class Person {
    private int id;
    private String name;

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

    public void setId(int id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }
}

 

public static void main(String[] args) {
    Person p1 = new Person();
    p1.setId(1);
    p1.setName("홍길동");

    Person p2 = new Person();
    p2.setId(1);
    p2.setName("홍길동");

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

 

만들고나서 동일한 id와 name을 갖는 사람 인스턴스를 2개 만들었습니다.

2개의 객체는 동일한 값들을 가지지만 독립된 인스턴스들이므로 '==', equals() 모두 false가 반환됩니다.

하지만 String처럼 해당 인스턴스들은 동일한 객체로 취급하고 싶을 때가 있을 겁니다.

이 때 equals 메서드를 재정의 해주면 됩니다.

 

인텔리제이를 사용해서 equals 메서드를 자동으로 재정의한 내용은 다음과 같습니다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return id == person.id && Objects.equals(name, person.name);
}

첫번째 if문은 자기 자신일때는 true를 바로 반환

두번째 if문은 매개 변수로 전달받는 참조 변수가 null 이거나 클래스 타입이 다르다면 false를 바로 반환

여기까지 왔으면 각 객체들의 value를 비교하여 둘 다 같으면 true를 반환합니다.

 

하지만 여기서 끝나는 것이 아닙니다.

equals 메서드를 재정의한 클래스는 모두 hashCode를 재정의 해야합니다.

그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으키게 됩니다.

 

hashCode 메서드

hashCode란 해시 알고리즘에 의해 생성된 정수 값입니다.

Object 클래스의 hashCode 메서드는 해시 코드값이 다르면 다른 인스턴스로 판단하고 해시 코드값이 같으면 equals()로 다시 비교합니다.

그렇게 때문에 해시 코드가 다르면 값에 대한 비교도 이루어지지 않고 다른 인스턴스로 판단해버립니다.

 

이에 대한 예를 들어보겠습니다.

 

public static void main(String[] args) {
    Map<Person, Integer> map = new HashMap<>();
    map.put(new Person(1, "홍길동"), 23);
    System.out.println(map.get(new Person(1, "홍길동"))); // null
}

 

출력문에 23이라는 값이 나와야 할 것 같지만 null을 반환합니다.

여기에는 HashMap에 넣을 때와 조회할 때 2개의 인스턴스가 사용되었습니다.

이러한 문제가 발생한 이유는 두 인스턴스가 서로 다른 해시코드를 반환하였기 때문입니다.

사용하면 안되지만 해결하기 제일 쉬운 방법은 아래와 같이 구현하는 것입니다.

 

@Override
public int hashCode() {
    return 42;
}

이렇게 사용하면 모든 객체에서 똑같은 해시코드를 반환하게 됩니다.

따라서 위와 같이 재정의만 해주더라도 출력문에서 23이 정상적으로 출력됩니다.

 

하지만 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환해야합니다.

전형적인 hashCode 메서드는 아래와 같습니다. 단순하고 충분히 빠른 방식입니다.

 

@Override
public int hashCode() {
    int result = Integer.hashCode(id);
    result = 31 * result + name.hashCode();
    return result;
}

 

하지만 위와 같은 방식은 이미 구현되어있는 hash 메서드를 사용하는 것 보다 해시 충돌이 일어날 가능성이 높으며 해시 충돌이 적은 방법을 꼭 써야한다면 인텔리제이에서 자동 작성해주는 Objects 클래스의 hash 메서드를 사용하면 됩니다.

 

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

 

Objects 클래스의 hash 메서드가 호출하는 hashCode 메서드를 보면 약간은 비슷한 형태로 해시코드를 만들어 리턴하는 것을 볼 수가 있습니다.

public static int hashCode(long a[]) {
    if (a == null)
        return 0;

    int result = 1;
    for (long element : a) {
        int elementHash = (int)(element ^ (element >>> 32));
        result = 31 * result + elementHash;
    }

    return result;
}

 

서로 다른 인스턴스라면 되도록 해시코드도 서로 다르게 구현해야 하는데, AutoValue라는 프레임워크를 사용하면 멋진 equals와 hashCode를 자동으로 만들어준다고 합니다. 해당 프레임워크는 추후에 알아보도록 하겠습니다.

 

toString 메서드

Object의 toString 메서드는 해당 인스턴스에 대한 정보를 문자열로 반환합니다.

클래스 이름@16진수로 표시한 해시 코드 의 형식을 띄고 있습니다.

toString의 일반 규약에는 '간결하면서 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 한다고 나와있습니다.

Person@abddb는 간결하다고 볼 수 있지만 Person{id=1, name = 홍길동} 처럼 직접적인 정보를 알려주는 형태가 훨씬 유익한 정보라 볼 수 있습니다. 또한 toString의 규약은 "모든 하위 클래스에서 이 메서드를 재정의하라"고 합니다.

따라서 그냥 무조건 재정의 하면 될 것 같습니다.

 

 

출처

http://www.yes24.com/Product/Goods/65551284

https://devlog-wjdrbs96.tistory.com/243

https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/

https://velog.io/@vgo_dongv/Java-equals%EC%99%80-hashCod-%EC%9E%AC%EC%A0%95%EC%9D%98

http://www.tcpschool.com/java/java_api_object