HihoBookStudy / EffectiveJava

이펙티브 자바 북스터디입니다.
1 stars 0 forks source link

[item10] Abstract class 에서의 equals 규약 #15

Closed ForteEscape closed 2 months ago

ForteEscape commented 3 months ago

추상 클래스의 하위 클래스에서라면 equals 규약을 지키면서도 값을 추가할 수 있다. 상위 클래스를 직접 인스턴스로 만드는 것이 불가능하다면 지금까지 이야기한 문제들은 일어나지 않는다.

해당 지문이 잘 이해되지 않는데 상세한 설명이 가능한지 질문하고 싶습니다.

zpqmdh commented 3 months ago

우선 클래스에서는 구체 클래스와 추상 클래스가 존재합니다.

  1. 구체 클래스 (Concrete class) : 클래스 내의 메서드를 모두 구현한 클래스이며 new 키워드로 객체 생성이 가능합니다.
  2. 추상 클래스 (Abstract class) : 구체 클래스가 아닌 클래스이며 하나 이상의 미구현 메서드를 가지고 있으며 new키워드로 객체 생성이 불가능합니다.

책 60페이지에서는 이렇게 말하고 있습니다.

추상 클래스의 하위 클래스에서라면 equals 규약을 지키면서도 값을 추가할 수 있다. (중략) 상위 클래스를 직접 인스턴스로 만드는 게 불가능하다면 지금까지 이야기한 문제들은 일어나지 않는다.

해당 문단을 이해하기 위해 책에서 언급한 예제와 추상 클래스를 상속받는 새로운 클래스를 구현하여 비교해봅시다.

public class TransitivityTest {
    public enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
    static 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;
        }
    }
    static 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 Point)) 
                return false;
            if (!(o instanceof ColorPoint))
                return o.equals(this);
            return super.equals(o) && ((ColorPoint) o).color == color;
        }
    }
    public static void main(String[] args) {
        ColorPoint p1 = new ColorPoint(0, 0, Color.RED);
        Point p2 = new Point(0, 0);
        ColorPoint p3 = new ColorPoint(0, 0, Color.BLUE);
        System.out.println(p1.equals(p2)); // true
        System.out.println(p2.equals(p3)); // true
        System.out.println(p3.equals(p1)); // false
    }
}

/*
Point의 equals는 색을 무시하고, ColorPoint의 equals는 고려한다.
p1-p2, p2-p3의 비교에서는 색을 무시하였지만 p1-p3에는 고려하기 때문이다.
*/

위의 코드는 교재 내의 추이성 위배 사례로 언급된 코드입니다.

교재 58페이지에서 볼 수 있듯이, 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족할 방법은 존재하지 않습니다.

하지만 만약 Point를 추상 클래스로 선언한다면 Point의 객체는 생성할 수 없기 때문에 p2는 생성할 수 없게 됩니다. 그렇다면 Point를 상속받는 ColorPointSmellPoint 에서만 객체 생성을 할 수 있게 될 것이며, 그렇게 되면 동일 타입끼리의 equals 비교가 가능할 것입니다.

public class TransitivityTest {
    public enum Color { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }
    public enum Smell { GOOD, BAD, SOSO }
    static abstract class Point {
        @Override 
        abstract public boolean equals(Object o); // 하위 클래스에서 무조건 구현해야 하는 함수
    }
    static class ColorPoint extends Point {
        private final int x, y;
        private final Color color;
        public ColorPoint (int x, int y, Color color) {
            this.x = x;
            this.y = y;
            this.color = color;
        }
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof ColorPoint)) // 동일 타입이 아닐 경우 false
                return false; 
            if (((ColorPoint)o).x == this.x && ((ColorPoint)o).y == this.y && ((ColorPoint)o).color == this.color) {
                return true;
            }else {
                return false;
            }
        }
    }
    static class SmellPoint extends Point {
        private final int x, y;
        private final Smell smell;
        public SmellPoint (int x, int y, Smell smell) {
            this.x = x;
            this.y = y;
            this.smell = smell;
        }
        @Override
        public boolean equals(Object o) {
            if (!(o instanceof SmellPoint)) // 동일 타입이 아닐 경우 false
                return false; 
            if (((SmellPoint)o).x == this.x && ((SmellPoint)o).y == this.y && ((SmellPoint)o).smell == this.smell) {
                return true;
            }else {
                return false;
            }
        }
    }
    public static void main(String[] args) {
        ColorPoint p1 = new ColorPoint(0, 0, Color.RED);
        ColorPoint p2 = new ColorPoint(0, 0, Color.BLUE);
        ColorPoint p3 = new ColorPoint(0, 0, Color.RED);
        SmellPoint p4 = new SmellPoint(0, 0, Smell.GOOD);

        System.out.println(p1.equals(p2)); // false
        System.out.println(p1.equals(p3)); // true
        System.out.println(p1.equals(p4)); // false
    }
}
image

equals 메서드를 오버라이딩 하지 않는다면, 추상 클래스를 상속받는 하위 클래스를 생성조차 할 수 없게 됩니다. 그렇게 되면 하위 클래스들은 자신만의 equals 함수를 가지게 되고, 같은 타입일때만 동등성 비교를 하게 됩니다!

ForteEscape commented 3 months ago

부모 클래스가 Abstract라 객체 인스턴스를 생성하지 못한다면 애초에 부모 클래스 타입과의 instanceof 연산을 할 필요가 없으니 부모 타입으로 생성된 인스턴스인 경우를 생각하지 않아도 되서 확장시켜도 해당 타입과의 비교만 수행하면 된다는 것으로 이해했습니다.

상세한 설명 감사드립니다! 혹시 제가 이해한 것이 다른 것이라면 이야기 해주시면 감사하겠습니다!