equals 와 hashCode(동일성 vs 동등성) 그리고 lombok

Jeonghwa·2023년 1월 18일
0

equals와 hashCode

equalshashCode 둘다 Object클래스에 정의 되어있기에, java의 모든 객체는 두 메서드를 상속받고 있다.

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

equals

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

Object에 구현된 equals 메서드는 객체가 동일한지 검사하기 위해 사용된다.
코드를 보면 == 비교를 통해 객체의 메모리 주소값이 같은지 식별한다. 이를 동일성(Identity) 비교라고 한다.

하지만 프로그래밍을 하다보면 서로 다른 메모리에 띄워져 있을 지라도 같은 값을 지니기 때문에 같은 객체로 인식이 되어야할 때가 있다. 이러한 경우 equals 를 오버라이딩해주고 논리적으로 같은 지위를 지녔는지 확인한다. 이를 동등성(Equality) 비교라고 한다.

예를 들어 같은 값을 갖는 문자열 2개를 생성하면 서로 다른 메모리에 할당되므로 동일하지 않다. 하지만 String 클래스에서 equals 메서드를 오버라이딩하여 객체가 같은 값을 갖는지 비교하도록 처리해놓았기 때문에 이는 논리적으로 동등하다.

String s1 = new String("hi");
String s2 = new String("hi");
System.out.println(s1==s2); // false
System.out.println(s1.equals(s2)); // true
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;
}

hashCode

public native int hashCode();

hashCode는 실행 중(Runtime)객체의 유일한 integer값을 반환한다. Object클래스에서는 heap에 저장된 객체의 메모리 주소를 이용해 해시코드를 만들어 반환하도록 되어있다. (반드시는 아님)

native 키워드 : C, C++ 그리고 어셈블리 같은 다른 언어들로 작성된 라이브러리들을 JNI(Java Native Interface)를 통해 Java에서 이용하고자 할 때 사용되는 키워드이다.
출처 : 위키백과 JNI

이는 HashTable, HashMap, HashSet 등 Hash를 이용하는 자료구조의 데이터 저장 위치를 결정하는데 사용되기 때문에, equals메서드를 오버라이딩했다면 hashCode도 반드시 오버라이딩 해줘야한다.

equals와 hashCode의 오버라이딩 규칙

Java API 문서에서는 equalshashCode를 오버라이딩할 때 다음 조건을 따라야 한다고 명시하고 있다.

  • Java 응용 프로그램을 실행하는 동안 equals메서드에 사용된 정보가 수정되지 않았다면, 같은 객체의 hashCode는 항상 동일한 정수값을 반환해야한다.
    (이 정수를 동일한 응용 프로그램의 다른 실행까지 유지해야할 필요는 없다.)
  • 두 객체가 equals 메서드에 따라 동등하다면 두 객체의 hashCode값도 일치해야 한다
  • 두 객체가 equals 메서드에 따라 동등하지 않다면 두 객체의 hashCode값은 일치하지 않아도 된다.

하지만 만약 다른 객체에 대해 동일한 hashCode를 생성한다면 HashTable을 생성하는데 불이익을 받을 수 있음을 인지해야한다.
출처 : 오라클 문서 hashCode

equals만 오버라이딩 할 경우

equals만 오버라이딩 할 경우 논리적으로 동등한 객체일지라도 hashCode의 값이 같지 않기 때문에 HashSet에서는 동등하지 않은 객체로 여기고 중복처리하지 않는다.

예시

public class Test {
    public static void main(String[] args) {
        HashSet<Tv> tvSet = new HashSet<>();
        tvSet.add(new Tv("Samsung"));
        tvSet.add(new Tv("Samsung"));
        System.out.println(tvSet.size());
    }

    public static class Tv{
        String name;

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

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

결과
결과를 보면 동일한 이름의 Tv를 넣었지만 HashSet의 크기는 1이 아닌 2가 된다.

> Task :Test.main()
2

hashCode도 함께 오버라이딩할 경우

Hash관련 자료구조들은 아래와 같은 과정을 거쳐 객체의 논리적 동등성을 비교한다.

Object의 hashCode를 사용하는 Tv클래스는 equals결과가 true일 지라도 hashCode서로 다른 메모리의 주소를 이용한 해시코드를 반환하기 때문에 다른 객체라 판단하는 것이다.
따라서 equals의 기준인 이름이 같을 경우 동일한 int값을 반환하도록 오버라이딩을 해주자.
예시

public class Test {
    public static void main(String[] args) {
        HashSet<Tv> tvSet = new HashSet<>();
        tvSet.add(new Tv("Samsung"));
        tvSet.add(new Tv("Samsung"));
        System.out.println(tvSet.size());
    }

    public static class Tv{
        String name;

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

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

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

결과

> Task :Test.main()
1

참고 : Tecoble - equals와 hashCode는 왜 같이 재정의해야 할까?


롬복 @EqualsAndHashCode

개발을 하다 보면 방금과 같이 일일이 정의해 주기 귀찮을 것이다.
그럴 때 @EqualsAndHashCode 롬복을 사용해주면 되는데 코드 생성 없이도 동일한 효과를 얻을 수 있다.

참고로 롬복을 사용하려면 프로젝트에 의존성 추가는 필수이다.
build.gradle

dependencies {
	...
    implementation 'org.projectlombok:lombok:1.18.18'
    ...
}

예시
일부러 int code 멤버 변수는 equals의 조건에서 제외시켰다.

public class Test {
    public static void main(String[] args) {
        HashSet<Tv> tvSet = new HashSet<>();
        tvSet.add(new Tv("Samsung", 1));
        tvSet.add(new Tv("Samsung", 2));
        System.out.println(tvSet.size());
    }

    @EqualsAndHashCode(exclude = "code")
    public static class Tv{
        String name;
        int code;

        public Tv(String name, int code) {
            this.name = name;
            this.code = code;
        }
    }
}

결과

> Task :Test.main()
1

참고 : projectlombok - EqualsAndHashCode

profile
backend-developer🔥

0개의 댓글