IT/Programming / / 2023. 4. 28. 09:18

<Effective Java> RULE 8 Equals를 재정의할 때는 일반 규약을 따르라

반응형

 

Equals 함수는 재정의하기 쉬워 보이지만 실수할 여지도 많고 그 결과는 끔찍하다고한다.

가장 간단한 방법은 Equals함수를 재정의하지 않는 방법인데 개인적으로 공통 함수로 제공되는 것을들을 그냥 사용하는것이 좋다고 생각한다. 꼭 필요한 경우에만 재정의 하도록 하자.

 

언제 Object.equals를 재정의를 해야하나?

1. 객체 동일성이 아닌 논리적 동일성의 개념을 지원하는 클래스일 때

2. 상위 클래스의 equals가 하위 클래스의 필요를 충족하지 못할 때

 

Equals 재정의가 필요없을 때

1. 싱글톤 패턴

2. 열거 자료형(Enum)

이러한 클래스는 객체 동일성이 곧 논리적 동일성이다.

Equals메서드는 동치관계를 구현한다.

 

동치관계1 - 반사성

null이 아닌 참조 x가 있을 때, x.equals(x)는 true를 반환한다. 모든 객체는 자기자신과 같아야 한다.

 

동치관계2 - 대칭성

null이 아닌 참조 x와 y가 있을 때, x.equals(y)는 y.equals(x)가 true일 때만 true를 반환한다. 두 객체에게 서로 같은지 물으면 같은 답이 나와야 한다는 것이다.

public class CaseInsensitiveString {
         private final String str;
 
         public CaseInsensitiveString(String str) {
                  if (str == null) {
                           throw new NullPointerException();
                  }
                  this.str = str;
         }
         @Override
         public boolean equals(Object o) {
                  if (o instanceof CaseInsensitiveString) {
                           return str.equalsIgnoreCase(((CaseInsensitiveString) o).str);
                  }
                  if (o instanceof String) { //한방향 정상동작!
                           return str.equalsIgnoreCase((String) o);
                  }
                  return false;
         }
}

CaseInsensitiveString cis = new CaseInsensitiveString(“Hello”);
String str = “Hello”;

//대칭성 위반 !
Cis.equals(str); - true
Str.equals(cis) – false
//Equals 메서드는 String객체에 대해는 알지만 
//String의 Equals는 CaseInsensitiveString이 뭔지 모른다.
 

** equals가 따라야 할 규약을 어기면, 그 객체를 만난 다른 객체들이 어떻게 행동할 지 예측할 수 없게 된다.

어떻게 방지해야 하나?

CaseInsensitiveString의 equals메서드가 String객체와 상호작용하지 않도록 해야한다.

 @Override   
public boolean equals(Object o){
       return (o instanceof CaseInsensitiveString) && 
        ((CaseInsensitiveString) o).str.equalsIgnoreCase(str);
}
 
동치관계3 - 추이성

null이 아닌 참조 x, y, z가 있을 때, x.equals(y)가 true이고 y.equals(z)가 true이면 x.equals(z)도 true이다. 첫 번째 객체가 두 번째 객체와 같고, 두 번째 객체가 세 번째 객체와 같다면 첫 번째 객체와 세 번째 객체도 같아야 한다는 것이다.

public class Point {
         private final int x;
         private final int y;
         public Point(int x, int y) {
                  // TODO Auto-generated constructor stub
                  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;
         }
 
}
 
//Point클래스를 상속받아 색상정보를 추가
class ColorPoint extends Point{
         private 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 ColorPoint))
                           return false;
                  return super.equals(o) && ((ColorPoint) o).color == color;
         }
         
}

Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
 
//대칭성 위반!
p.equals(cp) – true
cp.equals(p) - false
//Point를 ColorPoint와 비교하면 색상이 무시되고
//반대는 인자로 전달된 객체의 자료형 때문에 항상 False가 반환된다.
 
Point객체와 비교할 때 색상 정보는 무시하도록 한다면?

 

Point class ~
@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;
}


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
p2.equals(p3) – true
p1.equals(p3) – false 
//마지막 비교는 색상을 비교한다.
 
추이성을 해결하기 위한 방법?

객체 생성가능 클래스를 계승하여 새로운 값 컴포넌트를 추가하면서 equals 규약을 어기지 않을 방법은 없다.

대안 방법? 리스코프 대체 원칙 사용

어떤 자료형의 중요한 속성은 하위 자료형에도 그대로 유지되어서 그 자료형을 위한 메서드는 하위 자료형에도 잘 동작해야 한다는 원칙.

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

equals구현시 instanceof대신 getClass메서드를 사용하면 기존 클래스를 확장하여 새로운 값 컴포넌트를 추가하더라도 equals 규약을 준수할 수있다. 하지만 같은 클래스의 객체만 비교하게된다.

