Object 클래스의 equals, hashCode, toString

de_sj_awa·2021년 5월 9일
0

1. equals(Object obj)

equals() 메소드는 매개변수로 객체의 참조변수를 받아서 비교하여 그 값을 boolean 값으로 알려주는 역할을 한다. 아래의 코드는 Object 클래스에서 정의되어 있는 equals() 메소드의 실제 내용이다.

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

위의 코드에서 알 수 있듯이 두 객체의 같고 다름을 참조변수의 값으로 판단한다. 그렇기 때문에 서로 다른 두 객체를 equals() 메소드로 비교하면 항상 false를 결과로 반환한다.

public class EqualsEx1 {
    public static void main(String[] args){
        Value v1 = new Value(10);
        Value v2 = new Value(10);

        if(v1.equals(v2)){
            System.out.println("v1과 v2는 같습니다.");
        }else{
            System.out.println("v1과 v2는 다릅니다.");
        }

        v2 = v1;

        if(v1.equals(v2)){
            System.out.println("v1과 v2는 같습니다.");
        }else{
            System.out.println("v1과 v2는 다릅니다.");
        }
    }
}

class Value{
    int value;

    Value(int value){
        this.value = value;
    }
}

value라는 멤버변수를 갖는 Value 클래스를 정의하고, 두 개의 Value 클래스의 인스턴스를 생성한 다음 equals() 메소드를 이용해서 두 인스턴스를 비교하도록 했다. equlas() 메소드는 주소값으로 비교를 하기 때문에, 두 Value 인스턴스의 멤버변수 value의 값이 10으로 서로 같을지라도 equals() 메소드로 비교한 결과는 false일 수 밖에 없는 것이다.

v1과 v2는 다릅니다.
v1과 v2는 같습니다.

하지만 'v2 = v2;'을 수행한 후에는 참조변수 v2는 v1이 참조하고 있는
인스턴스의 주소값이 저장되므로 v2도 v1과 같은 주소값이 저장된다. 그래서 이번에는 v1.equals(v2)의 결과가 true가 되는 것이다.

Object 클래스로부터 상속받은 equals() 메소드는 결국 두 개의 참조변수가 같은 객체를 참조하고 있는데, 즉 두 참조변수에 저장된 값(주소값)이 같은지를 판단하는 기능밖에 할 수 없다는 것을 알 수 있다. equals() 메소드로 Value 인스턴스가 가지고 있는 value 값을 비교하도록 할 수는 없을까? Value 클래스에서 equals() 메소드를 오버라이딩하여 주소가 아닌 객체에 저장된 내용을 비교하도록 변경하면 된다. 다음의 예를 보자.

class Person{
    long id;

    public boolean equals(Object obj){
        if(obj instanceof Person){
            return id == ((Person)obj).id;  // obj가 Object 타입이므로 id 값을 참조하기 위해서는
                                            // Person 타입으로 형 변환이 필요하다.
        }else{
            return false;   // 타입이 Person이 아니면 값을 비교할 필요도 없다.
        }
    }

    Person(long id){
        this.id = id;
    }
}
public class EqualsEx2 {
    public static void main(String[] args){
        Person p1 = new Person(80118111122L);
        Person p2 = new Person(80118111122L);

        if(p1 == p2){
            System.out.println("p1과 p2는 같은 사람입니다.");
        }else{
            System.out.println("p1과 p2는 다른 사람입니다.");
        }

        if(p1.equals(p2)){
            System.out.println("p1과 p2는 같은 사람입니다.");
        }else{
            System.out.println("p1과 p2는 다른 사람입니다.");
        }
    }
}

equals() 메소드가 Person 인스턴스의 주소값이 아닌 멤버변수 id의 값을 비교하도록 하기위해 equals() 메소드를 다음과 같이 오버라이딩 했다. 이렇게 함으로써 서로 다른 인스턴스일지라도 같은 id(주민등록번호)를 가지고 있다면 equals() 메소드로 비교했을 때 true를 결과로 얻게 할 수 있다.

public boolean equals(Object obj){
    if(obj != null && obj instanceof Person){
        return id == ((Person)obj).id;
    }else{
        return false;
    }
}

String 클래스 역시 Object 클래스의 equals 메서드를 그대로 사용하는 것이 아니라 이처럼 오버라이딩을 통하여 String 인스턴스가 갖는 문자열 값을 비교하도록 되어 있다. 그렇기 때문에 같은 내용의 문자열을 갖는 두 String 인스턴스에 equals() 메소드를 사용하면 항상 true 값을 얻는 것이다.

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String aString = (String)anObject;
            if (!COMPACT_STRINGS || this.coder == aString.coder) {
                return StringLatin1.equals(value, aString.value);
            }
        }
        return false;
    }

