[Effective Java 3/E] 3장 모든 객체의 공통 메서드

대연.

·

2021. 2. 28. 21:47

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

기본 object를 상속하고 equals를 override 하지 않은 경우에는, Object의 equals를 상속하고 객체 식별성을 기준으로 판단한다.

String s1 = new String("a");
String s2 = new String("a");

s1 == s2; 
// false. 두 객체는 같은 "a"라는 문자열을 담고 있는 클래스지만 
// 각각 다른 인스턴스다.

s1.equals(s2); 
// true. 다른 객체지만 의미가 같으므로 논리적 동치성.

아래 규칙 중 단 하나라도 만족하면 재정의 하지 않도록 하자.

  1. 각 인스턴스가 본질적으로 고유하다.
  2. 값을 표현하는 게 아닌 동작하는 개체를 표현한다. thread는 equals가 성립할 수 없다.
  3. 인스턴스의 논리적 동치성을 검사할 일이 없다.
  4. 상위 클래스에서 재정의한 equals가 하위 클래스에 맞는 경우
  5. List 구현체들은 AbstractList로부터 상속 받아 쓰는데, 잘 작동한다
  6. 클래스가 private이거나 package-private(default access modifier)이고 equals 메서드를 호출할 일이 없다.
  7. @Override public boolean equals(Object o){ throw new AssertionError(); } //equals를 호출되지 않도록 하고싶다면 이렇게 하자!
  8. 값을 담고 있는 클래스라도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장할 때
  9. ex) enum과 인스턴스 통제 클래스(싱글톤). 이 때는 논리적 동치와 객체 식별성이 같은 의미가 된다.

그럼 언제 재정의 해야하나?

객체 식별성이 아니라 논리적 동치성을 확인하고 싶은데, 상위 클래스에서 equals 메서드를 재정의 하지 않았을 경우!

