<aside> <img src="/icons/help-alternate_gray.svg" alt="/icons/help-alternate_gray.svg" width="40px" />

들어가기에 앞서

Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서 final이 아닌 메서드는 모두 오버라이딩를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 명확히 정의되어 있다. 메서드를 잘못 구현하면 대상 클래스가 이 규약을 준수한다고 가정하는 클래스를 오동작하게 만들 수 있다. 3장에서는 final이 아닌 Object 메서드들을 언제 어떻게 재정의해야 하는지를 다룬다.

</aside>

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

먼저 Object 명세에서 말하는 동치관계란? 두 객체가 서로 같은지(동등한지)를 정의하는 방법이다. 쉽게 말해, 동치관계는 객체를 비교할 때 논리적으로 같은지 여부를 확인할 기준을 제공하는 개념으로 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산으로 본다면 이 부분집합을 동치류(equivalence class; 동치 클래스)라 한다. equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다.

이제 동치관계를 만족시키기 위한 다섯 요건을 살펴보자. 1. 반사성

    > `null`이 아닌 모든 참조 값 x에 대해, `x.equals(x)`는 `true`다.
    > 
    
    단순히 말하면 객체는 자기 자신과 같아야 한다.
    
    ```java
    public class ProgrammingLanguage {
        private String name;
    
        public ProgrammingLanguage(String name) {
            this.name = name;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;  // 반사성 만족
            if (o == null || getClass() != o.getClass()) return false;
            ProgrammingLanguage that = (ProgrammingLanguage) o;
            return name.equals(that.name);
        }
    
        @Override
        public int hashCode() {
            return name.hashCode();
        }
    
        public static void main(String[] args) {
            Set<ProgrammingLanguage> set = new HashSet<>();
            ProgrammingLanguage language = new ProgrammingLanguage("java");
            
            set.add(language);
            System.out.println(set.contains(language));  // true 출력 (반사성 만족)
        }
    }
    
    ```
    
2. 대칭성
    
    > `null`이 아닌 모든 참조 값 x, y에 대해, `x.equals(y)`가 `true`면 `y.equals(x)`도 `true`다.
    > 
    
    두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
    
    ```java
    //잘못된 코드 - 대칭성 위배
    
    // 대칭성을 위반한 클래스
    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;
    		}
    		
    		...
    }
    ```
    
    위 코드는 `CaseInsensitiveString`의 `equals`는 `String`을 알고 있지만, `String`의 `equals`는 `CaseInsensitiveString`의 존재를 모른다는 데 있다. 대칭성을 명백히 위반한다.
    
    ```java
    CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
    String s = "polish";
    
    cis.equals(s); // true
    s.equals(cis); // false
    ```
    
    **해결 - `CaseInsensitiveString`끼리만 비교하도록 한다.**
    
    ```java
    //대칭성을 만족하게 수정
    @Override public boolean equals(Object o){
    		return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s); 
    }
    ```
    