2. hashCode()

@IntrinsicCandidate
    public native int hashCode();

이 메서드는 해싱(hashing) 기법에 사용되는 '해시함수(hash function)'를 구현한 것이다. 해싱은 데이터관리기법 중의 하나인데 다량의 데이터를 저장하고 검색하는 데 유용하다. 해시함수는 찾고자하는 값을 입력하면, 그 값이 저장된 위치를 알려주는 해시코드(hashcode)를 반환한다.

일반적으로 해시코드가 같은 두 객체가 존재하는 것이 가능하지만, Object 클래스에 정의된 hashCode 메소드는 객체의 주소값을 이용해서 해시코드를 만들어 반환하기 때문에 서로 다른 두 객체는 결코 같은 해시코드를 가질 수 없다.

앞서 살펴본 것과 같이 클래스의 인스턴스 변수 값으로 객체의 같고 다름을 판단해야 하는 경우라면 equals() 메소드 뿐만 아니라 hashCode() 메소드도 적절히 오버라이딩 해야 한다.

같은 객체라면 hashCode()를 호출했을 때의 결과값이 해시코드도 같아야 하기 때문이다. 만약 hashCode()를 오버라이딩 하지 않는다면 Object 클래스에 정의된 대로 모든 객체가 서로 다른 해시코드 값을 가질 것이다.

equals 메소드에 의해 true가 나오는 두 객체의 hashcode는 같아야 한다. 그래야 hashcode가 의미가 있다. 따라서, 같은 hashCode() 메소드 결과를 갖도록 하려면 hashCode() 메소드도 Object 클래스에서 제공하는 그대로 사용하면 안 된다.

package objectpractice;

public class MemberDTO {
    public String name;
    public String phone;
    public String email;
    public MemberDTO(String name){
        this.name = name;
    }

    public boolean equals(Object obj){

        if (this == obj) return true;
        if (obj == null) return false;

        if (getClass() != obj.getClass()) return false;

        MemberDTO other = (MemberDTO) obj;

        if(name == null){
            if(other.name != null) return false;
        }else if (!name.equals(other.name)){
            return false;
        }

        if(email == null){
            if(other.email != null) return false;
        }else if (!name.equals(other.name)) return false;

        if(phone == null){
            if(other.phone != null) return false;
        }else if (!phone.equals(other.phone)) return false;

        return true;
    }

    public int hashCode(){
        final int prime = 1;
        int result = 1;
        result = prime * result + ((email == null) ? 0 : email.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((phone == null) ? 0 : phone.hashCode());
        return result;
    }
}

String의 hashCode()

public class HashCodeEx1 {
    public static void main(String[] args){
        String str1 = new String("abc");
        String str2 = new String("abc");

        System.out.println(str1.equals(str2));
        System.out.println(str1.hashCode());
        System.out.println(str2.hashCode());
        System.out.println(System.identityHashCode(str1));
        System.out.println(System.identityHashCode(str2));
    }
}

실행 결과

true
96354
96354
295530567
2003749087

String 클래스는 문자열의 내용이 같으면, 동일한 해시코드를 반환하도록 hashCode 메서드가 오버라이딩되어 있기 때문에, 문자열의 내용이 같은 str1과 str2에 대해 hashCode()를 호출하면 항상 동일한 해시코드 값을 얻는다.

반면에 System.identityHashCode(Object x)는 Object 클래스의 hashCode 메소드처럼 객체의 주소값으로 해시코드를 생성하기 때문에 모든 객체에 대해 항상 다른 해시코드값을 반환할 것을 보장한다. 그래서 str1과 str2가 해시코드는 같지만 서로 다른 객체라는 것을 알 수 있다.

    public int hashCode() {
        // The hash or hashIsZero fields are subject to a benign data race,
        // making it crucial to ensure that any observable result of the
        // calculation in this method stays correct under any possible read of
        // these fields. Necessary restrictions to allow this to be correct
        // without explicit memory fences or similar concurrency primitives is
        // that we can ever only write to one of these two fields for a given
        // String instance, and that the computation is idempotent and derived
        // from immutable state
        int h = hash;
        if (h == 0 && !hashIsZero) {
            h = isLatin1() ? StringLatin1.hashCode(value)
                           : StringUTF16.hashCode(value);
            if (h == 0) {
                hashIsZero = true;
            } else {
                hash = h;
            }
        }
        return h;
    }
public static int hashCode(byte[] value) {
        int h = 0;
        for (byte v : value) {
            h = 31 * h + (v & 0xff);
        }
        return h;
    }
    
public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length >> 1;
        for (int i = 0; i < length; i++) {
            h = 31 * h + getChar(value, i);
        }
        return h;
    }

