예를 들어, 아래와 같이 Menu
클래스가 있다고 하자.
public class Menu {
private final String name;
private final int price;
public Menu(final String name, final int price) {
this.name = name;
this.price = price;
}
}
이 때 name
과 price
값이 똑같은 두 Menu 객체를 비교하면 어떻게 될까?.
@Test
@DisplayName("같은 객체를 equals 비교")
void equals() {
//given
Menu friedChicken = new Menu("후라이드치킨", 16_000);
Menu friedChicken2 = new Menu("후라이드치킨", 16_000);
//when & then
assertThat(friedChicken).isEqualTo(friedChicken2);
}
두 개의 Menu객체(friedChicken, friedChicken2)는 name
과 price
가 서로 같은 객체 지닌다. 위 테스트 코드를 실행한 결과는 다음과 같다.
테스트가 실패했다. 그 이유는 두 객체의 주소값이 다르기 때문이다.
즉, equals
메서드는 주소값이 다른 객체는 서로 다른 객체로 판단한다.
두 객체를 같도록 하려면 Menu 클래스에 equals
메서드를 재정의 해야 한다.
public class Menu {
private final String name;
private final int price;
public Menu(final String name, final int price) {
this.name = name;
this.price = price;
}
// equals 재정의
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Menu))
return false;
Menu menu = (Menu)o;
return price == menu.price &&
Objects.equals(name, menu.name);
}
}
equals
메서드를 재정의 한 후 테스트 코드를 실행하면 테스트가 통과한다.
@Override로 재정의 하지 않으면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다.
Object 클래스에 정의된 equals()
는 다음과 같다.
public boolean equals(Object obj) {
return (this == obj);
}
즉, 오직 자기 자신과만 같다고 인식한다.
equals
가 논리적 동치성을 비교하도록 재정의 되어있지 않을때equals
가 논리적 동치성을 확인하도록 재정의 해두면, 그 인스턴스 값의 비교가 가능하고 Map의 key와 Set의 원소로 사용할 수 있다.인스턴스가 둘 이상 만들어지지 않음을 보장하는 클래스
equals
가 논리적 동치성까지 확인해준다고 볼 수 있다.(null이 아닌 모든 참조값 x,y,z에 대해)
x.equals(x)
는 true
x.equals(y)
가 true
이면 y.equals(x)
도 true
x.equals(y)
는 true
이고 y.equals(z)
는 true
이면 x.equals(z)
는 true
x.equals(y)
를 반복해서 호출해도 항상 true
또는 false
를 반환x.equals(null)
는 false
equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
리스코프 치환 원칙(Liskov substitution principle)
hashCode
도 반드시 재정의한다꼭 필요한 경우가 아니면 equals를 재정의하지 않는다. 대부분의 경우 Object의 equals 메서드가 원하는 비교를 정확히 수행한다.
재정의 해야할 경우 그 클래스의 핵심 필드를 빠짐없이 다섯 가지 규약을 지키며 비교한다.
equals
를 재정의한 클래스에는hashCode
도 반드시 재정의한다.Override either both of them or neither of them.
equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 객체의 hashCode
메서드는 몇 번을 호출해도 항상 같은 값을 반환한다. (단, 애플리케이션을 다시 실행하면 값은 바뀔 수 있다.)
equals(Object)
가 두 객체를 같다고 판단했으면, 두 객체의 hashCode 값은 항상 같다.
하지만 equals(Object)
가 두 객체를 다르다고 판단했더라도, 두 객채의 hashCode 값은 같을 수 있다. (해시 충돌)
If you are planning to use a class as Hash table key, then you must override both equals() and hashCode() methods.
Map<Menu,Integer> menus = new HashMap<>();
menus.put(new Menu("치킨", 16_000), 10);
menus.put(new Menu("감자튀김", 8_000), 2);
menus.put(new Menu("콜라", 2_000), 7);
Menu menu = new Menu("치킨", 16_000);
int count = menus.get(menu);
위 코드를 실행했을 때 count가 10일 것을 기대하지만 결과는 그렇지 않다.
@Test
@DisplayName("같은 값 객체는 해시값이 같은지 체크")
void hashcode_menu() {
//given
Map<Menu, Integer> menus = new HashMap<>();
menus.put(new Menu("치킨", 16_000), 10);
menus.put(new Menu("감자튀김", 8_000), 2);
menus.put(new Menu("콜라", 2_000), 7);
//when
Menu menu = new Menu("치킨", 16_000);
int count = menus.get(menu);
//then
assertThat(count).isEqualTo(10);
}
위 테스트 코드를 실행하면 아래와 같이 NullPointerException
이 발생한다.
그 이유는 menu
객체에 대한 해시값을 menus
에서 찾을 수 없기 때문이다. HashMap의 key값으로 Menu 클래스를 사용하기 위해서는 Menu 클래스에 hashCode() 메서드를 재정의 해줘야한다. 그래야 같은 값을 가진 객체가 항상 같은 해시값을 갖게된다.
public class Menu {
private final String name;
private final int price;
public Menu(final String name, final int price) {
this.name = name;
this.price = price;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Menu))
return false;
Menu menu = (Menu)o;
return price == menu.price &&
Objects.equals(name, menu.name);
}
// hashcode 재정의
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
hashcode
를 재정의 해주었으므로 같은 값을 가지는 객체는 같은 해시값을 갖는다. 따라서 테스트 코드는 통과한다.
equals
와 hashcode
메서드를 이해하기 위해서 자바에서 HashTable이 작동하는 원리를 간단히 살펴보자.
HashTable은 <key,value> 형태로 데이터를 저장한다. 이 때 해시 함수(Hash Function)을 이용하여 key값을 기준으로 고유한 식별값인 해시값을 만든다. (hashcode
가 해시값을 만드는 역할을 한다.) 이 해시값을 버킷(Bucket)에 저장한다.
하지만 HashTable 크기는 한정적이기 때문에 같은 서로 다른 객체라 하더라도 같은 해시값을 갖게 될 수도 있다. 이것을 해시 충돌(Hash Collisions)이라고 한다. 이런 경우 아래와 같이 해당 버킷(Bucket)에 LinkedList 형태로 객체를 추가한다.
이미지 출처: https://www.geeksforgeeks.org/implementing-our-own-hash-table-with-separate-chaining-in-java/
이처럼 같은 해시값의 버킷 안에 다른 객체가 있는 경우 equals
메서드가 사용된다.
HashTable에 put
메서드로 객체를 추가하는 경우
HashTable에 get
메서드로 객체를 조회하는 경우
위 그림에서 세 객체 (Entry<K1,V1>, Entry<K2,V2>, Entry<K3,V3>
)는 서로 같은 해시값을 같는다. 따라서 hashcode()
메서드는 같은 값을 리턴한다. 하지만 서로 값이 다른 객체이기 때문에 equals()
메서드는 false
를 리턴한다.
만약 equals()
와 hashcode()
중 하나만 재정의 하면 어떻게 될까? 위 예에서도 봤듯이 hashcode()
를 재정의 하지 않으면 같은 값 객체라도 해시값이 다를 수 있다. 따라서 HashTable에서 해당 객체가 저장된 버킷을 찾을 수 없다.
반대로 equals()
를 재정의하지 않으면 hashcode()
가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 된다. 따라서 역시 원하는 객체를 찾을 수 없다.
중요한 내용은 아니지만 중간에 '언제 equlas를 재정의 해야 할까?' 여기에 equlas라고 오타가 있어요👀 정리 너무 잘해주셔서 이해가 쏙쏙 됩니다 좋은 글 잘 보고갑니다😊 감사합니다!!