그러나 아래 규약들을 따라서 재정의 해야한다. 프로그래머가 의도한 대로 코드를 작성하기도 쉽지 않을 뿐더러, 인스턴스는 여기저기 불려 다닌다. equals() 메서드를 호출할 때, 아래의 규약들을 지킨다고 가정하고 동작하기 때문에, 꼭 지켜주어야 한다.

  1. 반사성(Reflexitivity) null이 아닌 모든 참조 값 x에 대해 x.equals(x)를 만족해야한다.
  2. 쉽게 쓰면 자기 자신이랑 같아야 한다!
  3. 대칭성(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 = Objects.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;
         }
    
             // 올바른 설계!
             // String 인스턴스와의 비교를 제거했다.
         @Override public boolean equals(Object o) {
             return (o instanceof CaseInsensitiveString) &&
                 ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
         }
    
     }
    
     CaseInsensitiveString a =new CaseInsensitiveString("Hi!");
     String b=  new String("hi!");
    
     a.equals(b); //true
     b.equals(a); //false
     //String class의 equals는 CaseInsensitiveString을 고려하지
     //않고 설계되어있다.
     //String과도 호환이 되도록 만드는 설계는 좋은 설계가 아니다.
    
  4. 내가 쟤랑 같으면 쟤도 나랑 같아야 한다.
  5. 추이성 (Transitivity) null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true이면 x.equals(z)도 true가 되야 한다는 조건이다.리스코프 치환 원칙(Liskov Substitution Principle)
    어떤 타입에서 중요한 속성은, 그 하위 타입에서도 마찬가지의 중요성을 띈다.
     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 == null || o.getClass() != getClass())
                     //getClass()는 해당 인스턴스의 클래스에 대한 메타데이터를 가지고 있다.
                 return false;
             Point p = (Point) o;
             return p.x == x && p.y == y;
         }
     }
    
     public class CounterPoint extends Point {
         private static final AtomicInteger counter =
                 new AtomicInteger();
    
         public CounterPoint(int x, int y) {
             super(x, y);
             counter.incrementAndGet();
         }
         public static int numberCreated() { return counter.get(); }
     }
    
     Set<Point> unitCircle = Set.of(
                     new Point( 1, 0), new Point( 0, 1),
                     new Point(-1, 0), new Point( 0,-1));
     unitCircle.contains(new CounterPoint(0,1); // false
    
     // CounterPoint와 Point의 getClass()는 다른 값을 반환한다.
    getClass(), instanceof도 작동하지 않는다면 어떤 대안이 있을까? 아이템 18에서 배우게 될 "상속 대신 컴포지션을 사용하라"를 참조하면 된다. Point를 상속하여 CounterPoint를 구현하는 대신에, Point를 ColorPoint의 private 필드로 두고 Point로 사용해야 할 경우, Point를 반환하는 View 메서드를 public으로 추가한다.java.sql.Timestamp는 java.util.Date를 확장하여 nanoSeconds 필드를 추가하였기에, 위에서 언급한 문제들이 발생한다. 섞어쓰지 않도록 주의하여야 하지만, 만약 무의식적으로 혹은 실수로 섞어 사용하였다면 디버깅이 매우 힘들다.
  6. 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); } // Point를 반환하는 view 구현 public Point asPoint() { return point; } @Override public boolean equals(Object o) { // 이젠 Point 인스턴스와 비교할 일은 없다. if (!(o instanceof ColorPoint)) return false; ColorPoint cp = (ColorPoint) o; // Point와 Color 고유의 equals를 분리하여 사용한다. return cp.point.equals(point) && cp.color.equals(color); } }
  7. 예를 들면, Point를 확장한 ColorPoint에서도 여전히 x,y좌표는 중요하며 ColorPoint는 Point로써 동작해야 한다.
  8. 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; } } public class ColorPoint extends Point { private final 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; } /* ColorPoint p1 = new ColorPoint(1, 2, Color.RED); Point p2 = new Point(1, 2); p1.equals(p2); //false : p2는 ColorPoint의 인스턴스가 아니기 때문에 p2.equals(p1); //true : Point에 정의된 equals를 따른다. */ // 잘못된 코드2 @Override public boolean equals(Object o) { if (!(o instanceof Point)) return false; // o가 일반 Point면 Point.equals()를 이용하여 비교한다. 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 : Point에 정의된 equals를 따른다. p2.equals(p3); //true : Point에 정의된 equals를 따른다. p3.equals(p1); //false : BLUE와 RED는 다르다. 추이성 위반 */ }
  9. 일관성 (consistency) null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.equals()의 판단에 신뢰할 수 없는 자원이 끼어들면 안된다. 신뢰할 수 없는 자원의 예는 java.net.URL이다. 주어진 URL에 매핑된 호스트 IP 주소를 이용해 비교하지만, 매번 같은 URL로 같은 호스트 IP주소를 받을 수 있음을 보장할 수 없다. 이를 피하기 위해서는 메모리 외부에 의존하지 않고 메모리에 존재하는 객체만을 사용한 결정적(Deterministic) 계산만 수행하여야 한다.
  10. 한 번 두 객체가 같다면, 어느 하나의 객체가 변하지 않는 한 영원히 일관성 있게 같아야 한다.
  11. null이 아닌 모든 x에 대해 x.equals(null)은 false다.equals()의 첫 번째 라인에 if(o == null) return false; 를 추가하여 쉽게 보호 가능하다.
  12. //explicit null pointer checking @Override public boolean equals(Object o) { if(o==null) return false; } //implicit null pointer checking @Override public boolean equals(Object o) { if(!o instanceof MyType)) return false; MyType mt = (MyType) o; } //instance of는 null이면 무조건 false를 반환한다.

