equals메서드는 많은 경우에 Object의 equals가 정확히 비교해주기 때문에 꼭 필요한 경우가 아니라면 재정의하지 말자.
equals를 재정의 하지 말아야하는 경우
- 각 인스턴스가 본질적으로 고유하다.
- 인스턴스가 가지는 값보다 동작하는 개체임을 나타내는 게 더 중요한 클래스(Thread)
- Thread클래스는 Object의 equals로 충분함
- 인스턴스의 논리적 동치성을 검사할 일이 없다.
- java.util.regex.Pattern의 equals를 재정의해서 두 Pattern인스턴스가 같은 정규표현식을 나타내는지 검사할 필요가 없다.
- 상위 클래스에서 재정의한 equals가 하위클래스에도 딱 들어맞는다.
- 클래스가 private이거나 package-private이고 equals메서드를 호출할 일이 없다.
equals가 실수로라도 호출되는 걸 막고 싶다면 아래와 같이 구현한다.
@Override public boolean equals(Object o) { throw new AssertionError(); // 호출 금지 }
equals를 재정의 해야할 때는?
객체 식별성(두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 확인해야하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다. (주로 값 클래스 ex. Integer, String)
값 클래스여도 같은 값의 인스턴스가 둘 이상 만들어지지 않는 클래스라면 재정의하지 않아도 된다.(ex. enum)
equals 메서드 재정의 규약
동치관계를 구현하며 다음을 만족한다.
☝️반사성(reflexivity)
- null이 아닌 모든 참조값 x에 대해, x.equals(x)는 true
- 객체는 자기자신과 같아야 한다는 뜻 > 위반하는 것이 더 어려움
☝️대칭성(symmetry)
- null이 아닌 모든 참조값 x,y에 대해 x.equals(y) = true 면, y.equals(x)=true
- 두 객체는 서로에 대한 동치 여부에 똑같이 대답해야함
public class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 대칭성 위배
@Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(((CaseInsensitiveString)o).s);
if(o instanceof String) // 한 방향으로만 작동한다
return s.equalsIgnoreCase((String) o);
return false;
}
}
대소문자를 무시하고 문자열을 비교하는 클래스를 구현했다.
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s= "polish";
System.out.println(cis.equals(s)); // true
System.out.println(s.equals(cis)); // false
CaseInsensitiveString의 equals는 String을 알지만 String의 equals는 CaseInsentiveString의 존재를 모른다.
대칭성을 위반한다.
☝️추이성(transitivity)
- null이 아닌 모든 참조값 x,y,z에 대해 x.equals(y) = true 면 y.equals(x)=true이고
y.equals(z) = true면 x.equals(z) = true - 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같으면 첫 번째와 세 번째 객체가 같아야 한다.
상위 클래스에 없는 새로운 필드를 하위 클래스에 추가하는 경우
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
위 클래스에서 색상을 더할 경우
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint)o).color == color;
}
}
색상정보는 무시하고 비교하기 때문에 equals를 새로 구현해보았다.
그러면 Point와 ColorPoint를 비교한 결과가 다를 수 있다.
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
p.equals(cp) // true
cp.equals(p) // false
색상을 무시하도록 할 경우
@Override
public boolean equals(Object o) {
if(!(o instanceof Point))
return false;
// o가 Point면 색상 무시하고 비교
if(!(o instanceof ColorPoint))
return o.equals(this);
// o가 ColorPoint면 색상까지 비교
return super.equals(o) && ((ColorPoint)o).color == color;
}
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1,2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
p1.equals(p2); // true
p2.equals(p3); // true
p1.equals(p3); // false
대칭성은 만족시켜주지만 추이성이 깨진다.
class SmellPoint extends Point {
private final Smell smell;
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
//o가 일반 Point이면 smell을 무시햐고 x,y정보만 비교한다.
if(!(o instanceof SmellPoint)) return o.equals(this);
//o가 SmellPoint면 smell까지 비교한다.
return super.equals(o) && this.smell == ((SmellPoint) o).smell;
}
}
Point p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new SmellPoint(1, 2, Smell.SPICY);
System.out.println(p1.equals(p2)); // StackOverflow
p1은 ColorPoint의 두 번째 if문 때문에 SmellPoint의 equals를 호출한다.
SmellPoint의 equals는 2번째 if문때문에 다시 ColorPoint의 equals를 호출한다.
무한 재귀로 인해 StackOverflow가 발생한다.
구체 클래스를 확장해 새로운 값을 추가하면서 equals규약을 만족시킬 방법은 없다.
▶️ equals의 instanceof를 getClass로 바꾸면 구체클래스를 상속할 수 있다는 뜻으로 들리지만 틀렸다.
리스코프 치환 원칙
어떤 타입에 있어 중요한 속성이라면 그 하위타입에서도 마찬가지로 중요하다는 원칙
@Override
public boolean equals(Object o) {
if(o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
위 코드는 같은 구현 클래스의 객체와 비교할 때만 true를 반환한다.
예를 들어 Point를 상속받는 CounterPoint객체를 만들고 equals를 getClass를 통해 작성했다면 CounterPoint 인스턴스는 어떤 Point와도 같을 수 없기 때문에 제대로 동작하지 않는다.
해결방법 - 상속 대신 컴포지션을 사용하라
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
equals 규약을 지키면서 값을 추가한다.
기존 클래스를 확장하는 대신 private필드로 두고 기존 클래스에 대응하는 메서드(asPoint)를 호출해 결과를 반환한다.
☝️일관성(consistency)
- null이 아닌 모든 참조값 x,y에 대해 x.equals(y)를 반복해서 호출하면 항상 true, 항상 false
- 두 객체가 같다면 어느 하나가 수정되지 않는 한 영원히 같다는 뜻
- 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들면 안됨
- java.net.URL의 equals는 매핑된 호스트의 IP주소로 비교하는데 그 결과가 항상 같다고 보장할 수 없다.
▶️ equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야함
- java.net.URL의 equals는 매핑된 호스트의 IP주소로 비교하는데 그 결과가 항상 같다고 보장할 수 없다.
☝️null-아님
- null이 아닌 모든 참조값 x에 대해 x.equals(null)은 false다.
- 모든 객체가 null과 같지 않아야 한다는 뜻
@Override public boolean equals(Object o) {
if(o == null)
return false;
...
}
명시적인 null검사는 할 필요가 없다.
@Override public boolean equals(Object o) {
if(!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
이렇게 검사하는 편이 낫다.
equals메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
자기자신이면 true를 반환한다. - instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기자신의 대응되는 '핵심'필드들이 모두 일치하는지 하나씩 검사한다.
equals를 다 구현했다면 체크할 것
- 대칭적인가
- 추이성이 있는가
- 일관적인가
이상의 비법에 따라 작성해본 클래스
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if(val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
public boolean equals(Object o) {
if(o == this)
return true;
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
equals 작성시 주의사항
- equals를 재정의할 때는 hashCode도 반드시 재정의하자
- 너무 복잡하게 해결하려 들지말자
- Object 외의 타입을 매개변수로 받는 메서드는 선언하지 말자.
equals 테스트는 AutoValue 프레임워크로
클래스에 어노테이션만 추가하면 AutoValue가 메서드들을 알아서 작성해준다.
'책리뷰 > 이펙티브자바' 카테고리의 다른 글
[이펙티브자바] 아이템11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.03.04 |
---|---|
[이펙티브자바] 아이템9. try-finally보다는 try-with-resources를 사용하라 (0) | 2022.02.28 |
[이펙티브자바] 아이템8. finalizer와 cleaner사용을 피하라 (0) | 2022.02.24 |
[이펙티브자바] 아이템7. 다 쓴 객체 참조를 해제하라 (0) | 2022.02.23 |
[이펙티브자바] 아이템6. 불필요한 객체 생성을 피하라 (0) | 2022.02.15 |