기록이 힘이다.

[이펙티브 자바] 10. equals는 일반 규약을 지켜 재정의하라 본문

JAVA

[이펙티브 자바] 10. equals는 일반 규약을 지켜 재정의하라

dev22 2023. 3. 14. 15:16
728x90

핵심 정리: equals를 재정의 하지 않는 것이 최선

  • 다음의 경우에 해당된다면 equals를  재정의 할 필요가 없다.
  •  
  • 각 인스턴스가 본질적으로 고유하다. ex)enum
  • 인스턴스의 '논리적 동치성'을 검사할 필요가 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 적절하다.
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다. 

핵심 정리: equals 규약

  • 반사성: A.equals(A) == true
  • 대칭성: A.equals(B) == B.equals(A)
    • CaseInsensitiveString
package me.whiteship.chapter02.item10;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

// 코드 10-1 잘못된 코드 - 대칭성 위배! (54-55쪽)
public final 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;
    }

    // 문제 시연 (55쪽)
    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
//        CaseInsensitiveString cis2 = new CaseInsensitiveString("polish");
        String polish = "polish";
        System.out.println(cis.equals(polish));
//        System.out.println(cis2.equals(cis));

        List<CaseInsensitiveString> list = new ArrayList<>();
        list.add(cis);

        System.out.println(list.contains(polish));
    }

    // 수정한 equals 메서드 (56쪽)
//    @Override public boolean equals(Object o) {
//        return o instanceof CaseInsensitiveString &&
//                ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
//    }
}
  • 추이성: A.equals(B) && B.equals(C), A.equals(C)
    • Point, ColorPoint(inherit), CounterPointer, ColorPoint(comp)
package me.whiteship.chapter02.item10.inheritance;

import me.whiteship.chapter02.item10.Color;
import me.whiteship.chapter02.item10.Point;

// Point에 값 컴포넌트(color)를 추가 (56쪽)
public class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }

    // 코드 10-2 잘못된 코드 - 대칭성 위배! (57쪽)
//    @Override public boolean equals(Object o) {
//        if (!(o instanceof ColorPoint))
//            return false;
//        return super.equals(o) && ((ColorPoint) o).color == color;
//    }

//    // 코드 10-3 잘못된 코드 - 추이성 위배! (57쪽)
    @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;
    }

    public static void main(String[] args) {
        // 첫 번째 equals 메서드(코드 10-2)는 대칭성을 위배한다. (57쪽)
//        Point p = new Point(1, 2);
//        ColorPoint cp = new ColorPoint(1, 2, Color.RED);
//        System.out.println(p.equals(cp) + " " + cp.equals(p));

        // 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다. (57쪽)
        ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
        Point p2 = new Point(1, 2);
        ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
        System.out.printf("%s %s %s%n",
                          p1.equals(p2), p2.equals(p3), p1.equals(p3));
    }
}

새로운 필드를 추가하면 equals 규약을 만족시킬 방법은 존재하지 않는다.

package me.whiteship.chapter02.item10.inheritance;


import me.whiteship.chapter02.item10.Color;
import me.whiteship.chapter02.item10.Point;

import java.util.Set;

// CounterPoint를 Point로 사용하는 테스트 프로그램
public class CounterPointTest {
    // 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다. (58쪽)
    private static final Set<Point> unitCircle = Set.of(
            new Point( 1,  0), new Point( 0,  1),
            new Point(-1,  0), new Point( 0, -1));

    public static boolean onUnitCircle(Point p) {
        return unitCircle.contains(p);
    }

    public static void main(String[] args) {
        Point p1 = new Point(1,  0);
        Point p2 = new CounterPoint(1, 0);

        // true를 출력한다.
        System.out.println(onUnitCircle(p1));

        // true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
        System.out.println(onUnitCircle(p2));
    }
}
package me.whiteship.chapter02.item10.composition;


import me.whiteship.chapter02.item10.Color;
import me.whiteship.chapter02.item10.Point;

import java.util.Objects;

// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60쪽)
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);
    }

    /**
     * 이 ColorPoint의 Point 뷰를 반환한다.
     */
    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);
    }

    @Override public int hashCode() {
        return 31 * point.hashCode() + color.hashCode();
    }
}
  • 일관성: A.equals(B) == A.equals(B)
  • null-아님: A.equals(null) == false
package me.whiteship.chapter02.item10;

import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Timestamp;
import java.util.Date;

public class EqualsInJava extends Object {

    public static void main(String[] args) throws MalformedURLException {
        long time = System.currentTimeMillis();
        Timestamp timestamp = new Timestamp(time);
        Date date = new Date(time);

        // 대칭성 위배! P60
        System.out.println(date.equals(timestamp));
        System.out.println(timestamp.equals(date));

        // 일관성 위배 가능성 있음. P61
        URL google1 = new URL("https", "about.google", "/products/");
        URL google2 = new URL("https", "about.google", "/products/");
        System.out.println(google1.equals(google2));
    }
}

 

핵심 정리: equals 구현 방법

  • == 연산자를 사용해 자기 자신의 참조인지 확인한다.
  • instanceof 연산자로 올바른 타입인지 확인한다.
  • 입력된 값을 올바른 타입으로 형변환 한다.
  • 입력 객체와 자기 자신의 대응되는 핵심 필드가 일치하는지 확인한다.
  •  
  • 구글의 AutoValue 또는 Lombok을 사용한다.
  • IDE의 코드 생성 기능을 사용한다.
  •  

부동소수점은 float이나 double의 compare()을 통해 비교 아닌 것은 equals를 통해 비교하면 된다.

핵심정리: 주의 사항

-equals를 재정의 할 때 hashCode도 반드시 재정의하자.(아이템11)

너무 복잡하게 해결하지 말자.

-Object가 아닌 타입의 매개변수를 받는 equals 메서드는 선언하지 말자.