본문 바로가기

programming/java

Item 10. equals 일반 규약을 지켜 재정의하라

equals 재정의가 필요하지 않은 경우

  1. 동치 관계인 인스턴스가 없다. (각 인스턴스가 본질적으로 고유하다)
    • 생성한 인스턴스가 모두 다르다는 것 → 비교하는 것이 무의미하다.
  2. equals를 사용할 일이 없다.
    • 인스턴스의 논리적 동치성(logical equality)를 검사할 일이 없다.
    • equals 메서드를 사용할 경우가 없는 경우
      • 클래스가 private 이거나 package-private 인 경우
  3. 쓸데없다.
    • 상위 클래스에 구현된 equals를 그대로 사용하는 경우

equals 메서드 재정의 일반 규약

equals 메서드는 동치관계 (equivalence relation)를 구현하며, 다음 특성을 만족한다.

  1. 반사성(reflexivity) : null 이 아닌 모든 참조 값 x에 대해 x.equals(x)
    는 true다.
  2. 대칭성(symmetry) : null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)가 true면, y.equals(x)도 true다.
  3. 추이성(transitivity) : null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true 이면, x.equals(z)도 true다.
  4. null이 아니다 : null이 아닌 모든 참조 값 x에 대해서 x.equals(null)은 false다.

예외 사항

구체 클래스를 확장할 때 새로운 필드 값 추가시 equals 규약 만족 불가하다.

따라서 이때는 상속 대신 컴포지션을 사용해야한다.

//속성1
public class Point {
    int x;
    int y;

    @Override
    public boolean equals(Object o) {
        //규약에 맞게 구현
        if (o == null) return false;
        if (o == this) return true;
        if(o instanceof Point){
            Point p = (Point) o;
            return p.x == this.x && p.y == this.y;
        }
        return false;
    }
}
//속성 2
public class Color {
    String colorName;

    @Override
    public boolean equals(Object o) {
        //규약에 맞게 구현
        if (o == null) return false;
        if (o == this) return true;
        if(o instanceof Color){
            Color c = (Color) o;
            return c.colorName.equals(this.colorName);
        }
        return false;
    }
}
//컴포지션 방식
public class ColorPoint {
    Point p;
    Color c;
    @Override
    public boolean equals(Object o) {
        //규약에 맞게 구현
        if (o == null) return false;
        if (o == this) return true;
        if(o instanceof ColorPoint){
            ColorPoint cp = (ColorPoint) o;
            return cp.p.equals(this.p) && cp.c.equals(this.c);
        }
        return false;
    }
}
//상속 방식
public class ColorPoint extends Point{
    String colorName;

    @Override
    public boolean equals(Object o) {
        //규약에 맞게 구현
        if (o == null) return false;
        if (o == this) return true;
        if(o instanceof ColorPoint){
            ColorPoint cp = (ColorPoint) o;
            return super.equals(o) && cp.colorName.equals(this.colorName);
        }
        return false;
    }
}

equals 구현 방법

  • == 연산자를 이용하여 입력이 자기 자신의 참조인지 확인해야 한다.
    • 자신의 참조라면 true를 반환해야 한다.
  • instanceof 연산자로 입력된 변수가 올바른 타입인지 확인해야 한다. 그렇지 않다면 false를 반환한다.
  • 입력을 올바른 타입으로 형변환한다. 위에서 타입을 검사했으므로 무조건 성공하게 됩니다.
  • 입력된 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치한지 비교한다.
    • 모든 필드가 일치하면 true, 그렇지 않다면 false를 반환한다.
    • float와 double을 제외한 기본 타입(primitive type)은 == 연산자로 비교하고
    • float와 double은 부동 소숫점 등을 위해 Float.compare, Double.compare로 비교한다.
    • 참조 타입 필드의 경우는 각각의 equals 메서드로 비교한다.
    • 배열은 Arrays.equals 메서드를 사용한다.

핵심정리

꼭 필요한 경우가 아니면 equals를 재정의하지 말자 Object equals가 많은 경우에 비교를 정확히 수행해준다. 재정의 해야 할때는 그 클래스의 핵심 필드를 모두 빠짐없이, 다섯가지 규약을 확실히 지켜가며 비교해야한다.