ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Equals 의 재정의 규칙 (Effective Java 3rd_Item10)
    Java 2019. 7. 25. 23:50
    반응형

     Object 클래스의 equals() 메서드는 재정의할 때 일반 규약을 지키지 않으면 예상치 못한 결과를 초래할 수 있습니다. 이러한 문제들을 회피하는 좋은 방법은 아예 재정의를 하지 않는 것입니다. 아래와 같은 상황에서는 equals() 메서드를 재정의하지 않는 것이 최선입니다.

    • 각 인스턴스가 본질적으로 고유할 때. (값을 표현하는 객체가 아니라 동작하는 객체를 표현하는 경우)
    • 인스턴스 간에 논리적인 동치성을 검사할 필요가 없을 때.
    • 상위 클래스에서 재정의한 equals() 메서드가 하위 클래스에도 딱 들어맞을 때.
    • 클래스가 private이거나 default이고, equals() 메서드를 호출할 일이 없을 때.

     

     만약 equals() 메서드를 재정의할 때는 반드시 동치 관계를 구현하며, 다음과 같은 일반 규약을 만족해야 합니다.

    • 반사성(reflexivity) : null 이 아닌 모든 참조 값 x 에 대해, x.equals(x) 는 true
    • 대칭성(symmetry) : null 이 아닌 모든 참조 값 x, y 에 대해, x.equals(y) 가 true이면 y.equals(x) 도 true
    • 추이성(transitivity) : null 이 아닌 모든 참조 값 x, y, z 에 대해, x.equals(y) 가 true 이고 y.equals(z) 도 true 면 x.equals(z) 도 true
    • 일관성(consistency) : null 이 아닌 모든 참조 값 x, y 에 대해, x.equals(y) 를 반복해서 호출하면 항상 true 나 false 를 반환

    * 여기서 동치 관계는 두 객체가 논리적으로 같은 값을 가지고 있는 관계를 말합니다. (물리적으로 같은지 비교하는 것이 아님)

     


    동치성

     간단하게 이야기 하자면 A.equals(B) == true 이면 B.equals(A) == true 인 관계를 말합니다. 아래와 같이 하나의 좌표를 나타내는 Point 클래스가 정의되어 있습니다.

     

    Point.class

    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 obj) {
            if (!(obj instanceof Point)) {
                return false;
            }
            Point p = (Point) obj;
            return p.x == this.x && p.y == this.y;
        }
    }

     위의 Point 클래스는 equals() 메서드를 재정의하여 인스턴스의 값을 직접 비교하도록 하여 동치성이 지켜지도록 합니다.

        @Test
        public void simpleEqualsTest() {
            Point point_1 = new Point(1, 2);
            Point point_2 = new Point(1, 2);
    
            System.out.println(point_1.equals(point_2));	//true
            System.out.println(point_2.equals(point_1));	//true
        }

     추이성

     추이성은 A.equals(B) == true 이고 B.equals(C) == true 이면, A.equals(C) == true 이어야 한다는 뜻입니다.

    위에서 보았던 좌표를 나타내는 Point 클래스를 확장하여 색상까지 나타내는 ColorPoint 클래스가 아래와 같이 있습니다.

    public class ColorPoint extends Point {
        private Color color;
    
        public ColorPoint(int x, int y, Color color) {
            super(x, y);
            this.color = color;
        }
    
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ColorPoint)) {
                return false;
            }
            return super.equals(obj) && ((ColorPoint) obj).color == this.color;
        }
    }
    

     위 클래스에서 오버라이드된 equals() 메서드는 동치성을 위반합니다. 위와 같은 경우 Point 클래스와 서브 클래스인 ColorPoint 클래스를 서로 비교하면 다른 결과가 나오게 됩니다. Point 클래스의 equals() 메서드는 컬러 비교를 무시하고, ColorPoint 클래스는 비교하는 클래스의 종류가 다르다며 매번 false를 반환하기 때문입니다.

        @Test
        public void badTransitivityTest() {
            Point p = new Point(1, 2);
            ColorPoint cp = new ColorPoint(1, 2, Color.RED);
    
            System.out.println(p.equals(cp));   //true
            System.out.println(cp.equals(p));   //false
        }

     그렇다고 아래와 같이 ColorPoint의 equals() 메서드에서 비교 대상이 Point 클래스일 경우에 색상을 무시하도록 하면 대칭성은 지켜주지만, 추이성을 위반하게 됩니다.

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Point)) {
                return false;
            }
    
            if (!(obj instanceof ColorPoint)) {
                return obj.equals(this);
            }
            return super.equals(obj) && ((ColorPoint) obj).color == this.color;
        }
        @Test
        public void 추이성_위배() {
            ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
            Point p2 = new Point(1, 2);
            ColorPoint p3 = new ColorPoint(1, 2, Color.BLACK);
    
            System.out.println(p1.equals(p2));  //true
            System.out.println(p2.equals(p3));  //true
            System.out.println(p1.equals(p3));  //false
        }

     위와 같은 결과가 나타나는 이유는 p1과 p2, p2와 p3 비교는 좌표만들 가지고 비교하지만 p1과 p3는 색상까지 비교를 하기 때문입니다.

     

    이렇듯, 객체 지향적 추상화를 포기하지 않고, 구체 클래스의 하위 클래스에서 값을 추가하는 방법은 존재하지 않습니다. 대신 아래와 같이 상속하여 구체 클래스를 만드는 대신 컴포지션을 이용하여 우회하여 equals 규약을 지키면서 값을 추가할 수 있습니다.

    public class ColorPoint {
        private Point point;
        private Color color;
    
        public ColorPoint(int x, int y, Color color) {
            point = new Point(x, y);
            this.color = color;
        }
    
        public Point asPoint() {
            return this.point;
        }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ColorPoint)) {
                return false;
            }
            ColorPoint cp = (ColorPoint) obj;
            return cp.point.equals(this.point) && cp.color.equals(this.color);
        }
    }

     


    equals() 메서드를 구현하는 단계별 방법

     1. == 연산자를 사용하여 입력이 자기 자신의 참조인지 확인합니다. 입력 값이 자기 자신일 경우, 굳이 비교 연산을 하지 않아도 true 값이 반환될 것이 자명하므로 복잡한 비교 연산을 진행하지 않아도 됩니다.

     

    2. instanceof 연산자로 입력이 올바른 타입인지 확인합니다. 만약 그렇지 않다면 false를 반환합니다. equals에 주어지는 입력은 클래스가 될 수도 있지만, 그 클래스가 구현한 특정 인터페이스가 될 수도 있습니다. 이런 경우에는 인터페이스를 구현한 서로 다른 클래스끼리도 비교할 수도 있으므로 equals 규약을 수정하기도 합니다.

     

    3. 입력을 올바른 타입으로 형변환합니다. 앞 단계인 2번에서 instanceof 검사를 했기 때문에 이 단계는 100% 성공합니다.

     

    4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사합니다. 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들어서는 안됩니다.

     


    참고자료

    이펙티브 자바 3판 (인사이트)

    반응형

    댓글

Designed by Tistory.