본문 바로가기

STUDY/이펙티브자바

3-1) equals는 일반 규약을 지켜서 재정의하라

equals 재정의

 

equals는 객체의 동일성을 비교하는 공통 메소드이다. 개발자는 필요에 따라서 자신이 개발한 클래스의 equals 메소드를 재정의 할 수 있다. 하지만 equals 메소드를 재정의할 경우 예기치 못한 문제가 생길 수 있다. 그렇기 때문에 아래의 경우 중 하나라도 해당된다면 메소드 재정의를 하지 않는 것이 옳다.

 

 

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

 

// Thread 인스턴스는 고유하기 때문에 thread1, thread2는 당연히 다르다.
Thread thread1 = new Thread();
Thread thread2 = new Thread();

// Customer 인스터스는 고객번호가 존재하고 customer1, customer2는 논리적으로 동일하다.
Customer customer1 = new Customer(00001);
Customer customer2 = new Customer(00001);

Thread 인스턴스는 시스템에서 실행중인 스레드를 나타내기 때문에 본질적으로 각각의 인스턴스가 고유하다. 이러한 경우 기본 Object의 equals 메소드로 충분하다. (=equals 메소드가 필요없다는 뜻이 아니다.)

 

 

2. 인스턴스의 논리적 동일성을 검사할 필요가 없다.

 

// 같은 정규식이지만 false가 나온다.
String pattern_str = "^[0-9]*$";
Pattern pattern1 = Pattern.compile(pattern_str);
Pattern pattern2 = Pattern.compile(pattern_str);
System.out.println(pattern1.equals(pattern2));

equals 메소드는 두 인스턴스 간의 논리적 동일성을 검사하는 메소드이다. 다르게 말해서 논리적 동일성을 검사할 필요가 없다면 재정의할 필요가 없다. Pattern 클래스의 경우 개발자가 패턴의 동일성을 검사할 필요가 없다고 생각해서 equals 메소드를 재정의하지 않았다.

 

 

3. 상위클래스에 정의한 equals를 사용할 수 있다.

 

equals 메소드는 Object 클래스부터 정의되어있고 상위클래스부터 하위클래스까지 상속된다. 따라서 상위 클래스에서 정의한 equals가 하위 클래스에도 사용할 수 있다면 equals 메소드를 재정의할 필요가 없다. 예를들어 Teacher라는 상위클래스가 있고 equals 메소드가 적절하게 정의되어있다면, MathTeacher 라는 하위 클래스에서 equals 메소드를 재정의할 필요는 없다.

 

 

4. 클래스가 private, package-private이고 equals를 호출 할 일이 없다.

 

클래스를 만들 때 공통 메소드를 재정의 하는 이유는 해당 클래스를 사용할 다른 개발자의 기대에 부응하기 위함이다. Java에서 모든 클래스는 equals 메소드를 가지고 있고 개발자들은 클래스의 인스턴스를 비교할 때 equals 메소드를 사용한다. 하지만 클래스가 클래스나 모듈의 외부에서 사용할 수 없도록 정의되어 있다면 해당 클래스를 다른 개발자가 사용할 수 없기때문에 equals 메소드를 재정의할 필요가 없다.

 

 


equals 일반규약

 

개발하는 클래스가 위의 경우에 해당하지 않고 논리적 동일성을 검사해야한다면 equals 메소드를 재정의해야한다. 이때 eqauls 메소드가 지켜야하는 규칙이 있다. 이를 equals 일반규약 이라고 부르고 그 내용은 아래와 같다.

 

1. 반사성: null이 아닌 모든값 x에 대해, x.equals(x)는 true이다.

2. 대칭성: null이 아닌 모든값 x, y에 대해, x.equals(y)가 true이면 y.equals(x)도 true이다.

3. 추이성: null이 아닌 모든값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true이면 x.equals(z)도 true이다.

4. 일관성: null이 아닌 모든값 x, y에 대해, 반복적으로 x.equals(y)를 호출해도 그 결과는 동일하다.

5. null이 아님: null이 아닌 모든값 x에 대해, x.equals(null)는 false이다.

 

+ 재밌는거

// Javascript에서 ==을 조심해서 사용해야 한다.
str = 'null'
console.log(str == null)

 

 


규약 1. 반사성

 

// equals 메소드를 잘 정의해야하는 이유
MyClass instance = new MyClass();
List<MyClass> lists = new ArrayList<MyClass>();
lists.add(instance);
lists.contains(lists);

반사성은 자신과 자신을 비교하면 다연히 true가 나와야한다는 말이다. 이 조건은 일부러 어기려고 하지 않는이상은 위반하기가 쉽지 않다.

 

 


규약 2. 대칭성

 

