본문 바로가기

Language/Java Plus

Effective JAVA - Item10 : equals는 일반 규약을 지켜 재정의하라.

 

 

 

1. 3장 시작 : 모든 객체의 공통 메서드

- Object의 final이 아닌 메서드(eqauls, hashcode, toString, clone, finalize)는 모두 재정의를 염두해 두고 설계된 것임.

- Object를 상속하는 즉, 모든 클래스는 해당 메서드를 재정의해야 한다.

- 잘못 구현시 해당 메서드들을 재정의할 때, 지켜야할 규약을 잘 지켰다고 가정하는 클래스(HashMap, HashSet 등)에 오작동을 초래

 

 

2. Item10 : eqauls는 일반 규약을 지켜 재정의하라.

- 일반적으로, 재정의를 안하게 되면 자기 자신과 같은지만 비교, 즉 인스턴스가 같은지만 비교한다.

 

 

3. eqauls를 재정의하지 않아도 되는 상황

3-1. 각 인스턴스가 본질적으로 고유하다.

- 값을 나타내는 것이 아닌 동작 개체를 표현하는 클래스

- 예 : Thread

3-2. 인스턴스의 논리적 동치성을 고려할 필요가 없다.

3-3. 상위클래스에서 재정의한 eqauls가 하위 클래스에도 딱 들어맞는다.

- 예 : Set은 AbstractSet, List는 AbstractList, Map은 AbstractMap의 eqauls를 그대로 사용중이다.

3-4. 클래스가 private 이거나 private-package인 경우 equals를 사용할 일이 없다.

- 이러한 경우 eqauls 내부에 throw new AssertionError()를 작성해 확실히 하는 것이 좋다.

 

 

 

4. eqauls를 재정의 하는 상황

- 객체 식별성(두 객체가 물리적으로 같은가)를 비교하는 것이 아닌 논리적 동치성을 비교해야 하는데, 상위 클래스의 eqauls가 논리적 동치성을 비교하도록 재정의 되지 않았을 때 재정의한다.

- 주로 값 클래스들(String, Integer)이 해당 된다.

- 재정의를 해야 객체를 Map의 키, Set의 원소 등으로 사용할 수 있다.

- 값 클래스라도 값이 같은 인스턴스가 2개 이상 만들어지는 것을 방지하는 통제 클래스라면 굳이 작성할 필요가 없다.

(enum, String 등)

 

 

 

5. eqauls를 재정의하기 위한 규칙 -> 해당 규칙을 어긴다면 오류 발생 주의

5-1. 반사성

// 3-1. 반사성 : null이 아닌 모든 참조 값 x에 대하여 x.equals(x)는 true 이다.
// - 객체는 자기 자신과 같아야 한다.

5-2. 대칭성

// 3-2. 대칭성 : null이 아닌 모든 참조 값 x,y에 대하여 x.equals(y)는 true이면 y.equals(x)도 true이다.
// - 잘못된 예시
// -> equals는 대소문자를 무시함.
// -> 위치를 바꾸었을때, 같은 결과를 출력하지 않음
CaseInsesitiveString obj = new CaseInsesitiveString("Polish");
String s = "polish";
System.out.println(obj.equals(s));
System.out.println(s.equals(obj));
// - equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응하는지 알 수 없다.
// -> 어떤 JDK에서는 false를 반환하기도 함.
List<CaseInsesitiveString> list = new ArrayList<>();
list.add(obj);
System.out.print("포함하고 있는가? ");
System.out.println(list.contains(obj));
// - 이를 해결하기 위해서는 String의 equals와 연동되어야 하는데 말도 안되는 얘기이다.
package Item10;

import java.util.Objects;

public class CaseInsesitiveString {
    private final String s;

    public CaseInsesitiveString(String s){
        this.s = Objects.requireNonNull(s);
    }

//    @Override
//    public boolean equals(Object o){
//        if(o instanceof CaseInsesitiveString){
//            return s.equalsIgnoreCase(((CaseInsesitiveString) o).s);
//        }
//        if(o instanceof String){
//            return s.equalsIgnoreCase(((String) o));
//        }
//        return false;
//    }

