예를 들어, 아래와 같이 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 메서드를 재정의 한 후 테스트 코드를 실행하면 테스트가 통과한다.
equals 메서드
@Override로 재정의 하지 않으면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다.
Java equals()
Object 클래스에 정의된 equals()는 다음과 같다.
public boolean equals(Object obj) {
return (this == obj);
}
즉, 오직 자기 자신과만 같다고 인식한다.
언제 equals를 재정의 해야 할까?
- 논리적 동치성을 확인해야 하는데 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되어있지 않을때
- 주로 값 클래스 : Integer, String처럼 값을 표현하는 클래스
- 두 값 객체를 equals로 비교한다는 것은 객체가 같은지가 아니라 값이 같은지 알고 싶은 것이다.
- equals가 논리적 동치성을 확인하도록 재정의 해두면, 그 인스턴스 값의 비교가 가능하고 Map의 key와 Set의 원소로 사용할 수 있다.
값 클래스 중 equals를 재정의하지 않아도 되는 경우
인스턴스가 둘 이상 만들어지지 않음을 보장하는 클래스
- ex) Enum
- 이런 클래스에서는 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않는다.
- 따라서 논리적 동치성과 객체 식별성이 사실상 똑같은 의미가 된다.
- 이런 경우 Object의 equals가 논리적 동치성까지 확인해준다고 볼 수 있다.
Equals 메서드 규약
(null이 아닌 모든 참조값 x,y,z에 대해)
- 반사성(reflexivity) : x.equals(x)는 true
- 대칭성(symmetry) : x.equals(y)가 true이면 y.equals(x)도 true
- 추이성(transitivity) : x.equals(y)는 true이고 y.equals(z)는 true이면 x.equals(z)는 true
- 일관성(consistency) : x.equals(y)를 반복해서 호출해도 항상 true 또는 false를 반환
- null-아님 : x.equals(null)는 false
equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
리스코프 치환 원칙(Liskov substitution principle)
- 어떤 타입에 있어 중요한 속성이면 그 하위 타입에서도 중요하다.
- 그 타입의 모든 메서드가 하위 타입에도 똑같이 잘 작동해야 한다.
- "Point의 하위 클래스는 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다."
양질의 equals 메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.(2번에서 instanceof 검사를 했으니 100% 성공한다.)
- 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
- equals를 재정의할 땐 hashCode도 반드시 재정의한다
꼭 필요한 경우가 아니면 equals를 재정의하지 않는다. 대부분의 경우 Object의 equals 메서드가 원하는 비교를 정확히 수행한다.
재정의 해야할 경우 그 클래스의 핵심 필드를 빠짐없이 다섯 가지 규약을 지키며 비교한다.
hashCode 메서드
equals를 재정의한 클래스에는 hashCode도 반드시 재정의한다.
Override either both of them or neither of them.
hashCode 규약
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 객체의 hashCode 메서드는 몇 번을 호출해도 항상 같은 값을 반환한다. (단, 애플리케이션을 다시 실행하면 값은 바뀔 수 있다.)
- equals(Object)가 두 객체를 같다고 판단했으면, 두 객체의 hashCode 값은 항상 같다.
- 하지만 equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객채의 hashCode 값은 같을 수 있다. (해시 충돌)
- 해시 테이블에서 해시 충돌이 발생하면 LinkedList 형태로 객체를 추가한다.
- 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
- 좋은 해시 함수라면 서로 다른 인스턴스에 대해 다른 해시코드를 반환한다.
hashCode를 재정의 하지 않았을 경우 생기는 문제점
- 같은 값을 가진 객체가 서로 다른 해시값을 갖게 될 수 있다.
- 특히 HashMap의 key 값으로 해당 객체를 사용할 경우 문제가 발생한다.
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를 재정의 해주었으므로 같은 값을 가지는 객체는 같은 해시값을 갖는다. 따라서 테스트 코드는 통과한다.
Java HashTable
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 메서드로 객체를 추가하는 경우
- 값이 같은 객체가 이미 있다면(equals()가 true) 기존 객체를 덮어쓴다.
- 값이 같은 객체가 없다면(equals()가 false) 해당 entry를 LinkedList에 추가한다.
HashTable에 get 메서드로 객체를 조회하는 경우
- 값이 같은 객체가 있다면 (equals()가 true) 그 객체를 리턴한다.
- 값이 같은 객체가 없다면(equals()가 false) null을 리턴한다.
위 그림에서 세 객체 (Entry<K1,V1>, Entry<K2,V2>, Entry<K3,V3>)는 서로 같은 해시값을 같는다. 따라서 hashcode() 메서드는 같은 값을 리턴한다. 하지만 서로 값이 다른 객체이기 때문에 equals() 메서드는 false를 리턴한다.
equals()와 hashcode()를 같이 재정의해야 하는 이유
만약 equals()와 hashcode() 중 하나만 재정의 하면 어떻게 될까? 위 예에서도 봤듯이 hashcode()를 재정의 하지 않으면 같은 값 객체라도 해시값이 다를 수 있다. 따라서 HashTable에서 해당 객체가 저장된 버킷을 찾을 수 없다.
반대로 equals()를 재정의하지 않으면 hashcode()가 만든 해시값을 이용해 객체가 저장된 버킷을 찾을 수는 있지만 해당 객체가 자신과 같은 객체인지 값을 비교할 수 없기 때문에 null을 리턴하게 된다. 따라서 역시 원하는 객체를 찾을 수 없다.
결론
- equals()와 hashcode()는 항상 같이 재정의 한다.
- Always override hashCode() if we override equals()
- 값객체(Value Object)에는 equals()와 hashCode()를 재정의하자
- Override equals() and hashCode() for value objects(VO)
Reference
'Language > Java' 카테고리의 다른 글
많이 쓰이는 디자인 패턴 (0) | 2022.06.08 |
---|---|
인터페이스와 추상클래스의 차이..? (0) | 2022.06.08 |
자바 메소드(Method) 타입 : public, protected.. (0) | 2022.01.19 |
Java 소켓 통신 이란? (0) | 2022.01.07 |
최근댓글