public final class CaseInsensitiveString {
    ...
    public boolean equals(Object o) {
        if (o instanceof String) 
            return s.equalsIgnoreCase((String) o);
        return false;
    }
    ...
}

// 실행결과 true, false가 나와서 대칭성을 위반한다.
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";

System.out.println(cis.equals(s));
System.out.println(s.equals(cis));

대칭성은 두 객체의 서로에 대한 비교 결과가 동일해야 한다는 것이다. 위 예제 코드에서 대소문자를 무시하는 클래스를 만들고 equals 메소드를 재정의 하였다. 재정의한 메소드는 CaseInsensitiveString 객체에서 String 객체를 비교할때는 true를 반환하지만, String 객체에서 CaseInsensitiveString 객체를 비교할때는 false를 반환하여 대칭성을 위반한다.


규약 3. 추이성

 

public class Point {
    private final int x;
    private final int y;
    ...
    public boolean equals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }
    ...
}

public class ColorPoint extends Point {
    private final Color color;
    ...
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
    ...
}

// 대칭성을 위반한다.
Point p = new Point(0,0);
ColorPoint cp = new ColorPoint(0,0,RED);

System.out.println(p.equals(cp));
System.out.println(cp.equals(p));

추이성이란 여러 객체를 차례로 비교하였을 때, 모두 true이거나 false라면 처음과 끝의 객체를 비교하였을 때 그 결과가 같아야한다는 것이다. 위의 예제를 보면 Point와 이를 상속한 ColorPoint 클래스는 대칭성을 위반하였다. Point와 ColorPoint를 비교할 때는 좌표만을 비교하는데 ColorPoint와 Point를 비교할 때는 좌표와 색상을 비교해서 결과가 달라진다. 그렇다면 ColorPoint와 Point를 비교할 때 좌표만을 보면 문제가 해결될까?

 

public class ColorPoint extends Point {
    private final Color color;
    ...
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        if (!(o instanceof ColorPoint))
	        return o.equals(this);
        return super.equals(o) && ((ColorPoint) o).color == color;
    }
    ...
}

// 추이성을 위반한다.
ColorPoint cp1 = new ColorPoint(0,0,RED);
Point p = new Point(0,0);
ColorPoint cp2 = new ColorPoint(0,0,BLUE);

System.out.println(cp1.equals(p));
System.out.println(p.equals(cp2));
System.out.println(cp1.equals(cp2));

ColorPoint와 Point를 비교할 때 좌표만을 보도록 했더니 추이성을 위반했다. cp1은 p이고, p는 cp2이지만 cp1은 cp2가 아니다. 그렇다면 어떻게 문제를 해결해야 할까? 정답은 "필드를 추가하면서 클래스를 상속하면 equals 규약을 지킬 수 없다"이다.

 

public class ColorPoint {
    private final Point point;
    private final Color color;
    ...
    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);
    }
    ...
}

하지만 상속을 사용하지않고 컴포지션을 사용하면 위의 문제들을 우회할 수 있다. 위의 문제들이 Color의 관점으로 비교할 때와 ColorPoint의 관점으로 비교할때가 혼재되어서 생긴것이다. 위와같이 상속이 아니라 컴포지션을 사용하면 개발자의 코드에 따라 특정 관점으로 비교를 수행할 수 있다.

 

 


규약 4. 일관성

 

일관성은 객체의 반복되는 비교 결과가 객체의 수정이 없는 이상 항상 같아야 한다는 것이다. 일반적으로 지키기 쉬운 규약이다. 하지만 equals 메소드 호출시마다 외부 자원을 가져와서 비교하는 로직이 존재한다면, 외부 자원의 상태에 따라서 결과가 달라질 수 있고 일관성을 위반하게 된다. 따라서 equals 메소드에서 비교하는 값은 실행중인 프로그랭의 메모리에 존재하는 값만을 비교하는것이 옳다.

 

 


규약 5. null이 아님

 

public boolean equals(Object o) {
    if (o == null)
        return false;
    if (!(o instanceof MyClass))
        return false;
    ...
}

마지막으로 null이 아닌 객체와 null을 비교하면 무조건 false를 반환해야 한다는 규약이다. 따라서 equals 메소드에는 null이 비교값으로 들어오면 false를 반환하는 조건이 들어가야한다. 하지만 이를 하나의 if문으로 만들필요는 없다. 클래스 검사 if문의 instanceof가 그 역할을 포함하고 있다.

 

 


equals 메소드 구현 방법

 

public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;
    ...
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNum == lineNum && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }
    ...
}

1. == 연산자를 사용하여 자기 자신이 비교값인지 확인한다.

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

3. 비교값을 올바른 타입으로 변환한한다.

4. 타입의 비교에 필요한 핵심필드를 모두 비교한다.

+ 마지막으로 대칭성, 추이성, 일관성을 검사해본다.

728x90