    // 수정
    @Override
    public boolean equals(Object o){
        return o instanceof CaseInsesitiveString && ((CaseInsesitiveString) o).s.equalsIgnoreCase(s);
    }

}

 

5-3. 추이성

// 3-3. 추이성 : null이 아닌 모든 참조 값 x,y,z에 대하여 x.equals(y)는 true, y.equals(x)도 true이면 x.equals(z)도 true이다.
// 첫번째 객체와 두번째 객체가 같고 두번째 객체와 세번째 객체가 같으면 첫번째와 세번째 객체도 같아야 한다.

 

예시 : 최상단 클래스를 상속해 필드를 늘려가는 클래스 구현

- 최상단 클래스 : Point(x, y)

- 하위클래스 : ColorPoint(x, y, Color)

 

 

- 하위클래스끼리만 비교 : 대칭성이 위배 됨.

// 3-3-1. ColorPoint 객체끼리만 비교
// 대칭성 위배
// System.out.println(point1.equals(point2));
// System.out.println(point2.equals(point1));

- 둘의 결과가 같지 않음 : Point의 eqauls를 써서 ColorPoint와 비교시 같지만 ColorPoint의 eqauls를 사용해 비교시 다르다고 나옴

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

 

- 새로 생성된 필드는 비교 안함 : 추이성이 위배 됨.

// 3-3-2. 색상 무시 비교
// 추이성 위배
// 스택오버플로우도 발생가능 - 다른 하위클래스 만들어 해당 하위클래스와 equals하는 경우

- a=b, b=c a=c?가 성립이 안됨 : ColorPoint p1, Point p2, ColorPoint p3가 있을 때, 색상을 무시하고 x, y 좌표만 비교시 p1과 p2는 같고 p2와 p3는 같으므로 p1=p3여야하지만 실제 Color는 다름.

@Override
public boolean equals(Object o){
    // 아무곳에도 속하지 않는 경우 : false 반환
    if(!(o instanceof Point)){
        return false;
    }
    // Point지만 ColorPoint는 아닌 경우 : Color 빼고 비교
    if(!(o instanceof ColorPoint)){
        return o.equals(this);
    }
    // ColorPoint인 경우 : 색상까지 전체 비교
    return super.equals(o) && ((ColorPoint) o).color.equals(this.color);
}

 

 

- 최상단 클래스의 eqauls를 getClass()를 통해 구현 : 해당 방법은 리스코프 치환 원칙 위배

// 3-3-3. Point의 equals 메서드를 getClass()를 통해 처리
// - 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로 활용이 가능해야한다.
// - 위의 기본적인 조건이 위배됨
public boolean equals11(Object o){
    if(o == null | o.getClass() != getClass()){
        return false;
    }
    Point p = (Point) o;
    return p.x == this.x && p.y == this.y;
}
// 리스코프 치환 원칙 : 어떤 타입에 있어서 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다.
// 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 작동해야 한다.

- ColorPoint는 여전히 Point인데, 비교가 안됨

- 추가적으로 컬렉션들은 eqauls를 통해 비교하는데, 상위클래스로 정의된 컬렉션안에 하위 클래스를 넣을 수 없는 것이된다.

 

// 다른 예시
// - java.sql.Timestamp는 java.util.Date를 확장한 후 nanoseconds 필드를 추가하였다.
// - 결과적으로, Timestamp의 eqauls는 대칭성을 위배, Date의 객체와 섞여 한 컬렉션에 들어가면 동작 오류 발생
// - TimeStamp를 이렇게 설계한 것은 실수이니 따라하면 안된다.

 

 

- 결론

// 결론 : 구체 클래스를 확장해 새로운 값을 추가해가며 equals 규약을 만족시킬 방법은 존재하지 않는다.

 

 

- 우회방법

