Item10. equals는 일반 규칙을 지켜 재정의하라

Java에서 최상위 class인 Object의 final이 아닌 methode들 (equals, hashCode, toString ,clone, finalize)는 모두 overriding을 염두에 두고 설계된 것이라, overriding시 지켜야 하는 일반 규약이 명확하다.

1
public boolean equals(Object obj);

이 중에서 equals method는 == 연산자와 마찬가지로 참조변수의 참조값을 비교하도록 정의되어 있는데, 다음과 같은 상황에서는 overriding 하지 않는 것이 좋다.

1
2
3
4
5
6
7
1. 각 인스턴스가 고유한 경우  ex) Thread와 같이 객체가 값을 표현하는 목적이 아닌 경우

2. 인스턴스가 논리적으로 동일한지 검사할 일이 없는 경우

3. 상위 class에서 재정의한 equals가 하위 클래스에도 적합한 경우

4. 클래스가 private 이거나, package-private이고, equals method가 호출될 일이 없는 경우

equals는 언제 overriding해야하는가?

  • 두 객체가 참조값이 동일한지(물리적으로 동일한 주소값을 같는지) 가 아니라, 두 객체가 논리적으로 동일한지 값을 비교하고자 할떄 사용한다.

  • 인스턴스가 둘 이상 만들어지는 값 클래스

equals method overriding시 규약

1
2
3
4
5
6
7
8
9
10
11

1. 반사성 (reflexivity) : null이 아닌 모든 참조값 x에 대해 x.equal(x) == true

2. 대칭성 (symmetry) : null이 아닌 모든 참조값 x,y에 대해 y.equals(x) == true -> x.equals(y) == true

3. 추이성 (transitivity) : null이 아닌 모든 참조값 x,y,z에 대해 x.equals(y) == true , y.equals(z) == true -> x.equals(z) == true

4. 일관성 (consistency) : null이 아닌 모든 참조값 x,y에 대해 x.equals(y) 를 반복해서 호출하면 항상 true or false

5. null 아님

( https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object) )

상속구조에서 Equals method 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@AllArgsConstructor
public class Coffee {

protected Integer temperature; // 커피 온도

protected Size size; // 커피 사이즈 Large,Medium,Small


@Override
public boolean equals(Object o) {

if( !(o instanceof Coffee)){
return false;
}
Coffee coffee = (Coffee) o;
return this.size.equals(coffee.size) && this.temperature.equals(coffee.temperature);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Espresso extends Coffee {

private Integer shotNum;


public Espresso(Integer temperature, Size size , Integer shotNum) {
super(temperature, size);
this.shotNum = shotNum;
}

@Override
public boolean equals(Object o) {
if(!(o instanceof Espresso)){
return false;
}
return super.equals(o) && ((Espresso) o).shotNum.equals(this.shotNum);
}
}

위와 같이 코드를 짜면, 자식 클래스에 새로운 값타입이 추가되서 자식에서 equals를 호출하게 되면 , 대칭성이 성립하지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

class CoffeeTest {

@Test
void testSymmetry() {
Coffee coffeeA = new Coffee(25, Size.MEDIUM);
Coffee coffeeB = new Coffee(25, Size.MEDIUM);
assertThat(coffeeA.equals(coffeeB)).isEqualTo(coffeeB.equals(coffeeA));
}

@Test
@DisplayName("상속구조에서 equals 테스트")
void testSymmetryInheritance(){
Coffee coffee = new Coffee(25, Size.MEDIUM);
Espresso espresso = new Espresso(25, Size.MEDIUM, 3);
assertThat(espresso.equals(coffee)).isNotEqualTo(coffee.equals(espresso));
}
}

해결방법

  1. equals method 를 instance of 대신 class가 다르면 false를 반환 (지양)
    * 리스코프 치환 원칙(LSP) : 상위 타입의 객체를 하위 타입의 객체을 사용하는 프로그램은 정상적으로 동작해야 함을 위배
  • contains와 같이 내부적으로 equals를 사용하는 method에서 하위 class가 들어오면 parameter와 무관하게 항상 부모class와 같지않음을 return 함.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@AllArgsConstructor
public class Coffee {

protected Integer temperature; // 커피온도

protected Size size; // 커피 사이즈 Large,Medium,Small


@Override
public boolean equals(Object o) {

if( o == null || o.getClass() != this.getClass()){
return false;
}

Coffee coffee = (Coffee) o;
return this.size.equals(coffee.size) && this.temperature.equals(coffee.temperature);
}

}

2. 상속 대신 컴포지션 사용

상속구조를 버리고, 멤버변수에 값타입으로 사용한다. (지향)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Espresso  {

private Coffee coffee;

private Integer shotNum;


public Espresso(Integer temperature , Size size , Integer shotNum) {
this.coffee = new Coffee(temperature, size);
this.shotNum = shotNum;
}

public Coffee asCoffee(){
return coffee;
}

@Override
public boolean equals(Object o) {
if(!(o instanceof Espresso)){
return false;
}
Espresso ep = (Espresso) o;
return ep.coffee.equals(ep) && (ep).shotNum.equals(this.shotNum);
}
}

더이상 상속구조가 아니기에 다음과 같이 값타입을 view method로 꺼내서 비교하면 된다.

1
2
3
4
5
6
7
@Test
void testEquals() {
Espresso espresso = new Espresso(25, Size.MEDIUM, 10);
Coffee coffee = new Coffee(25, Size.MEDIUM);
assertThat(espresso.asCoffee().equals(coffee)).isTrue();
assertThat(coffee.equals(espresso.asCoffee())).isTrue();
}

결론은 equals 비교할일이 없다면 굳이 재정의하지말고 값을 비교해야할 값타입인 경우에는 상속구조를 사용하지 말고, 필드로 값을 추가한다.

equals method 구현방법 정리

1
2
3
4
5
6
7
8
9
10
1. == 연산자를 이용해 입력이 자신의 참조인지 확인

2. instanceof 로 타입 확인

3. 명시적 형 변환

4. 값이 같다고 판단할떄 필요한 필드들의 일치여부 확인

+) hashCode도 재정의 (item 11)

Comments