equals와 hashCode는 모든 객체의 부모 객체인 Object클래스에 정의되어 있습니다.
클래스를 전언할 때 extends 키워드로 다른 클래스를 상속하지 않으면 암시적으로 Object 클래스를 상속합니다.
때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있습니다.
equals 메소드는 어떤 2개의 객체가 동일한지 검사하기 위해 사용됩니다.
대표적으로 String 타입의 변수를 비교할때 가장 많이 사용되는 메소드일 것입니다.
아래 코드를 보시면 문자열의 equals() 메소드는 어떻게 다뤄지는지 확인해보겠습니다.
String 클래스에서 equals 메소드를 오버라이드하여 객체가 같은 값을 갖는지 동등성(Equality)을 비교하도록 처리하고 있습니다.
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;
}
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}
그렇다면 클래스 자료형의 객체 데이터일 경우 equals() 메소드는 어떻게 다뤄질까요?
비교할 대상이 객체일 경우 객체의 주소를 이용하여 비교하면 됩니다.
즉, 객체 자체를 비교할때는 == 이나 equals() 나 똑같다고 보시면 됩니다.
아래 코드에서 ==의 경우 객체 타입인 경우에는 주소 값을 비교하기에 서로 다른 주소를 가지고 있어 false가 출력됩니다.
equals 메소드 또한 객체 타입인 경우 주소 값을 비교하기 때문에 false가 출력됩니다.
class Member {
String name;
public Member(String name) {
this.name = name;
}
}
public class EqualsExample {
public static void main(String[] args) {
Member 테스트1 = new Member("테스트");
Member 테스트2 = new Member("테스트");
System.out.println(테스트1 == 테스트2);
System.out.println(테스트1.equals(테스트2));
}
}
위 예시의 테스트1, 테스트2 변수 모두 각기 다른 객체를 초기화하여 힙 영역에 따로 저장하고 있으니 두 객체 변수를 비교하면 주소값이 일치하지 않아 당연히 false가 출력됩니다.
이는 물론 프로그램 입장에서는 둘이 다른 것이 맞지만 사용자 입장에서는 두 객체의 데이터가 동일하기에 같다고 봐야할 수도 있습니다.
때문에 객체를 비교할 때 주소 값이 아닌 객체의 필드 값을 기준으로 동등성(Equality) 비교를 하고 싶다면, equals 메소드를 오버라이딩하여 주소 값이 아닌 필드 값 비교로 재정의 하면 됩니다.
class Member {
String name;
public Member(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Member)) {
return false;
}
Member member = (Member) o;
return Objects.equals(name, member.name);
}
}
앞서 보여준 코드에서 equals 메소드를 오버라이딩을 하였습니다.
현재 객체 this와 매개변수 객체가 같을 경우 true를 반환합니다.
만약 매개변수 객체가 Member 타입과 호환되지 않으면 false를 반환합니다.
타입이 호환이 되면 다운 캐스팅을 진행합니다.
this 객체와 매개변수 객체 이름이 같을 경우 true, 아닌 경우 false
hashCode 메소드는 객체의 주소 값을 해싱 기법을 통해 해시 코드로 만든 후 반환합니다.
실제 Object 클래스에 정의된 hashCode() 메소드를 확인해보자
native 키워드가 들어간 메소드는 OS가 가지고 있는 메소드를 의미합니다.
native는 메소드에만 적용 가능한 제어자로 JNI를 통해 Java에서 이용하고자 할 때 사용됩니다.
JNI는 저수준의 언어로 작성된 native 코드를 JVM에 적재시키고 실행해주는 머신입니다.
public native int hashCode();
동일한 객체는 동일한 메모리 주소를 갖는 것을 의미하기 때문에 동일한 객체는 동일한 해시 코드를 가져야 합니다.
euqals()는 오버라이딩 하면 hasCode()도 같이 오버라이딩 해야 합니다.
왜냐하면 equals()의 결과가 true인 두 객체의 해시코드는 반드시 같아야 하는 자바의 규칙이 존재하기 때문입니다.
또한 보통의 IDE에서 equals와 hashCode를 같이 재정의 해주는데, 그 이유는 hash 값을 사용하는 Collection Framework를 사용할 때 문제가 발생하기 때문입니다.
아래 코드를 실행하면 HashSet이기 때문에 members.size() = 1 을 기대하지만 실제로는 2가 출력됩니다.
해시코드가 다르기 대문에 중복된 데이터가 컬렉션에 추가된 것입니다.
public class EqualsExample {
public static void main(String[] args) {
HashSet<Member> members = new HashSet<>();
Member 테스트1 = new Member("테스트1");
Member 테스트2 = new Member("테스트1");
members.add(테스트1);
members.add(테스트2);
System.out.println("members.size() = " + members.size());
System.out.println("테스트1.hashCode() = " + 테스트1.hashCode()); // 398690014
System.out.println("테스트2.hashCode() = " + 테스트2.hashCode()); // 1526298704
}
}
hash 값을 사용하는 Collection(HashMap, HashSet, HashTable)은 객체가 논리적으로 같은지 비교할 때 아래 그림의 과정을 거칩니다.
가장 먼저 데이터가 추가된다면, hashCode()의 리턴 값을 컬렉션에 가지고 있는지 비교합니다.
hashCode()의 값이 같다면 다음으로 equals() 메소드의 리턴 값을 비교하게 되고 true이면 논리적으로 같은 객체라고 판단합니다.
출처: https://velog.io/@poiuyy0420/Java%EC%9D%98-equals-%EC%99%80-hashcode
위에 소개해 드린 Member 코드를 보시면 HashSet에 Member 객체를 추가할 때도 위와 같은 과정으로 중복 여부를 판단하고 HashSet에 추가하게 됩니다.
하지만 Member 객체는 hashCode() 오버라이딩 하고 있지 않아서 Object 클래스의 hashCode 메소드가 사용되었고 객체마다 다른 값을 리턴하였습니다.
따라서 Collection 예시와 같은 오작동을 방지하기 위해 hashCode메소드도 정의해주면 됩니다.