예를 들어, 아래와 같이 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 메서드를 재정의 한 후 테스트 코드를 실행하면 테스트가 통과한다.
.png)
@Override로 재정의 하지 않으면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다.
Object 클래스에 정의된 equals()는 다음과 같다.
public boolean equals(Object obj) {
return (this == obj);
}
즉, 오직 자기 자신과만 같다고 인식한다.
equals가 논리적 동치성을 비교하도록 재정의 되어있지 않을때equals가 논리적 동치성을 확인하도록 재정의 해두면, 그 인스턴스 값의 비교가 가능하고 Map의 key와 Set의 원소로 사용할 수 있다.인스턴스가 둘 이상 만들어지지 않음을 보장하는 클래스
equals가 논리적 동치성까지 확인해준다고 볼 수 있다.(null이 아닌 모든 참조값 x,y,z에 대해)
x.equals(x)는 truex.equals(y)가 true이면 y.equals(x)도 truex.equals(y)는 true이고 y.equals(z)는 true이면 x.equals(z)는 truex.equals(y)를 반복해서 호출해도 항상 true 또는 false를 반환x.equals(null)는 falseequals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
리스코프 치환 원칙(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 이 발생한다.
.png)
그 이유는 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 메서드로 객체를 조회하는 경우
.png)
위 그림에서 세 객체 (Entry<K1,V1>, Entry<K2,V2>, Entry<K3,V3>)는 서로 같은 해시값을 같는다. 따라서 hashcode() 메서드는 같은 값을 리턴한다. 하지만 서로 값이 다른 객체이기 때문에 equals() 메서드는 false를 리턴한다.
만약 equals()와 hashcode() 중 하나만 재정의 하면 어떻게 될까? 위 예에서도 봤듯이 hashcode()를 재정의 하지 않으면 같은 값 객체라도 해시값이 다를 수 있다. 따라서 HashTable에서 해당 객체가 저장된 버킷을 찾을 수 없다.
반대로 equals()를 재정의하지 않으면 hashcode()가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 된다. 따라서 역시 원하는 객체를 찾을 수 없다.
중요한 내용은 아니지만 중간에 '언제 equlas를 재정의 해야 할까?' 여기에 equlas라고 오타가 있어요👀 정리 너무 잘해주셔서 이해가 쏙쏙 됩니다 좋은 글 잘 보고갑니다😊 감사합니다!!