본문 바로가기
Language/Effective java

모든 객체의 공통 메서드

by y.j 2022. 6. 6.
728x90

INTRO

Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 명확히 정의되어 있다.일반 규약에 맞게 재정의 하지 않으면 규약을 준수한다고 가정하는 클래스(HashMap, HashSet 등)를 오작동하게 만들 수 있다.

 

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

 equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있어서 자칫하면 끔찍한 결과를 초래한다. 재정의 하지 않으면 클래스의 인스턴스는 오직 자기 자신과만 같게 된다.

 

재정의 하지 않는 상황

  • 각 인스턴스가 본질적으로 고유하다.
 값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스가 여기 해당된다. (Thread)

 

  • 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
 Pattern의 인스턴스가 같은 정규표현식인지 나타내는지 검사할 수 있지만, 설계자에 따라 필요하지 않을 경우 재정의 하지 않아도 된다.

 

  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
 Set 구현체는 AbstractSet에서 equals를 상속받아 쓰고, List는 AbstractList로부터 Map은 AbstractMap으로부터 상속받아 그대로 쓴다.

 

  • 클래스가 private이거나 package-private이면 equals메서드를 호출 할 일이 없다.

 

  • Enum은 인스턴스가 2개이상 만들어지지 않는다.

 

  • 값이 같은 인스턴스가 2개 이상 만들어지지 않음을 보장 할 경우

 

 

 

재정의를 해야 할 때

  • 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 경우 
 String, Integer같은 값 클래스 : 값의 비교가 필요하지 같은 객체인지 필요 없는 경우가 많다.

 

일반 규약

규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응하지 모른다.

반사성  null이 아닌 모든 참조 값 x에 대해 x.equals(x)는 true이다.
대칭성 null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 true이다.
추이성  null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true이면 y.equals(x)도 true이다.
일관성  null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)를 반복해서 호출하면 항상 true이거나 false이다.
null-아님 null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false이다.

 

  • 반사성 : 자기 자신과 같아야 한다는 뜻이다.

 

  • 대칭성 : 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다.

CaseInsensitiveString

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;
    }
}

main

public static void main(String[] args) {
    CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    String s = "Polish";
    System.out.println(cis.equals(s));                        // true
    System.out.println(s.equals(cis));                        // false

    List<CaseInsensitiveString> list = new ArrayList<>();
    list.add(cis);
    System.out.println(list.contains(s));                      // false
}

cis는 String의 존재를 알고 있지만, String은 CaseInsensitiveString의 존재를 모르고 있다. 따라서 대칭성에 어긋나고 이 객체를 사용하는 다른 객체들이 어떻게 반응 할 지 모른다.

 

해결방법

서로 false가 return된다.

@Override
public boolean equals(Object o) {
    return o instanceof CaseInsensitiveString &&
            ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

추이성 : 첫 번째 객체와 두 번째 객체가 같고 두 번째 객체와 세 번째 객체가 같다면 첫 번째와 세 번째도 같아야 한다.


Point

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 == x && p.y == y;
    }
}

ColorPoint 

대칭성 위배

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 obj) {
        if(!(obj instanceof ColorPoint))
            return false;
        return super.equals(obj) && ((ColorPoint) obj).color == color;
    }
}

추이성 위배

@Override
public boolean equals(Object obj) {
    if(!(obj instanceof ColorPoint))
        return false;
    
    if(!(obj instanceof ColorPoint))
        return obj.equals(this);
    
    return super.equals(obj) && ((ColorPoint) obj).color == color;
}

 

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);    // p1.equals(p2) -> true
Point p2 = new Point(1, 2);                         // p2.equals(p3) -> true
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);   // p1.equals(p3) -> false

리스코프 치환 법칙 위배

 * 어떤 타입에 중요한 속성이라면 하위 타입에서도 중요하다

@Override
public boolean equals(Object obj) {
    if(obj == null || obj.getClass() != getClass())
        return false;
    Point p = (Point) obj
    return p.x == x && p.y == y;
}

해결방법

상속 받지 않고 Point를 정의 한 후 각 속성을 equals로 비교한다.

public class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        this.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(point) && cp.color.equals(color);
    }
}

 

 

  • 일관성 : 두 객체가 같다면 앞으로도 영원히 같아야 한다는 뜻이다.

 

  • null-아님 : 모든 객체가 null가 같지 않아야 한다는 뜻이다.

obj == null은 불필요하다.

@Override
public boolean equals(Object obj) {
    if(obj == null)                         // 불필요하다.
        return false;
    if(!(obj instanceof ColorPoint))
        return false;
    ColorPoint cp = (ColorPoint) obj;
    return cp.point.equals(point) && cp.color.equals(color);
}

 

equals를 규약 지키면서 구현 하기

1. == 연산자를 사용해 자기 자신의 참조인지 확인한다.

성능에 도움을 준다.

 

2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.

 

3. 입력을 올바른 타입으로 형변환 한다.

 

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.

 

 

그 외 주의 할 점

  • float와 double
특수한 부동소수 값 등을 다뤄야 하기 때문에 compare메서드 사용하여 비교한다.

 

  • null도 정상 값으로 취급하는 참조 타입
Objects.equals(Object, Object)로 비교한다.

 

  • equals를 구현했다면 세가지를 꼭 확인하자!
대칭적인가?
추이성이 있는가?
일관적인가?

일반적인 예

@Override
public boolean equals(Object obj) {
    if(obj == this)                         
        return true;
    if(!(obj instanceof ColorPoint))
        return false;
    ColorPoint cp = (ColorPoint) obj;
    return cp.point.equals(point) && cp.color.equals(color);
}

  • equals를 재정의 할 땐 hashCode도 반드시 재정의하자.

 

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

 

  • Object외의 타입을 매개변수로 받는 equals메서드는 선언하지 말자.

 

728x90

'Language > Effective java' 카테고리의 다른 글

모든 객체의 공통 메서드  (0) 2022.06.07
모든 객체의 공통 메서드  (0) 2022.06.06
객체 생성과 파괴  (0) 2022.06.05
객체 생성과 파괴  (0) 2022.06.01
객체 생성과 파괴  (0) 2022.06.01

댓글