// 우회 방법
// "상속대신 컴포지션을 사용하라"
// - Point를 상속하는 대신 Point를 ColorPoint의 Private Field로 두고, ColorPoint와 같은 위치의
// 일반 Point를 반환하는 View 메서드를 public으로 추가하는 방식
// - 아래의 경우는 eqauls 규약을 지키면서 상위클래스에 값을 추가한 하위클래스를 만드는 경우이다.
// - 위에서 나온 문제들을 해당 클래스는 하위클래스가 아님을 명시하며 해소하는 느낌이다.
package Item10;

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

    public newColorPoint(Point point, Color color){
        this.point = point;
        this.color = color;
    }

    public Point asPoint(){
        return this.point;
    }

    @Override
    public boolean equals(Object o){
        if(!(o instanceof newColorPoint)){
            return false;
        }
        newColorPoint p = (newColorPoint) o;
        return p.point.equals(this.point) && p.color.equals(this.color);
    }
}

 

 

5-4. 일관성

// 3-4. 일관성 : null이 아닌 모든 참조 값 x,y에 대하여 x.equals(y)를 반복해도 호출해도 같은 값이 나온다.
// - 두 객체가 같다면 앞으로도 영원히 같아야 한다.
// - 가변 클래스는 비교 시점에 따라 다를 수 있지만 불변 클래스는 한번 다르면 끝까지 달라야한다.
// - 클래스가 불변이든 가변이든 eqauls의 판단에 신뢰할 수 없는 자원이 끼어들어서는 안된다.
// - equals 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.

 

 

 

5-5. null이 아님.

// 3-5. null이 아님 : null이 아닌 모든 참조값 x에 대하여 x.equals(null)은 false이다.
// - 모든 객체가 null과 같지 않아야 한다.
// if(0 == null)과 같은 코드는 필요하지 않다. if(!(o instanceof ClassName))으로 묵시적 검사를 진행한다.

 

 

 

 

6. equals 메서드를 구현하는 방법

// 4-0. equals의 매개변수는 Object로 설정한다.
// 4-1. == 연산자를 활용해 입력이 자기자신의 참조인지 확인한다.
// 4-2. instanceof 연산자로 입력이 올바를 타입인지 확인한다.
// 4-3. 입력을 올바른 형태로 형변환한다.
// 4-4. 입력 객체와 자기 자신의 대응하는 '핵심'필드들이 모두 일치하는지 하나씩 검사한다.
// (+) 필드 비교 방법
// - float, double 비교 : Float.compare(float, float), Double.compare(double, double) 사용
// (Float.equals, Double.equals는 오토박싱을 사용하기에 성능이 좋지 않음)
// - 나머지 기본 타입 : ==로 비교
// - 참조 타입 필드 : equals 메서드로 비교

// (+) null도 정상값으로 취급하는 참조 타입 필드
// - 정적 메서드인 Objects.eqauls(Object, Object)를 활용해 NullPointException을 예방하자.
System.out.println(Objects.equals(newP2, newP3));

// (+) 어떤 필드를 먼저 비교하느냐에 따라 성능차이 발생 가능
// - 다를 가능성이 더 크거나 비용이 싼 필드를 먼저 비교
// (그래야 비용이 비싼 필드는 나중에 비교하게 되고 속도가 향상 됨)
// - 동기화용 락 필드와 같이 객체의 논리적 상태와 관련없는 필드는 굳이 비교하지 않는다.

 

 

7. 최종 결론

// 최종 결론
// "equals를 다 구현했다면 세가지만 자문하자! 대칭성, 추이성, 일관성"

// 주의사항
// - equals를 정의할 때는 hashcode도 반드시 같이 정의하자.
// - 너무 복잡하게 해결하려 들지 말자.
// (예를들어, File 비교시 심볼릭 링크를 비교해 같은 파일을 가리키는지 확인할 필요가 없다.)
// - equals의 매개변수로는 무조건 Object로만 설정한다.
// - 사실, 자동생성 프로그램에 맡기는것이 실수도 적고 안전하다.

 

 

 

https://github.com/mokjaemin/EffectiveJAVA

 

GitHub - mokjaemin/EffectiveJAVA: Study Files Of Effective JAVA

Study Files Of Effective JAVA. Contribute to mokjaemin/EffectiveJAVA development by creating an account on GitHub.

github.com