BOOK 6 - 이펙티브 자바(3)
3장 모든 객체의 공통 메서드
Object는 기본적으로 상속해서 사용하도록 설계되었다.final이 아닌 메서드는 모두 오버라이드를 염두에 두고 설계된 것이라 재정의시 지켜야 하는 일반 규약이 명확히 정의되어 있다.equals,hashCode,toString,clone,finalize
equals는 일반 규약을 지켜 재정의하라
equals는 재정의하지 않으면 클래스의 인스턴스는 오직 자기자신과만 같게 된다.
재정의하지 않는 것이 좋은 상황
- 각 인스턴스가 본질적으로 고유하다.
- 값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스가 해당한다.
Thread가 좋은 예이다.
-
인스턴스의 ‘논리적 동치성’을 검사할 일이 없다.
- 상위 클래스에서 재정의한
equals가 하위 클래스에도 들어맞는다. - 클래스가 private 이거나 package-private 이고
equals메서드를 호출할 일이 없다. - 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스
- 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않으므로 사실상 논리적 동치성과 객체 식별성(object identity)이 같은 의미가 된다.
재정의해야하는 상황
- 객체의 물리적 동치성이 아니라 논리적 동치성을 확인해야 하고, 상위 클래스의
equals가 논리적 동치성을 비교하도록 구현되어있지 않았을 때이다.
재정의할 때 따라야하는 규약
null이 아닌 모든 참조 값 x, y, z에 대해,
- 반사성(reflexivity) :
x.equals(x) == true - 대칭성(symmetry) :
x.equals(y) == true이면y.equals(x) == true - 추이성(transitivity) :
x.equals(y) == true이고y.equals(z) == true이면x.equals(z) == true - 일관성(consistency) :
x.equals(y)를 반복해서 호출하면 항상 같은 값을 반환한다. - null 아님 :
x.equals(null) == false
대칭성 / 추이성 위반 예제
- 2차원에서의 점을 표현하는 클래스가 있다.
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;
}
}
- 위처럼
equals를 재정의하면 대칭성에 위배된다.Point를ColorPoint에 비교한 결과와 그 둘을 바꿔 비교한 결과가 다를 수 있다.
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.equals(cp) == true이고,cp.equals(p) == false이다.- 그러면
ColorPoint.equals에서Point객체와 비교할 때는 색상을 무시하도록 재정의하면 될까?
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
if (!(o instanceof ColorPoint)) return o.equals(this);
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 규약을 만족시킬 방법은 존재하지 않는다.
우회 방법 - 상속 대신 컴포지션 사용
- 구체 클래스의 하위 클래스에 값을 추가하는 방법 대신
Point를ColorPoint의 private 필드로 두고,Point를 반환하는 view 메서드를 public으로 추가하는 방식이다.
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);
}
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);
}
}
- 자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다.
java.sql.Timestamp는java.util.Date를 확장한 후nanoseconds필드를 추가했다.- 그 결과로
Timestamp의equals는 대칭성을 위배하며,Date객체와 한 컬렉션에 넣거나 섞어 사용하면 엉뚱하게 동작할 수 있다.
일관성 위배 예제
- 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다.
java.net.URL의equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다.- 호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없다.
- 이는
URL의equals가 일관성을 위배하게 한다.
- 이런 문제를 피하려면 equals는 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
null-아님 예제
- equals의 입력이 null인지를 명시적으로 확인하기보다는 입력받은 객체를 적절히 형변환하는 방식을 사용하는 것이 좋다.
// 명시적 null 검사
pulblic boolean equals(Object o) {
if(o == null) return false;
}
// 묵시적 null 검사
public boolean equals(Object o) {
if (!(o instanceof MyType)) return false;
MyType mt = (MyType) o;
...
}
equals가 타입을 확인하지 않으면 잘못된 타입이 인수로 주어졌을 때ClassCastException이 발생하게 된다.instanceof는 첫번째 피연산자가null이면 false를 반환하기 때문에 명시적으로 null 검사를 하지 않아도 된다.
좋은 재정의 방법
==연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.- 자기자신이면 true를 반환한다. (성능 최적화용)
instanceof연산자로 입력이 올바른 타입인지 확인한다.- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다.
float과double을 제외한 기본 타입 필드는==연산자로 비교한다.float->Float.compare(float, float),double->Double.compare(double, double)각각 정적 메서드로 비교한다.- 특수한 부동소수 값을 다루기 때문이다.
- 참조 타입 필드는
equals메서드로 비교한다.
- 어떤 필드를 먼저 비교하느냐가 성능을 좌우하기도 한다.
- 다를 가능성이 더 크거나 비교하는 비용이 더 싼 필드를 먼저 비교한다.
- 핵심 필드로부터 계산해낼 수 있는 파생 필드는 굳이 비교할 필요없지만, 파생 필드를 비교하는 쪽이 더 빠를 때도 있다.
- 동기화용 lock 필드 같이 객체의 논리적 상태와 관련 없는 필드는 비교하면 안된다.
좋은 재정의 방법을 따른 예제
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
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;
}
}
마지막 주의사항
- equals를 재정의할 땐 hashCode도 반드시 재정의하자
Object외의 타입을 매개변수로 받는equals메서드는 선언하지 말자.- 이것은
Object.equals를 재정의한 것이 아니라 다중정의한 것이다.
- 이것은
equals를 재정의하려거든 hashCode도 재정의하라
equals를 재정의한 모든 클래스에서hashCode도 재정의해야한다.- 그렇지 않으면 해당 클래스의 인스턴스를
HashMap이나HashSet같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
hashCode 규약
equals비교에 사용되는 정보가 변경되지 않았다면, 어플리케이션이 실행되는 동안 그 객체의hashCode는 항상 같은 값을 반환해야 한다.- 단, 어플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
equals가 두 객체를 같다고 판단했다면, 두 객체의hashCode는 같은 값을 반환해야 한다.equals는 물리적으로 다른 두 객체를 논리적으로 같다고 할 수 있다.Object의 기본hashCode메서드는 이 두 객체를 전혀 다르다고 판단하여, 재정의하지 않는다면 규약을 위반하게 된다.
equals가 두 객체를 다르다고 판단했더라도, 두 객체의hashCode가 서로 다른 값을 반환할 필요는 없다.- 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
두번째 규약 위배 예제
- 위에서 봤던
PhoneNumber클래스에hashCode메서드가 재정의되지 않았다면, 두번째 규약에 위배된다.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 876, 5309), "제니");
m.get(new PhoneNumber(707, 876, 5309)); // => "제니"가 나오길 기대하지만, 실제로 null을 반환한다.
- 여기에는 2개의 인스턴스가 사용되었다.
- 하나는 put 할 때 사용되었고, 하나는 get 할 때 사용되었다.
- 두 객체는 논리적으로 동치이지만,
hashCode를 재정의하지 않았기 때문에 서로 다른 해시코드를 반환하여 규약을 지키지 못한다.
좋은 hashCode 작성
- 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다.
- 이것이 세번째 규약이 요구하는 속성이다.
- 이상적인 해시 함수는 서로 다른 인스턴스들을 32비트 정수 범위에 균일하게 분배해야 한다.
-
int result를 선언한 후, 값 c로 초기화한다.- 이 때 c는 해당 객체의 첫번째 핵심 필드를 2.a 방식으로 계산한 해시코드다.
-
해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.
a. 해당 필드의 해시코드 c를 계산한다.
- 기본 타입 필드 :
Type.hashCode(f) - 참조 타입 필드 : 이 클래스의
equals메서드가 이 필드의equals를 재귀적으로 호출해 비교한다면, 이 필드의hashCode를 재귀적으로 호출한다.- 필드의 값이
null이라면 0을 사용한다.
- 필드의 값이
- 배열 : 핵심 원소 각각을 별도 필드처럼 다룬다.
- 이상의 규칙을 재귀적으로 적용해 각 핵심 원소의 해시코드를 계산한 다음, 2.b 방식으로 갱신한다.
- 모든 원소가 핵심 원소라면
Arrays.hashCode를 사용한다.
b. 2.a에서 계산한 해시코드 c로
result를 갱신한다.result = 31 * result + c
- 기본 타입 필드 :
-
result를 반환한다.
- 파생 필드는 해시코드 계산에서 제외해도 된다.
-
equals비교에 사용되지 않은 필드는 반드시 제외해야 한다. result값에 31을 곱하는 이유는 31이 홀수이면서 소수이기 때문이다.- 31을 이용하면, 이 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화할 수 있다. (
(i << 5) - i)
- 31을 이용하면, 이 곱셈을 시프트 연산과 뺄셈으로 대체해 최적화할 수 있다. (
적용 예제
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
Objects.hash
Objects클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인hash를 제공한다.- 이 메서드를 사용하면 앞서 구현한 코드와 비슷한 수준의
hashCode함수를 한 줄로 작성할 수 있다.- 속도는 더 느리다.
- 성능에 민감하지 않은 상황에서 사용하자.
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
lazy initialization
- 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기 보다 캐싱하는 방식을 고려하는 것이 좋다.
- 이 타입의 객체가 주로 해시의 키로 사용될 것 같다면, 인스턴스가 만들어질 때 해시코드를 계산해둬야 한다.
- 해시의 키로 사용되지 않는 경우라면,
hashCode가 처음 불릴 때 계산하는 지연 초기화 전략을 사용할 수 있다.
private int hashCode;
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
- 필드를 지연 초기화하려면 그 클래스를 Thread-Safe 하게 만들도록 신경써야 한다.
toString을 항상 재정의하라
- toString의 일반 규약에 따르면 ‘간결하면서 사람이 읽기 쉬운 형태의 유익한 정보’를 반환해야 한다.
- toString을 잘 구현한 클래스는 디버깅하기 쉽다.
- toString은 그 객체가 가진 주요 정보를 모두 반환하는게 좋다.
- 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다.
toString 포맷
- toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다.
- 포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋다.
- 포맷을 한번 명시하면 평생 그 포맷에 얽매이게 된다.
- 만약 다음 릴리즈에서 포맷을 바꾼다면, 이전에 이를 사용하던 코드들과 데이터들은 유효하지 않게 될 것이다.
- 포맷 명시 여부와 관계없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하는 것이 좋다.
clone 재정의는 주의해서 진행하라
Cloneable: 복제해도 되는 클래스임을 명시하는 mixin interface
Cloneable 인터페이스
- 이 인터페이스는
Object의clone메서드의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환한다.- 그렇지 않은 클래스의 인스턴스에서 호출하면
CloneNotSupportedException예외를 던진다.
- 그렇지 않은 클래스의 인스턴스에서 호출하면
clone 규약
- 이 객체의 복사본을 생성해 반환한다.
- ‘복사’의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다.
- 일반적인 ‘복사’의 의도는 다음과 같다.
- 일반적으로 다음 식은 참이지만, 반드시 만족해야 하는 것은 아니다.
x.clone() != xx.clone().getClass() == x.getClass()x.clone().equals(x)
- 관례상, 이 메서드가 반환하는 객체는
super.clone()를 호출해 얻어야 한다. - 관례상, 반환된 객체와 원본 객체는 독립적이어야 한다.
super.clone()
- 강제성이 없다면 점만 빼면 생성자 연쇄와 비슷하다.
clone메서드가super.clone()이 아닌 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 오류를 내지는 않을 것이다.- 그러나 이 클래스의 하위 클래스에서
super.clone()을 호출한다면 잘못된 클래스의 객체가 만들어져 하위 클래스의 clone 메서드가 제대로 동작하지 않을 것이다.
- 그러나 이 클래스의 하위 클래스에서
예제
- 제대로 동작하는
clone메서드를 가진 상위 클래스를 상속해Cloneable을 구현하고 싶다고 하자.
public class PhoneNumber implements Cloneable {
...
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
- 먼저
super.clone()을 호출한다.- 이렇게 얻은 객체는 완벽한 복제본이다.
- 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 이 객체는 완벽히 우리가 원하는 상태라 손볼 것이 없다.
- 그러나 쓸데없는 복사를 지양하는 관점에서 불변 클래스는 굳이
clone메서드를 제공하지 않는 것이 좋다.
Object의 clone 메서드는Object를 반환하지만,PhoneNumber의 clone 메서드는PhoneNumber를 반환하게 했다.- 재정의한 메서드의 반환 타입은 상위 클래스의 메서드의 반환 타입의 하위 타입일 수 있다.
- 이 경우 클라이언트가 형변환하지 않아도 된다.
- 이를 위해
super.clone()으로 얻은 객체를 반환하기 전에PhoneNumber로 형변환 하였다.
try-catch블록으로 감싸준 이유는 Object의 clone 메서드가 checked exception인CloneNotSupportedException을 던지도록 선언되었기 때문이다.
가변 객체를 참조하는 클래스
- 클래스가 가변 객체를 참조하는 순간 앞서의 구현은 프로그램을 망칠 수 있다.
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
...
}
- 이 클래스의
clone메서드가 단순히super.clone의 결과를 그대로 반환한다면 어떻게 될까?- 반환된 인스턴스의
size필드는 올바른 값을 가질 것이다. elements필드는 원본 인스턴스와 똑같은 배열을 참조할 것이다.- 즉, 원본이나 복제본 중 하나를 수정하면 다른 하나도 같이 수정되어 불변식을 해친다는 의미이다.
- 따라서 프로그램이 이상하게 동작할 수 있다.
- 반환된 인스턴스의
-
clone 메서드는 사실상 생성자와 같은 효과를 내기 때문에 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
Stack의clone메서드가 제대로 동작하려면 스택 내부 정보를 복사해야 하는데, 가장 쉬운 방법은elements배열의clone을 재귀적으로 호출하는 것이다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- 만약
elements필드가final이었다면 이 방식은 동작하지 않는다.- 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서
final한정자를 제거해야 할 수도 있다.
- 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서
deepCopy
clone을 재귀적으로 호출하는 것만으로 충분하지 않을 때도 있다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
...
}
Stack에서처럼 단순히 버킷 배열의clone을 재귀적으로 호출한다면, 복제본은 자신만의 버킷 배열을 가질 것이다.- 그러나 이 배열은 원본과 같은 연결 리스트를 참조하여, 원본과 복제본 모두 예기치 않게 동작할 가능성이 크다.
- 이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@Override
public 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();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
HashTable.Entry가 deep copy를 지원하도록 보강되었다.-
HashTable.clone은 먼저 적절한 크기의 새로운 버킷 배열을 생성한 다음, 원래의 버킷 배열을 순회하며 deep copy를 수행한다. Entry.deepCopy는 자신을 재귀적으로 호출한다.- 이 방식은 간단하며, 버킷이 너무 길지 않다면 잘 작동한다.
- 재귀 호출때문에 리스트의 원소 수만큼 스택 프레임을 소비하기 때문에 리스트가 길면 스택 오버플로우를 일으킬 위험이 있다.
- 재귀 호출 대신 반복자를 써서 순회하는 방향으로 수정할 수도 있다.
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;
}
요약 및 기타 주의사항
Cloneable을 구현한 Thread-safe한 클래스를 작성할 때는clone메서드 역시 적절히 동기화 해줘야 한다.Cloneable을 구현하는 모든 클래스는clone을 재정의해야 한다.- 접근 제한자는
public으로, 반환 타입은 클래스 자신으로 변경한다.
- 접근 제한자는
Cloneable을 구현한 클래스를 확장한다면 어쩔 수 없이clone메서드를 잘 구현해야 하지만, 그렇지 않은 경우는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.
public Yum(Yum yum) {...}
public static Yum newInstance(Yum yum) {...}
- 복사 생성자와 복사 팩터리는 clone 방식보다 나은 면이 많다.
- 언어 모순적이고 위험한 객체 생성 매커니즘(생성자를 쓰지 않는 방식)을 사용하지 않는다.
- 정상적인 final 필드 용법과도 충돌하지 않는다.
- 불필요한 checked exception을 던지지 않는다.
- 형변환이 필요치 않다.
- 또한 복사 생성자와 복사 팩터리는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
- 이를 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다.
- 예를 들어 컬렉션 구현체들은
Collection이나Map타입을 받는 생성자를 제공한다.
Comparable을 구현할지 고려하라
compareTo는Comparable인터페이스의 메서드이다.- 두가지를 제외하고
equals와 성격이 같다.
- 두가지를 제외하고
-
compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 의미한다.- 따라서
Comparable을 구현한 객체들의 배열은 다음처럼 정렬할 수 있다.
- 따라서
Arrays.sort(a);
- 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
compareTo 규약
- 이 객체와 주어진 객체의 순서를 비교한다.
- 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
- 비교할 수 없는 타입의 객체가 주어지면
ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 부호 함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.
-
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) - 추이성 :
x.compareTo(y) > 0 && y.compareTo(z) > 0이면x.compareTo(z) > 0 x.compareTo(y) == 0이면sgn(x.compareTo(z)) == sgn(y.compareTo(z))(x.compareTo(y) == 0) == (x.equals(y))- 이 사항은 필수는 아니지만 지키는 것이 좋다.
- 이 규약을 지키지 않은 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스에 정의된 동작과 엇박자를 낼 것이다.
compareTo는 타입이 다른 객체를 신경쓰지 않아도 된다.- 타입이 다르면
ClassCastException을 던져도 된다. - 다른 타입 사이의 비교도 허용하지만, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄진다.
- compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다.
- 타입이 다르면
equals와 비슷하게, 기존 클래스를 확장한 구체 클래스에서 새로운 필드를 추가했다면 compareTo 규약을 지킬 방법이 없다.- 우회법도
equals와 동일하게Comparable을 구현한 클래스를 확장하는 대신, 독립된 클래스를 만들고 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두도록 한다. - 그 다음 내부 인스턴스를 반환하는 view 메서드를 제공한다.
- 우회법도
compareTo 작성 요령
equals와 비슷하다.Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로,compareTo메서드의 인수 타입은 컴파일 타임에 정해진다.- 입력 인수의 타입을 확인하거나 형변환 할 필요가 없다.
- 각 필드의 순서를 비교한다.
- 기본 타입 필드 : 박싱된 기본 타입 클래스의 정적 메서드
compare메서드 호출 - 객체 참조 필드 :
compareTo메서드를 재귀적으로 호출Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야한다면Comparator를 사용한다.Comparator는 직접 만들거나 자바가 제공하는 것 중 골라쓰면 된다.
- 기본 타입 필드 : 박싱된 기본 타입 클래스의 정적 메서드
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
...
}
CaseInsensitiveString이Comparable<CaseInsensitiveString>을 구현했다.- 이는
CaseInsensitiveString참조는CaseInsensitiveString참조와만 비교할 수 있다는 의미이다.
- 이는
- 클래스에 핵심 필드가 여러개라면 가장 핵심적인 필드부터 비교한다.
- 비교 결과가 0이 아니라면 바로 그 결과를 반환한다.
- 비교 결과가 같지 않을 때까지 그 다음으로 중요한 필드를 비교해나간다.
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0) {
result = Short.compare(lineNum, pn.lineNum);
}
}
return result;
}
비교자 생성 메서드
- 자바 8에서는 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.
- 이 방식은 코드가 간결해지지만, 약간의 성능 저하가 발생한다.
public 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- 입력 인수 : 객체 참조를 int 타입 키에 매핑하는 키 추출 함수(key extractor function)
- 추출된 키를 기준으로 순서를 정하는 Comparator를 반환하는 정적 메서드이다.
- 예제에서는
PhoneNumber에서 지역코드를 추출하는 lambda를 인수로 받아 추출한 지역코드를 기준으로 순서를 정하는Comparator<PhoneNumber>를 반환한다.
thenComparingIntComparator의 인스턴스 메서드- int 키 추출 함수를 입력 받아 다시 Comparator를 반환한다.
객체 참조용 비교자 생성 메서드
comparing이라는 정적 메서드 2개가 다중정의 되어 있다.- 키 추출자를 받아서 그 키의 자연적 순서를 사용한다.
- 키 추출자 하나와 추출된 키를 비교할 비교자까지 2개의 인수를 받아 순서를 비교한다.
thenComparing이란 인스턴스 메서드 3개가 다중정의 되어 있다.- 비교자 하나만 인수로 받아 그 비교자로 순서를 비교한다.
- 키 추출자를 인수로 받아 그 키의 자연적 순서로 비교한다.
- 키 추출자 하나와 추출된 키를 비교할 비교자까지 2개의 인수를 받아 순서를 비교한다.
‘값의 차’로 비교하는 방식
- 이따금씩 값의 차를 기준으로 첫번째 값이 두번째 값보다 작으면 음수를, 같으면 0을, 크면 양수를 반환하는
compareTo나compare메서드를 발견할 것이다.
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Obejct o2) {
return o1.hashCode() - o2.hashCode();
}
};
- 이 방식은 정수 오버플로우를 일으키거나, 부동소수점 계산 방식에 따른 오류를 낼 수 있어 바람직하지 않다.
- 대신 다음 두 방식 중 하나를 사용하자.
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());