3. 추이성
    
    > `null`이 아닌 모든 참조 값 x, y, z에 대해, `x.equals(y)`가 `true`이고, `y.equals(z)`도 `true`면 `x.equals(z)`도 `true`다.
    > 
    
    첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다.
    상위 클래스에 없는 새로운 필드를 하위 클래스에 추가하며 `equals`를 재정의할 때 자주 발생하는 문제다.
    
    예시로 살펴보자.
    
    ```java
    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 o) {
    				if(!o instanceof Point)
    						return false;
    				Point p = (Point) o;
    				return p.x == x && p.y == y;
    		}
    		
    		...
    }
    ```
    
    ```java
    public class ColorPoint extends Point {
    		private final Color color;
    		
    		public ColorPoint(int x, int y, Color color) {
    				super(x, y);
    				this.color = color;
    		}
    		
    		...
    }
    ```
    
    equals 메서드는 어떻게 해야 할까? 그대로 둔다면 Point의 구현이 상속되어 색상 정보는 무시한 채 비교를 수행한다.
    
    ```java
    //비교 대상이 또 다른 ColorPoint이고 위치와 색상이 같을 때만 true를 반환
    @Override public boolean equals(Object o) {
    		if(!o instanceof ColorPoint)
    				return false;
    		return super.equals(o) && ((ColorPoint) o).color == color;
    }
    
    //잘못된 코드 - 대칭성 위배!
    ```
    
    ```java
    Point p = new Point(1, 2);
    ColorPoint cp = new ColorPoint(1, 2, Color.RED);
    
    p.equals(cp); // true
    cp.equals(p); // false
    ```
    
    색상을 무시하도록 하면 해결될까?
    
    ```java
    @Override public boolean equals(Obejct 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;
    }
    ```
    
    ```java
    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, 추이성 위배
    ```
    
    이 방식은 대칭성은 지켜주지만, 추이성을 깨버린다.
    
    또한 이 방식은 무한 재귀에 빠질 위험도 있다.
    
    ```java
    //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;
    }
    ```
    
    ```java
    ColorPoint p1 = new ColorPoint(1,2, Color.RED);
    SmellPoint p2 = new SmellPoint(1,2);
    
    // 1. p1.equals(p2) 호출
    //    -> ColorPoint의 equals() 실행
    //    -> if(!(o instanceof ColorPoint)) 통과하지 못함, p2.equals(p1) 호출
    
    // 2. p2.equals(p1) 호출
    //    -> SmellPoint의 equals() 실행
    //    -> if(!(o instanceof SmellPoint)) 통과하지 못함, p1.equals(p2) 호출
    
    // 3. 다시 p1.equals(p2) 호출 -> 무한 반복... StackOverflow Error
    ```
    
    그럼 해법은 무엇일까? 사실 이 현상은 모든 객체 지향 언어의 동치관계에서 나타나는 근본적인 문제다. 구체 클래스를 확장해 새로운 값을 추가하면서 `equals` 규약을 만족시킬 방법은 존재하지 않는다. 객체 지향적 추상화의 이점을 포기하지 않는 한은 말이다.
    
    이 말은 얼핏, `equals` 안의 `instanceof` 검사를 `getClass` 검사로 바꾸면 규약도 지키고 값도 추가하면서 구체 클래스를 상속할 수 있다는 뜻으로 들린다.
    
    ```java
    @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;
    }
    ```
    
    위의 코드는 같은 구현 클래스의 객체와 비교할 때만 `true`를 반환한다.
    해당 코드는 리스코프 치환 원칙을 위배하기 때문에 잘못된 코드이다.
    
    해결 1- 상속 대신 컴포지션을 사용하라(`equals` 규약을 지키면서 값 추가하기)
    
    ```java
    public class ColorPoint{
    		private final Point point;
    		private final Color color;
    		
    		public ColorPoint(int x, int y, Color color) {
    				point = new Point(x, y);
    				this.color = Objects.requireNonNull(color);
    		}
    		
    		/* 이 ColorPoint의 Point 뷰를 반환한다. */
    		public Point asPoint(){ // view 메서드 패턴
    				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);
    		}
    		
    		...
    }
    ```
    
    `Point`를 상속하는 대신 `Point`를 `ColorPoint`의 *private* 필드로 두고, `ColorPoint`와 같은 위치의 일반 `Point`를 반환하는 뷰(view 메서드)를 *public*으로 추가하는 식이다.
    
    - `ColorPoint` vs `ColorPoint`: `ColorPoint`의 `equals`를 이용하여 color값까지 모두 비교
    - `ColorPoint` vs `Point`: `ColorPoint`의 `asPoint`를 이용하여 `Point`로 바꿔, `Point`의 `equals`를 이용해 x, y비교
    - `Point` vs `Point`: `Point`의 `equals`를 이용해 x, y값 모두 비교
    
    자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다.
    
    ex) `java.sql.Timestamp`: `java.util.Date` 확장 후 `nanoseconds` 필드 추가.
    → `Timestamp`의 `equals`는 대칭성을 위배하며, `Date`와 섞어 쓸 때 엉뚱하게 동작할 수 있다. 그래서 `Timestamp` API 설명에는 `Date` 와 섞어 쓸 때의 주의사항을 언급하고 있다.
    
    해결 2- **추상 클래스의 하위 클래스 사용하기**
    
    추상 클래스의 하위 클래스에서는 `equals` 규약을 지키면서도 값을 추가할 수 있다. 상위 클래스의 인스턴스를 직접 만드는 게 불가능하기 때문에, 하위 클래스끼리의 비교가 가능하다.
    
4. 일관성
    
    > `null`이 아닌 모든 참조 값 x, y에 대해, `x.equals(y)`를 반복해서 호출하면 항상 `true`이거나 `false`다.
    > 
    
    두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 앞으로도 영원히 같아야 한다.
    
    - 가변 객체의 경우 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있다.
    - 불변 객체는 한번 다르면 끝까지 달라야 한다.
    - 클래스가 불변이든 가변이든 `equals`의 판단에 신뢰할 수 없는 자원이 끼어들게해서는 안 된다.
    ex) `java.net.URL`의 `equals`는 주어진 URL과 매핑된 호스트의 IP주소를 이용해 비교하는데, 호스트 이름을 IP주소로 바꾸려면 네트워크를 통해야 하므로 그 결과가 항상 같다고 보장할 수 없다. → `equals`는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
5. **null**-아님
    
    > `null`이 아닌 모든 참조 값 x에 대해, `x.equals(null)`은 `false`다.
    > 
    
    모든 객체가 `null`과 같지 않아야 한다.
    
    ```java
    //잘못된 명시적 null 검사 - 필요 없다!
    @Override
    public boolean equals(Object o) {
    		if(o == null)
    				return false;
    		...
    }
    ```
    
    ```java
    //올바른 묵시적 null 검사 - 이쪽이 낫다.
    @Override
    public boolean equals(Object o) {
    		if(!(o instanceof MyType)) 
    				return false;		
    		MyType myType = (MyType) o;
    		...
    }
    ```
    
    동시성을 검사하려면 `equals`는 건네받은 객체를 적절히 형변환한 후 필수 필드들의 값을 알아내야 한다.
    따라서, 형변환에 앞서 `instanceof` 연산자로 입력 매개변수가 올바른 타입인지 검사해야 한다.
    입력이 `null`이면 타입 확인 단계에서 `false`를 반환하므로 `null` 검사를 명시적으로 하지 않아도 된다.


11)equals를 재정의하려거든 hashCode도 재정의하라

아이템 10의 연장선으로 해당 아이템의 제목을 지키지 않으면, hashCode의 일반 규약을 어기게 되어 클래스의 인스턴스를 컬렉션(HashMap, HashSet …)의 원소로 사용할 때 문제를 일으킬 것이다.