객체 생성 가능 클래스를 계승해서 새로운 값 컴포넌트를 추가할 만족스러운 방법은 없지만 문제만은 해결할 수 있는 방법은 계승하는 대신 구성하는 방법이다.

class ColorPoint{
         private final Point point;
         private final Color color;
 
         public ColorPoint(int x,int y,Color color) {
                  if(color == null){
                           throw new NullPointerException();
                  }
                  point = new Point(x,y);
                  this.color = 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);
         }
}


		ColorPoint p1 = new ColorPoint(1,2,Color.RED);
		Point p2 = new Point(1,2);
		ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
		
		System.out.println(p1.asPoint().equals(p2)); true
		System.out.println(p2.equals(p3.asPoint())); true
		System.out.println(p1.asPoint().equals(p3.asPoint())); true
 

 

Point를 계승해서 ColorPoint를 만드는 대신 ColorPoint안에 private Point필드를 두고 public 뷰 메서드를 하나 만드는 것이다.(규칙 5)

이 뷰 메서드는 ColorPoint가 가리키는 위치를 Point객체로 반환한다.

 

* 객체 생성 가능 클래스를 계승하여 값 컴포넌트를 추가한 클래스 예시

java.sql.Timestamp는 java.util.Date를 계승해서 nanoseconds를 추가한 것

Timestamp의 equals메서드는 대칭성을 위반하므로 Timestamp객체와 Date객체를 같은 컬렉션에 보관하거나 섞어쓰면 문제가 발생한다.

 

동치관계4 - 일관성

null 아닌 참조 x와 y가 있을 때, equals를 통해 비교되는 정보에 아무 변화가 없다면, x.equals(y) 호출결과는 호출 횟수와 상관없이 항상 같아야 한다. 일단 같다고 판정된 객체들은 추후 변경되지 않는 한 계속 같아야 한다는 것이다. 따라서 클래스를 구현할 때 변경 불가능하도록 구현해야 하는지 깊이 생각해봐야한다.(규칙 15)

 

신뢰성이 보장되지 않는 자원들을 비교하는 equals를 구현하는 것은 삼가야한다.

예)

java.net.URL의 equals는 URL에 대응되는 호스트의 IP주소를 비교하여 반환 : 호스트명을 IP주소로 변환하려면 네트워크에 접속해야 하므로 언제나 같은 결과가 나온다는 보장이 없다.

 

동치관계5 - Null에 대한 비 동치성

모든 객체는 null과 동치 관계가 아니다라는 요구조건하에 Null이 아닌 참조 x에 대해서, x.equals(null)은 항상 false다. 상당수 클래스는 equals안에서 null조건을 명시적으로 검사해서 예외를 발생하지 않도록 해야한다.

그러면 반드시 null체크를 해야하나?

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

 

인자와 비교하기 위해 equals메서드는 먼저 인자를 형변환 해야한다. 그래야 접근자 메서드를 호출하거나 필드를 검사할 수 있기 때문이다. 그러나 형변환 이전에, equals는 우선 instanceof 연산자를 이용해 인자의 자료형이 정확한지 검사해야 한다.

@Override   
public boolean equals(Object o){
       if( !(o instanceof ColorPoint))
             return false;
       ColorPoint cp = (ColorPoint) o;
}                
 

 

이런 형검사 코드가 없는 상태에서 엉뚱한 자료형의 객체가 equals메서드에 전달되면 결국 ClassCastException이 발생되어 equals규약이 깨지게 된다. 하지만 instanceof 연산자는 첫 번째 피연산자가 null이면 두번째 피연산자의 자료형에 상관없이 무조건 false를 반환한다. 따라서 equals에 null이 인자로 전될되어도 형 검사과정에서 false가 반환될 것이므로 null검사는 필요가 없다.

 

equals메서드를 구현하기 위한 지침

1. ==연산자를 사용하여 equals의 인자가 자기자신인지 검사하라

2. instanceof 연사자를 사용하여 인자의 자료형이 정확한지 검사하라

3. equals의 인자를 정확한 자료형으로 반환하라

4. “중요”필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사한다.

A. float나 double이외의 자료형은 ==연산자로 비교

B. float – Float.compare, double – Double.compare 사용(-0.0f나 Float.NaN상수)

5. equals메서드 구현을 끝냈다면 대칭성, 추이성, 일관성의 세 속성이 만족하는지 검토하라

 

equals메서드를 구현 시 주의사항

1. equals구현할 때는 hashCode도 재정의하라(규칙 9)

2. 너무 머리쓰려고 하지 마라

A. 필드들만 보고 동치성만 검사하면 equals규약을 준수하기 쉽다.

3. equals메서드의 인자 형을 Object에서 다른 것으로 바꾸지 마라

A. Overloding이 아니라 Override로 equals메서드 작성

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유