올바른 equals() 메서드 구현하기

  1. == 연산자를 이용해 자기 자신의 참조인지 확인하기
  2. 비교작업이 많은 연산량을 요구할 때, 자기 자신과 비교하는 경우 빠르게 가지치기가 가능하다.
  3. instanceof 연산자로 입력이 올바른지 확인한다.
  4. 자기 자신의 클래스를 기준으로 삼는게 일반적이지만, 그 클래스가 implements한 인터페이스를 기준으로 삼기도 한다. 그렇게 하면 해당 인터페이스를 구현한 다른 클래스의 인스턴스와 비교를 가능케 한다.
  5. 입력을 올바른 타입으로 형변환한다.
  6. 2단계에서 문제가 생길 여지를 차단한다.
  7. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.primitive type은 ==로 비교하고, float과 double은 compare 메서드를 이용한다. equals()를 사용하지 않는 이유는 오토박싱으로 인한 성능 이슈가 발생할 수 있기 때문이다.null도 정상 값으로 취급하는 참조 타입 필드의 경우, Objects.equals()로 비교해 NullPointerException 발생을 예방하여야 한다.객체의 논리적 상태와 관련없는 동기화용 lock 필드 같은 것은 비교하면 안된다.
  8. 핵심 필드로부터 유도할 수 있는 파생 필드는 비교할 필요는 없지만, 만약 이 파생 필드가 전체 객체의 상태를 대변할 수 있다면 이쪽을 비교하는 것이 빠르다. Polygon 클래스의 경우, 모든 변과 정점을 일일이 비교하는 대신 캐시해둔 영역만 비교하면 결과를 빠르게 알 수 있다.
  9. 앞의 CaseInsensitiveString은 그 필드의 표준형(Canonical form)을 저장해두고 표준형끼리 비교하면 최적화를 할 수 있다. 또 다른 최적화 팁은 다를 가능성이 높은 필드부터 먼저 비교하는 방법이 있다.
  10. 배열의 모든 원소가 비교해야하는 핵심원소의 경우에는 Arrays.equals 메서드들 중 하나를 사용하다.
  11. 논리적 동치성에 포함되는 필드 모두를 비교한다. 하나라도 다르면 false를 반환하는데, 만약 2단계에서 인터페이스를 사용한다면 비교하고자 하는 인스턴스의 필드값을 가져올 때도 해당 인터페이스의 메서드를 사용해야 한다.

위의 규칙을 지켜 equals()를 구현했다면

  1. 대칭적인가?
  2. 추이성이 있는가?
  3. 일관적인가?
  4. 위 세가지 원칙을 테스트하는 unit test를 작성하여 돌려보자
  5. 혹은 equals 메서드를 AutoValue를 이용하여 작성했다면 테스트를 생략할 수도 있다.

마지막 주의상황

  1. equals()를 재정의 했다면 hashCode도 반드시 재정의하자
  2. HashSet과 같은 Set을 사용할 경우에 문제가 된다.
  3. 너무 복잡하게 해결하려 들지말자
  4. 필드들의 동치성만 검사해도 규약을 어기지 않고 쉽게 구현할 수 있다.
  5. 입력 타입은 반드시 Object여야 한다.
  6. public boolean equals(MyClass o) {} // X //overriding이 아닌 overloading이다. //@override annotation을 사용하면 방지할 수 있다. public boolean equals(Object o) {} // O

참고하기 : Google의 AutoValue 프레임워크

클래스에 annotation 하나만 추가하면 이 메서드들을 알아서 작성해준다.

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

equals를 재정의한 클래스는 hashCode도 재정의 해야한다. 그렇지 않다면 hash 기반 컬렉션 사용에 문제가 생긴다.

hashCode 일반 규약

  • equals 비교에 사용되는 정보가 변경되지 않았다면, hashCode 메서드는 어플리케이션이 실행되는 동안 일관된 값을 반환하여야 한다.
  • equals가 두 객체를 같다고 판단한다면, 두 객체의 hashCode는 똑같은 값을 반환하여야 한다.
  • equals가 두 객체를 다르다고 판단하더라도, hashCode가 서로 다른 값을 반환할 의무는 없다. 하지만 다른 값을 반환하게 되면 hash table의 성능이 좋아진다.

이상적인 해시 함수는 주어진 다른 인스턴스를 32비트 정수 범위에 균일하게 분배해야 하는데, 완벽할 순 없지만 좋은 hashCode를 작성하는 요령은 다음과 같다. (책의 순서와 다릅니다.)