System.identityHashCode(Object x)

@IntrinsicCandidate
    public static native int identityHashCode(Object x);

3. toString()

이 메서드는 인스턴스에 대한 정보를 문자열(String)로 제공할 목적으로 정의한 것이다. 인스턴스의 정보를 제공한다는 것은 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻이다.

Object 클래스에 정의된 toString()은 아래와 같다.

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

클래스를 작성할 때 toString()을 오버라이딩하지 않는다면, 위와 같은 내용이 그대로 사용될 것이다. 즉, toString()을 호출하면 클래스 이름에 16진수의 해시코드를 얻게 될 것이다.

class Card {
    String kind;
    int number;

    Card(){
        this("SPADE", 1);
    }

    Card(String kind, int number){
        this.kind = kind;
        this.number = number;
    }
}
public class CardToString {
    public static void main(String[] args){
        Card c1 = new Card();
        Card c2 = new Card();
        
        System.out.println(c1.toString());
        System.out.println(c2.toString());
    }
}

실행 결과

com.company.Card@776ec8df
com.company.Card@4eec7777

Card 인스턴스를 두 개 생성한 다음, 각 인스턴스에 toString()을 호출한 결과를 출력했다. Card 클래스에서 Object 클래스로부터 상속받은 toString()을 오버라이딩 하지 않았기 때문에 Card 인스턴스에 toString()을 호출하면, Object 클래스의 toString()이 호출된다. 그래서 위의 결과에 클래스이름과 해시코드 결과가 출력되었다. 서로 다른 인스턴스에 대해서 toString()을 호출하였으므로 클래스 이름은 같아도 해시코드값이 다르다는 것을 확인할 수 있다.

public class ToStringTest {
    public static void main(String[] args){
        String str = new String("KOREA");
        java.util.Date today = new java.util.Date();

        System.out.println(str);
        System.out.println(str.toString());
        System.out.println(today);
        System.out.println(today.toString());
    }
}

실행 결과

KOREA
KOREA
Sun May 09 23:39:33 KST 2021
Sun May 09 23:39:33 KST 2021

위의 결과에서 알 수 있듯이 String 클래스와 Date 클래스의 toString()을 호출하였더니 클래스 이름과 해시코드 대신 다른 결과가 출력되었다.

String 클래스의 toString()은 String 인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩되어 있고, Date 클래스의 경우 Date 인스턴스가 갖고 있는 날짜와 시간을 문자열로 변환하여 반환하도록 오버라이딩되어 있다.

이처럼 toString()은 일반적으로 인스턴스나 클래스에 대한 정보 또는 인스턴스 변수들의 값을 문자열로 변환하여 반환하도록 오버라이딩되는 것이 보통이다.

이제 Card 클래스에서도 toString()을 오버라이딩해서 보다 쓸모 있는 정보를 제공할 수 있도록 바꿔보자.

String 클래스의 toString()

public String toString() {
        return this;
    }
class Card{
    String kind;
    int number;

    Card(){
        this("SPADE", 1);   // Card(String kind, int number)를 호출
    }
    Card(String kind, int number){
        this.kind = kind;
        this.number = number;
    }
    public String toString(){
        return "kind : " + kind + ", number : " + number;
    }
}
public class CardToString2 {
    public static void main(String[] args){
        Card c1 = new Card();
        Card c2 = new Card("HEART", 10);
        System.out.println(c1.toString());
        System.out.println(c2.toString());
        System.out.println(c1);
        System.out.println(c2);
    }
}

Card 인스턴스의 toString()을 호출하면 인스턴스가 갖고 있는 인스턴스 변수 kind와 number의 값을 문자열로 변환하여 반환하도록 toString을 오버라이딩했다. 오버라이딩할 때, Object 클래스에 정의된 toString()의 접근제어자가 public이므로 Card클래스의 toString()의 접근제어자도 public으로 했다는 것을 눈여겨 보자.

조상에 정의된 메서드를 자손에서 오버라이딩할 때는 조상에 정의된 접근범위보다 같거나 더 넓어야 하기 때문이다. Object 클래스에서 toString()의 접근제어자가 public이므로, 이를 오버라이딩하는 Card 클래스에서는 toString()의 접근 제어자를 public으로 할 수 밖에 없다.

참고

profile
이것저것 관심많은 개발자.

0개의 댓글