ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 자바 Itme 10 - equals는 일반 규약을 지켜 재정의 하라
    언어/Effective Java 2024. 5. 28. 21:46

     

    1. equals를 재정의 하면 안 되는 경우 

     

    1. 각 인스턴스가 본질적으로 고유할 때 

    • 값 클래스(Integer, String 처럼 값을 표현) 가 아닌 동작하는 개체를 표현하는 클래스(Thread)

    2. 인스턴스의 '논리적 동치성' 검사할 일이 없을 때 (동등성)

    • 논리적 동치성 검사의 1가지: Pattern의 인스턴스가 같은 정규 표현식을 나타내는 지 검사 

    3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 들어맞을 때 

    • 같은 특징을 가지면 equals를 상속받아 사용하는 걸 권장한다.

    4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없을 때

     

    // equals가 실수로 호출되는 걸 막고 싶다면
    @Override public boolean equals (Object o){
      throw new AssertionError();
    }

     


     

    2. equals를 재정의 해야하는 경우 

     

    객체 식별성 혹은 동일성 (x) -> 두 객체가 물리적으로 같은가

     

    논리적 동치성 혹은 동등성(o)

     

    객체 식별성이 아니라 논리적 동치성을 확인할 때, 상위 클래스의 논리적 동치성을 비교하도록 재정의 되지 않았을 때 

    (주로 값 클래스)

     


     

    3. Equals 메서드의 규약 - 동치관계

     

    동치 클래스 : 집합을 서로 같은 원소들로 이루어진 부분집합으로 나누는 연산 

    > equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환이 가능해야한다.

     

    3.1 반사성 (relexivity)

     

    • null이 아닌 모든 참조 값 x에 대해 x.equals(y)는 true이다. - 객체는 자기 자신과 같아야한다. 
    public class Fruit{
      private String name;
    
      public Fruit(String name){
        this.name = name;
      }
    
      public static void main(){
        List<Fruit> list = new ArrayList<>();
        Fruit f = new Fruit("apple");
        list.add(f);
        list.contains(f); // false일 경우에는 반사성을 만족하지 못하는 경우이다.
      }
    }
    • 생성한 객체가 다른 참조변수에 담기더라도 자기 자신과 같아야함 

     

    3.2 대칭성 (symmetry)

     

    • null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면, y.equals(x)도 true이다.
    / 대칭성을 위반한 클래스
    public final class CaseInsensitiveString{
      private final String s;
    
      public CaseInsensitiveString(String s){
        this.s = Obejcts.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;
      }
    }
    
    //대칭성을 만족하게 수정
    @Override public boolean equals(Object o){
      return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); // String에 대한 instanceof 부분을 빼고 구현한다.
    }

     

    3.3 추이성(trasitivity)

     

    • null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true면 y.equals(z)도 true면 x.equals(z)도 true이다)
    • 이 추이성 조건 떄문에, equals를 재정의하면 안도는 경우에 superclass에서 equals를 정의한 경우 언급한다.

     

    3.4 일관성 (consistency)

    • null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
      • 즉 , 두 객체가 같다면 수정하지 않는 한 앞으로도 영원히 같아야 한다.
    • 가변객체 = 비교 시점에 따라 서로 다를 수 있다.
    • 불변객체 = 한번 다르면 끝까지 다른다.
    • equals는 항상 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야한다.
    • 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들면 안된다.

    3.5 null이 아님

     

    > null이 아닌 모든 참조값 x에 대해, x.equals(null)은 false이다. 

    모든 객체는 null과 같지 않아야한다.

     

    명시적 null 검사

    @Override public boolean equals(Object o){
      if( o == null){
        return false;
      }
    }

     

    묵시적 null 검사 

    @Override public boolean equals(Obejct o){
      if(!(o instanceof MyType))     // instanceof 자체가 타입과 무관하게 null이면 false 반환함.
        return false;
      MyType mt = (MyType) o;
    }

     

    4. 부모 equals 재정의시 문제

     

    4.1 대칭성 위배 문제 

    // ColorPoint.java 의 equals
    @Override public boolean equals(Object o){
      if(!(o instanceof ColorPoint))
        return false;
      return super.equals(o) && ((ColorPoint) o).color == color;
    }
    
    public static void main(){
      Point p = new Point(1,2);  
      ColorPoint cp = new ColorPoint(1,2, Color.RED);
      p.equals(cp);    // true (Point의 equals로 계산)
      cp.equals(p);    // false (ColorPoint의 equals로 계산: color 필드 부분에서 false)
      
      //논리적으로 같다고 판단하기 위해 자식의 필드까지 따로 검사할 필요가 없을 때 
      // 오버라이딩을 통해 새로 정의하면 위와 같은 문제가 발생한다.
    }

     

     

    4.2 추이성 위배 문제 

    /ColorPoint.java의 equals
    @Override public boolean equals(Obejct 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(){
      ColorPoint p1 = new ColorPoint(1,2, Color.RED);
      Point p2 = new Point(1,2);
      ColorPoint p3 = new ColorPoint(1,2, Color.BLUE);
      p1.equals(p2);    // true (ColorPoint의 equals 비교 //2번째 if문에서 Point의 equals로 변환)
      p2.equals(p3);    // true (Point의 equals 비교 // x,y 같으니 true)
      p1.equals(p3);    // false (ColorPoint의 equals 비교)
      
      
      // 3객체는 논리적으로 같은 객체로 두고 싶은데 위와 같이 재정의 문제로 추이성의 문제 발생 가능하다.
    }

     

    4. 3 무한 재귀에 빠질 수 있다.

    //SmellPoint.java의 equals
    @Override public boolean equals(Obejct o){
      if(!(o instanceof Point))
        return false;
      if(!(o instanceof SmellPoint))
        return o.equals(this);
      return super.equals(o) && ((SmellPoint) o).color == color;
    }
    
    public static void main(){
      ColorPoint p1 = new ColorPoint(1,2, Color.RED);
      SmellPoint p2 = new SmellPoint(1,2);
      p1.equals(p2);
      // 처음에 ColorPoint의 equals로 비교 : 2번째 if문 때문에 SmellPoint의 equals로 비교
      // 이후 SmellPoint의 equals로 비교 : 2번째 if문 때문에 ColorPoint의 equals로 비교
      // 무한 재귀의 상태!
    }
    • 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
    • 그렇다고 instanceof 검사 대신 getClass 검사를 하라는 것은 아니다. 
    • 또한 리스코프 치환 원칙을 위배한다: Point 하위의 클래스는 정의상 여전히 Point이기 때문에 언제든 Point로 활용되어야함 

    5. equals 문제 해결 방법 

     

    5.1 상속 대신 컴포지션을 사용하라 

     

    public class ColorPoint{
      private final Point point;
      private final Color color;
    
      public Point asPoint(){ //view 메서드 패턴
        return point
      }
    
      @Override public boolean equals(Object o){
        if(!(o instanceof ColorPoint)){
          return false;
        }
        ColorPoint cp = (ColorPoin) o;
        return cp.point.equals(point) && cp.color.equals(color);
      }
    }

     

    • ColorPoint - ColorPoint equals를 이용하여 color 값까지 모두 비교
    • ColorPoint - Point -> ColorPoint를 asPoint()를 통해 Point로 바꿔서 equals하면, x,y만 비교 
    • Point - Point의 equlas를 이용하여 x,y값 비교 

    5.2 추상 클래스의 하위클래스 사용하기 

     

    추상클래스의 하위클래스에서는 equals 규약을 지키면서도 값을 추가할 수 있다.

    상위 클래스를 직접 인스턴스로 어차피 못만든다. 

     


    6. 양잘의 equals 메서드를 구현하는 4단계 

     

    6.1 ==연산자를 사용해 입력이 자기 자신의 참조인지 확인한다. 

     

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

     

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

     

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

     

     


    7. Equals 구현 시 추가 주의사항 

     

    1. 기본타입: ==연산자 비교 

     

    2. 참조타입 : equals() 비교

     

    3. float,double 타입 : Foat.compare(float,float), DOuble.compare(double,double) 비교 

     

    4. 배열 빌프 : 원소를 각각 지침대로 비교한다. / 모두 핵심 필드라면 Arrays.equals()를 사용 

     

    5. null 정상값 취급 방지 

     

    6. Object.equals(object,object)로 비교하여 NullpointException을 방지하자

     

    7. 필드의 표준형을 저장하자

     

    8. 비교하기 복잡한 필드는 필드의 표준형을 저장후 비교하자: 불변 클래스에 제격이다.

     

    9. 필드 비교 순서가 equals 성능을 좌우한다.

     

    10. 다를 가능서이 높은 필드 우선 비교하자 

     

    11. equlas 재정의할 땐 hashCode도 반드시 재정의하자

     

    12. Object 외의 타입을 매개변수로 받는 equals메서드 선언은 피라자 (재정의가 아닌 다중정의가 된다)

     


    참고자료 

    https://javabom.tistory.com/2

     

    아이템 [10] - equals는 일반 규약을 지켜 재정의하라

    1. equals를 재정의 하면 안되는 경우 equals는 재정의하기 쉬워보이지만 곳곳에 함정이 있다. 문제를 회피하는 가장 쉬운 길은 아예 재정의하지 않는 것이다. ㄱ. 각 인스턴스가 본질적으로 고유할

    javabom.tistory.com

    https://velog.io/@lychee/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-10.-equals%EB%8A%94-%EC%9D%BC%EB%B0%98-%EA%B7%9C%EC%95%BD%EC%9D%84-%EC%A7%80%EC%BC%9C-%EC%9E%AC%EC%A0%95%EC%9D%98-%ED%95%98%EB%9D%BC

     

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

    equals를 다 구현했다면 세 가지만 자문해보자.대칭적인가? 추이성이 있는가? 일관적인가?equals 메서드를 재정의하지 않고 그냥 두면, 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다. 각

    velog.io

     

Designed by Tistory.