해시코드 계산하는 방법

  • primitive type은 boxing class의 hashCode(f)를 수행한다.
  • 참조 타입 필드의 경우, hashCode를 작성하고 있는 클래스가 이 필드의 equals를 재귀적으로 호출하여 비교한다면, hashCode 또한 재귀적으로 호출하도록 짜야 한다.
  • 만약 재귀적인 계산이 복잡할 것 같으면, 표준형(Canonical representation)을 만들어 그 표준형의 hashCode를 호출한다.
  • 필드가 배열이라면, 핵심원소 각각을 별도 필드처럼 다루어야 한다. 모든 필드가 핵심 필드라면, Arrays.hashCode를 사용하고 그렇지 않은 경우 상수(일반적으로 0)을 사용한다.

hashCode() 구현하기

  1. int 변수 result를 해당 객체의 첫 번째 핵심 필드의 해시코드로 초기화 한다
  2. 나머지 핵심 필드에 대해서는 각 필드의 해시코드를 계산한 값 c를 가지고 다음 작업을 수행한다
  3. result = 31 * result + c;
  4. result로 반환한다.

구현 이후 주의사항

  1. 구현 이후에 이 메서드의 unit test를 작성해야 하는 것은 equals()와 같다.
  2. 파생 필드는 계산에서 제외해도 되지만, equals 비교에 사용되지 않은 필드는 '반드시' 제외해야 한다.
  3. 성능을 높이겠다고 해시코드를 생략한다면, 핵심 필드를 생략해서 안된다. 해시 품질이 떨어져 충돌이 많이 일어나게 된다. 같은 해시 버켓에 여러개의 엔트리가 있다면, 그 모든 버켓을 탐색하여야 하므로 해시 테이블의 속도가 O(n)에 가깝게 변할 것이다.
  4. hashCode() 의 생성 규칙을 API User에게 알려주지 않는다. 클라이언트가 이 값에 의지하여 코드를 작성하게 되면 추후 계산 방식이 바뀔 때 문제가 발생할 수 있다.

해시코드 TMI?

책에서 소개하는 31을 선택하는 이유는 다음과 같다.

  • 홀수이면서 소수(prime number)이다.
  • 시프트 연산과 뺄셈으로 대체하여 최적화하기 쉽다. 31⇒((1 << 5) - 1

납득이 가지는 않아서 찾아보았다. 사람들 궁금한건 전부 비슷한가 보다... https://stackoverflow.com/questions/299304/why-does-javas-hashcode-in-string-use-31-as-a-multiplier

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622

  • 요약하자면, 세 가지의 데이터셋을 가지고 해싱을 해보았는데 여러 소수와 다른 방식(WAIS, Weingberger 등)을 가지고 테스트 해 보았으나, hash함수 대신 true random generator를 가지고 나올 수 있는 것을 제외하고는 모두 비등비등한 성능을 보였지만, 31이 모든 아키텍쳐에서 빠르게 계산할 수 있다는 장점이 있어서 선택되었다는게 내용입니다.

-구현 파트의 2번에서, 필드를 곱하는 순서에 따라 result가 달라진다.

참조하기

  • Object 클래스의 정적 메서드 hash는 코드가 간결해지지만 느리다.
@Override public int hashCode(){
        return Objects.hash(field1, field2, ..., fieldn );
        //몇 개의 필드가 들어가도 상관업
}

ITEM 12. toString을 항상 재정의하라

일반적으로 상속받는 Object.toString은 클래스_이름@16진수로_표시한_해시코드를 반환한다.

toString의 규약은

  1. 간결하면서 읽기 쉬운 형태의 유익한 정보
  2. 모든 하위 클래스에서 이 메서드를 재정의하기

toString은 그 객체가 가진 주요 정보 모두를 반환하는 것이 좋다.

  • 디버그 용도로 사용할 때 숨겨진 정보 때문에 애를 먹을 것이다.

만약 그 크기가 너무 비대하다면 요약정보를 담자. ex) 대구 거주자 전화번호부(123456789개)

toString을 구현할 때, 반환값의 포맷을 문서화할 지 고민해야 한다.

장점

  1. 표준적이고 명확하며 사람이 읽기 쉽다
  2. 값 그대로 입출력에 사용하거나 CSV 파일처럼 사람이 읽을 수 있는 데이터 객체로 저장할 수도 있다.

단점

  1. 포맷을 한 번 명시하면, 클라이언트 코드에 의존성이 쌓여 그 포맷을 수정하기 어려울 것이다.

포맷을 명시하지 않더라도 의도는 주석을 통해 명확히 밝혀야 한다.

toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.

  • 개발자가 toString을 파싱해서 사용하는 참극이 벌어지지 않도록...

ITEM 13. clone 재정의는 주의해서 진행하라

Clonable은 복제해도 되는 클래스임을 명시하는 용도의 mixin interface지만, clone 메서드가 선언된 곳은 Object이고, protect인데 Cloanable은 상위 클래스에 정의된 protect clone 메서드를 변경하는 것인 아주 기형적인 형태다. Clonable을 구현하는 것 만으로는 외부 객체에서 clone 메서드를 호출할 수 없다. 이 Clonable 인터페이스는 clone의 동작 방식을 결정한다. 그 객체의 필드 하나하나를 복사한 객체를 반환한다.

Clonable을 구현한 클래스는 강제할 수 없고, 허술한 프로토콜을 지켜야 한다.

  1. x.clone() != x
  2. x.clone().getClass() == x.getClass()
  3. x.clone().equals(x)
  4. x.clone().getClass() == x.getClass() (상위 클래스가 이 규약을 따른다면 참이다.)

가변 상태를 참조하지 않는 클래스의 clone 메서드

        @Override public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

상위 클래스의 clone()이 구현이 잘 되어있다고 가정할 때, 이는 문제가 없는 코드다.

공변반환 타이핑을 통해 재정의한 메서드의 반환타입은 상위 클래스의 메서드가 반환하는 타입(Object)의 하위 타입(PhoneNumber)일 수 있다.

가변 상태를 참조하는 클래스의 clone 메서드

    @Override public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
                        // 배열 뿐 아니라 배열의 요소까지 복사해주어야 한다.
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

단순 array의 참조 변수만 복사한다면 불변식이 깨진다. 예를 들어서, 원본과 복제본이 있을 때, 복제본을 수정하면 원본이 수정이 된다. 그래서 각 elements를 Arrays.clone()을 사용하여 복제한다.

주의할 점은 elements 필드가 final이라면, 새로운 값을 할당할 수 없기 때문에 final 한정자를 제거하여야 할지 고민하여야 한다.

HashTable의 예제

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        public Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    @Override
    protected HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = buckets.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

복제는 자기만의 버킷 배열을 받지만, 다음 엔트리를 가르키는 next가 같기 때문에 같은 linked list를 공유한다.

이를 방지하기 위해 재귀적으로 연결 리스트를 복사하면 좋지만, 여러 문제가 많으니 반복문으로 작성하는 예시

public class HashTable implements Cloneable {
    private Entry[] buckets = ...;

    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        public Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
                Entry deepCopy() {
            Entry result = new Entry(key, value, next);
            for (Entry p = result; p.next != null; p = p.next)
                p.next = new Entry(p.next.key, p.next.value, p.next.next);
            return result;
        }
        }
        @Override
    protected HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = new Entry[buckets.length];
            for (int i = 0; i < buckets.length; i++) {
                if (buckets[i] != null)
                    result.buckets[i] = buckets[i].deepCopy();
            }
                        // 버킷을 순회하며 deepCopy를 통해 새로운 객체를 생성하여 연결해준다.
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

복잡한 가변 객체를 복제하는 방법

HashTable에선 저수준에서 일일이 복제하는 방법을 썼지만, 모든 key-value 쌍에 대해서 put(key,value)를 호출해주는 방법이 있다. 그러나 이 메서드는 final이나 private여야 한다. 재정의 될 수 있는 메서드를 호출하면 꼬이게 된다.

여러 주의사항

사용의 편의성을 위해서 재정의한 public clone 메서드에서는 throw절을 없애야 한다.

상속용 클래스는 Cloneable을 구현해서는 안된다.

  1. 제대로 작동하는 clone 메서드를 구현해 protected로 두고 Clone...Exception을 던질수 있다고 선언하는 방법(Object와 같다)
  2. clone을 동작하지 않도록 비워놓고 재정의하지 못하도록 final을 붙인다.

쓰레드 안전 클래스는 super.clone()외에 별 다른 할 일이 없더라도 재정의하여 동기화해 주어야 한다.

복사 생성자

public Yum(Yum yum) {...};

복사 생성자는 자기와 같은 클래스의 인스턴스를 인수로 받는다.

복사 팩터리

 public static Yum new Instance(Yum yum) {...};

복사 생성자를 모방한 정적 팩터리이다.

복사 생성자와 복사 팩터리는 장점이 많다.

  1. Clonable의 엉성한 규약에 기대지 않는다.
  2. final 필드를 굳이 수정할 필요가 없다
  3. 불필요한 검사 예외도 없고
  4. 형변환도 필요가 없다.
  5. 해당 클래스가 구현한 인터페이스 타입의 인스턴스도 인수로 받을 수 있다.

인터페이스 기반의 복사 생성자와 복사 팩터리의 이름은 변환 생성자와 변환 팩터리이다. (conversion constreuctor/factory). 이를 이용하면 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 맘대로 선택할 수 있다.( HashSet→TreeSet)

요약하기

가급적이면 배열 빼고는 Clonable 쓰지말자.

ITEM 14. Comparable을 구현할지 고려하라

compareTo는 제네릭 하며, equals와 다르게 순서까지 비교할 수 있다. Comparable을 구현한 것은 natural order가 있음을 뜻한다.

규약은 equals와 유사하다.

  1. 대칭성. 모든 x,y에 대해 x.compareTo(y) == -y.compareTo(x)를 만족한다.
  2. 추이성. Comparable을 구현한 클래스는 모든 x,y,z에 대해 x.compare(y) > 0 && y.compare(z) >0이면 x.compareTo(z) > 0이다.
  3. 반사성. Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))다.
  4. x.compareTo(y) == 0 && x.equals(y)은 true여야 한다.
  5. 타입이 다른 객체가 compareTo에 주어지면 ClassCastException을 던진다. 다른 타입 사이의 비교를 허용하고자 하면, 공통으로 구현한 인터페이스를 매개로 한다.

나머지 제약조건과 우회방식은 equals와 같다.

5번 규약의 중요성

new Decimal("1.0")과 new Decimal("1.00")이 있으면, 두 객체를 넣을 때, HashSet에서는 equals를 이용하여 비교하기 때문에 두 객체는 서로 다르다. 하지만 TreeSet에서는 compareTo 동치성을 판단하기 때문에 두 개가 같은 객체로 인식이 된다.

기본 타입 필드가 여럿일 때 비교하기


    public int compareTo(PhoneNumber pn) {
        int result = Short.compare(areaCode, pn.areaCode); // 가장 중요
        if (result == 0)  {
            result = Short.compare(prefix, pn.prefix); // 2번째로 중요
            if (result == 0)
                result = Short.compare(lineNum, pn.lineNum); //3번째로 중요
        }
        return result;
    }

비교자 생성 메서드를 활용하기

private static final Comparator<PhoneNumber> COMPARATOR =
            comparingInt((PhoneNumber pn) -> pn.areaCode)
                    .thenComparingInt(pn -> pn.prefix)
                    .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

처음 comparingInt를 호출할 때만 타입을 선언해주면 된다.

short보다 작은 타입은 int용을, 그 이외에는 long과 double 용을 사용하면된다.

값의 차이를 이용한 비교자는 추이성을 위반한다

static Comparator<Object> hashCodeOrder = new Comparator<>() {
        (Object o1, Object o2) -> o1.hashCode() - o2.hashCode();
}

정수 오버플로를 일으키거나 부동소수점 오류를 낼 수도 있고, 앞의 코드보다 빠르지도않다.

대신 아래 둘 중의 하나를 사용하도록 하자.

static Comparator<Object> hashCodeOrder = new Comparator<>() {
        (Object o1, Object o2) -> Integer.compare(o1.hashCode(), o2.hashCode())
}

static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

0개의 댓글