메서드를 추가하고 싶어도 그 인터페이스의 구현체들을 모두 찾아 직접 수정해야 되기 때문이다. 자바 8 이후에는 이러한 불편을 해소하고자 인터페이스에 메서드 구현을 추가할 수 있도록 디폴트 메서드를 지원하기 시작했다.
디폴트 메서드를 선언하면, 그 인터페이스를 구현한 모든 클래스에서 재정의하지 않아도 사용할 수 있다. 기존 인터페이스에 메서드를 추가하는게 쉬어졌지만, 사실 구현체들은 인터페이스에서 새로운 메서드가 추가되었는지 알 수 없다. 그러니 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기가 어렵긴 마찬가지다.
자바 8 부터 Collection 인터페이스에 추가된 디폴트 메서드인 removeIf() 를 살펴보자. 이 메서드는 인자로 주어진 Boolean 함수(predicate)가 true 를 반환하는 모든 원소를 제거한다. 그런데 이 디폴트 메서드가 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다.
아파치 커먼즈 라이브러리의 org.apache.commons.collections4.collection.SynchronizedCollection 클래스는 java.util의 Collections.synchronizedCollection 정적 팩토리 메서드가 반환하는 클래스와 비슷하다. 아파치 버전은 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공합니다. 즉, 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스이다.
아파치의 SynchronizedCollection 클래스는 처음에 removeIf 메서드를 재정의하지 않고 있었다.
만약 재정의되지 않았던 옛날 버전의 클래스를 자바 8과 함께 사용한다면(removeIf 의 디폴트 구현을 물려받게 된다면), removeIf 의 구현이 동기화에 관해 아무것도 모르기 때문에 락 객체를 사용할 수 없게 된다. 따라서 SynchronizedCollection 인스턴스를 여러 쓰레드가 공유하는 환경에서 한 쓰레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.
그래서 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피하자.
새로운 인터페이스를 만드는 경우라면, 표준적인 메서드 구현을 제공하는데 유용한 수단이 될 수 있다. 이전 아이템에서 설명했듯이 해당 인터페이스를 활용하는 클라이언트도 여러 개 만들어서 의도한 용도에 맞게 잘 부합되는지 확인하자.
자바8 부터 인터페이스도 default 메서드와 static 메서드를 제공할 수 있게 되어 이제는 두 메카니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다. 자바8 의 인터페이스 기초 영상은 아래를 참고하자.
사실 큰 차이는 없어지고, 추상클래스의 단점만 부각되는 꼴이 되었다.
추상 클래스를 정의한 타입을 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 하는데 자바가 단일 상속만 지원하다 보니, 추상 클래스 방식은 새로운 타입을 정의하는데 커다란 제약이 있다.
반면, 인터페이스는 선언된 메서드들을 모두 구현하고 일반 규약을 잘 지킨다면 다른 어떤 클래스를 상속해도 같은 타입으로 취급된다. 그래서 기존 클래스에 새로운 인터페이스를 구현할 수 있다는 장점이 있다. 인터페이스의 장점에 대해 더 자세히 알아보자.
인터페이스의 장점
1 . 인터페이스는 믹스인 정의에 알맞다.
mixin 은 클래스 구현 타입으로 '주된 타입' 외에 선택적 기능을 혼합하여 제공함을 뜻한다. 예를 들어 Comparable 은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스이다. 추상 클래스는 기존 클래스를 덮어 씌워 계층 구조를 이루어야 하기 때문에 부가 기능을 혼합해서 제공하기가 힘들다. (상속은 완벽히 IS-A 관계를 가져야 함)
2 . 인터페이스로 계층구조가 없는 타입 프레임워크를 만들 수 있다.
타입을 계층적으로 정의하는 상속 구조가 개념들을 구조적으로 표현할 수 있지만, 상속은 캡슐화를 해칠 수 있다.
반면에 인터페이스는 아래와 같이 부가 기능들을 섞어 새로운 타입으로 만들 수 있다. 상속처럼 다른 클래스로 구현할 때 어떠한 제약이 없다.
같은 구조를 추상 클래스로 만들었다면 가능한 조합 전부를 각각의 클래스로 정의한 고도비만 계층구조가 만들어진다.
완벽한 IS-A 관계에 있어야 하므로, 속성이 N 개라면 지원해야 할 조합의 갯수는 2^N 개가 된다.
인터페이스의 디폴트 메서드
인터페이스 규약이 바뀌어 새로운 메서드를 추가 선언해야 된다고 한다면, 그 인터페이스를 구현한 모든 클래스에서 그 해당 메서드를 추가 구현해야 한다. 이런 문제점 때문에 자바 8 부터 디폴트 메서드를 제공하고 있다. 인터페이스 메서드 중 구현 방법이 명백한 것이 있다면 그 구현을 디폴트 메서드로 제공해도 된다. 참고로, 많은 인터페이스들이 equals 와 hashCode 같은 Object 의 메서드를 정의하는데 이들을 디폴트 메서드로 제공하면 안 된다.
인터페이스의 단점
인터페이스는 인스턴스 필드를 가질 수 없고 public 이 아닌 정적 멤버도 가질 수 없다.
인터페이스 + 추상 골격 구현 = 템플릿 메서드 패턴
인터페이스의 장점과 추상 클래스의 장점을 모두 취한 패턴이 템플릿 메서드 패턴이다. 인터페이스로 타입을 정의하고 골격 구현 클래스에서 나머지 메서드들까지 구현하면서 골격 구현을 확장하는 것만으로 인터페이스를 구현하게 되버리고 쉬어진다.
Item 18 에서 상속을 고려해두지 않고 설계한 클래스를 상속했을 때 문제점들을 살펴보았다.
이번 장에서 상속용 클래스를 설계하는 방법에 대해 자세히 살펴보자.
첫 번째, 메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨야 한다.
Item 18 에서 본 것처럼, 클래스 API 가 자신의 다른 메서드를 호출할 수 있다. 더 나아가 어떤 순서로 호출되는지, 각각의 호출 결과가 어떤 side-effect 를 일으키는지 호출했을 때 모든 상황을 문서로 남겨야 한다. 백그라운드 스레드나 정적 초기화 과정에도 호출되어 다른 영향을 끼칠 수 있으니 모두 작성하자.
API 문서의 메서드 설명 끝에 "Implementation Requirements" 로 시작되는 절을 볼 수 있는데, 이 부분이 바로 메서드의 내부 동작 방식을 설명하는 곳이다. @ImplSpec 태그를 메서드 주석에 붙여두면 자바독 도구가 자동으로 생성해준다.
java.util.AbstractCollection remove 메서드
java.util.AbstractCollection remove 메서드의 주석부분을 살펴보면 Iterator 메서드를 재정의하면 remove 메서드 동작에 영향을 미칠 수 있다는 것을 확실히 알 수 있다. "좋은 API 문서는 '어떻게' 가 아닌 '무엇을' 하는지 설명해야 한다" 라는 격언과 반대로 상속은 캡슐화를 해치기 때문에 이렇게 자세히 작성해두어야 한다.
두 번째, 클래스 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개하여야 한다.
하위 클래스에서 사용할 수 있도록 상위 클래스에서 미리 기능들을 제공할 필요가 있다.
java.util.AbstractList removeRange 메서드
java.util.AbstractList removeRange 메서드는 하위 클래스에서 부분 리스트의 clear 메서드를 고성능으로 제공하기 위해 만들어졌다고 한다. 하위 클래스에서 잘 사용할 수 있도록 메서드들을 미리 구현하여 protected 지시어로 제공하는데 어떤 메서드를 노출시킬지 결정하기가 쉽지 않다고 한다. 상속용 클래스의 하위 클래스들을 직접 만들어 보고 여러 개 만들었는데도 전혀 쓰이지 않는다면 private 이었어야 할 가능성이 크다. 이전 아이템에서 소개했듯이 상속용 클래스를 만들고 릴리즈하면 해당 클래스를 기반으로 하위 클래스들이 여러 개 생기므로, 반드시 하위 클래스를 만들어 검증해야 한다.
세 번째, 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출될 수 있다. 하위 클래스 생성과 동시에 로직이 잘못 실행될 수 있으므로 주의가 필요하다.
// 절대 해서는 안 된다.
public class Super {
public Super() { overrideMe(); }
public void overrideMe() {}
}
public final class Sub extends Super {
private final Instant instant;
Sub() { instant = Instant.now(); }
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args){
Sub sub = new Sub();
sub.overrideMe();
}
}
instant 가 두 번 출력된다고 기대하지만 첫 번째는 null 을 출력한다. 하위 클래스의 생성자가 해당 필드를 초기화하기도 전에 overrideMe 메서드가 불리기 때문이다.
네 번째, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
Cloneable 과 Serializable 인터페이스를 구현한 클래스는 설계하기가 더 까다롭다. clone 메서드는 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출할 수 있고, readObject 메서드는 하위 클래스의 상태가 미처 다 역직렬화하기 전에 재정의한 메서드를 호출할 수 있다. Serializable 인터페이스를 구현한 클래스가 readResolve 나 writeReplace 메서드를 갖는다면 이 메서드들은 private 가 아닌 protected 로 선언해야 한다.
가급적, 상속용으로 설계하지 않은 클래스는 상속을 금지하자.
클래스를 final 로 선언하거나 모든 생성자를 private 나 package-private 으로 선언하고, public 정적 팩토리 메서드를 만들어 두자. 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 준다.
만약 재정의 가능 메서드를 꼭 써야한다면, private 도우미 메서드로 옮기고 이 도우미 메서드를 호출하게끔 변경하자.
ITEM 17 과 마찬가지로 OCP(Open Close Priciple) 원칙에 대한 내용이다.
이 ITEM 을 확인하기 전에 위 우아한 테크코스 - OCP 와 전략패턴 영상을 보는 것을 추천한다.
OCP 원칙을 설명하면서 상속은 is-a 관계, 합성은 has-a 관계일 때 사용하여야 한 된다는 점과 템플릿 메서드 패턴과 전략 패턴의 확실한 차이도 확실히 배워갈 수 있다.
우아한 테크코스 영상은 초보 프로그래머가 알아야 되는 내용들을 잘 설명해준다.
이번 장에서는 상속과 비교하면서 합성 연관관계를 권장하고 있다.
상속을 사용하면 상위 클래스와 하위 클래스 개념이 생긴다.
상위 클래스가 버전이 릴리즈 될 때마다 내부 구현이 달라지면 그 여파로 하위 클래스가 오동작할 수 있다.
그래서 상위 클래스가 충분히 확장을 고려하지 않은 상태로 구현되면 캡슐화가 깨질 수 있다.
상속이 잘 되려면...
1 . 상위 클래스와 하위 클래스가 한 프로그래머가 통제하여야 상위 및 하위 클래스가 변경될 시 그 side-effect 를 잘 알고 같이 수정할 수 있다.
2 . 확장할 목적으로 설계되어 있어야 하고 문서화도 잘 되어 있어야 한다.
3 . 상위 클래스와 하위 클래스가 완벽히 IS-A 관계에 있어야 한다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
위와 같이 HashSet 이 처음 생성된 이후로 아이템이 몇 개가 더해졌는지 알기 위해 상속을 사용해서 addAll 과 add 메서드를 Override 했다고 생각해보자.
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
재정의한 addAll 메서드를 호출하면 3을 반환하는 것이 아니라 6을 반환한다.
원인은 addAll 메서드는 add 메서드를 사용해서 구현하기 때문이다. add 함수에도 addCount 변수를 1증가시키고 있으니 6이 반환되는 것이다.
이런 내부 구현 방식은 HashSet 문서에 나와 있지 않다. HashSet 클래스를 만든 프로그래머가 이런 사실을 공유하지 않는다면 잘못된 방식으로 구현할 수 있다는 것을 보여준다. 심지어 HashSet 클래스의 addAll 메서드가 다음 릴리즈 때에도 구조가 유지될 것이라는 보장이 없다.
다시 메서드를 재정의한다면 되지 않을까?...
상위 클래스의 private 변수를 써야하는 상황이라면 아예 불가능하기도 하고, 상위 클래스의 메서드를 다시 재정의 해야 될 수도 있다. 성능, 상속을 한 이유가 사라짐, 오류 발생할 가능성 등 다양한 문제점이 생길 수 있다.
또 메서드를 재정의했는데 상위 클래스에서 추가한 메서드의 이름과 반환 타입이 같다면 컴파일조차 되지 않는다.
합성을 사용하자
새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조한다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 점에서 이러한 설계를 합성(composition)이라고 한다.
그리고 새로운 클래스의 메서드에서 기존 클래스에 대응되는 메서드를 호출하여 그 결과를 받는다.
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
... (생략) ...
public boolean add(E e) { return s.add(e); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
}
위와 같이 Set 을 클래스 내 변수로 생성해서 Set 의 메서드를 호출하는 전달 메서드를 정의하면 상위 클래스의 메서드 변경에도 유연하게 대처할 수 있다.
ITEM 16 "public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라"
class Point {
public double x;
public double y;
}
간혹 이렇게 데이터 필드 값들을 모아놓은 클래스들을 본 적이 있을 것이다.
데이터 전달용(DTO)이나 값 객체, 데이터 복사용 객체로 클래스의 기능이 많이 퇴보하긴 했지만 자주 사용한다.
이런 public 클래스를 선언할 때 필드 값들을 public 지시어로 두면 데이터 필드에 직접 접근할 수 있어 캡슐화의 이점을 제공하지 못한다. 불변식을 보장할 수 없으며 API 를 수정하지 않고는 내부 표현을 바꿀 수 없다. 외부에서 필드에 접근할 때 부수 작업을 수행할 수도 없다는 점도 크다.
class Point {
private double x;
private double y;
public Point (double x, double y) {
this.x = x;
this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public void setX(double x) { this.x = x; }
public void setY(double y) { this.y = y; }
}
Public 클래스라면 getter 와 setter 접근자 메서드를 제공하고 내부 필드들은 private 로 감추자. 클래스 내부 표현 방식을 유연하게 바꿀 수 있다는 장점이 있다. package-private 클래스나 private 클래스라면 데이터 필드를 노출하더라도 문제가 없다. 같은 패키지안에서 사용하거나, 톱레벨 클래스에서만 접근하니 괜찮다.
예를 들어 위와 같이 회원 가입에 필요한 서비스들("회원 가입", "회원 탈퇴", "회원 검색")을 나누어서 인터페이스로 제공하고 있다면, 각 팀원들이 한 가지 기능들을 맡아 개발할 수 있다.
2 . 시스템 관리 비용을 낮춘다.
소스코드 한 줄로 기능들이 뒤 섞여 있는 것보다 각 컴포넌트를 빨리 파악하여 디버깅 할 수 있고, 다른 컴포넌트로 교체하는 부담도 적다.
위 예시에서 "회원 검색" 에 대한 요구사항이 변경되었을 경우, getMembers 인터페이스의 구현체의 로직 일부분을 변경하기만 하면 된다.
3 . 성능 최적화에 도움을 준다.
정보 은닉 자체가 성능 향상이 되지는 않지만, 프로파일링해서 최적화할 컴포넌트를 정해서 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화 할 수 있다.
속도가 더 빠른 알고리즘으로 바꾼다던지, 입력/수정/읽기 세션의 크기를 보고 아키텍처를 변경할 수도 있다. 독립적이기에 가능하다.
4 . 소프트웨어 재사용성을 높인다.
다른 컴포넌트에 의존하지 않고 독자적으로 동작할 수 있는 컴포넌트라면 다른 곳에서도 충분히 재사용할 수 있다.
5 . 큰 시스템을 제작하는 난이도를 낮춰준다.
시스템 전체가 만들어지지 않아도 개별 컴포넌트의 동작을 검증할 수 있다. Mockito 를 이용해 가짜 객체를 생성하고 그 객체를 통해 테스트 케이스 작성이 가능해진다. DIP 원칙을 통해 인터페이스로 접지한 객체를 DI(dependency injection) 해서 독자적인 테스트 케이스를 만들 수도 있다.
public class MemberTest {
private static class Member {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
@Mock
Member member;
@Test
public void 멤버별_테스트() {
Member member = mock(Member.class);
assertTrue(member != null);
when(member.getName()).thenReturn("gold-egg");
when(member.getAge()).thenReturn(29);
assertTrue(member.getName() == "silver-egg");
assertTrue(member.getAge() == 30);
}
}
위와 같이 Member 에 대한 클래스만 따로 테스트 하는 것을 볼 수 있다.
정보 은닉은 모든 클래스와 멤버의 접근성을 가능한 좁히는 쪽으로 설계하면 자연스레 안전한 캡슐화가 된다.
자바에서는 아래와 같이 접근 제한자를 제공하고 있으며 클래스 (및 인터페이스), 변수, 메서드등 범용적으로 사용할 수 있다.
JAVA 의 접근제한자
private: 멤버를 선언한 top-level 클래스에서만 접근할 수 있다. package-private: 멤버가 소속된 패키지 안의 모든 클래스에서 접근 할 수 있다. protected: package-private의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스에서도 접근할 수 있다. public: 모든 곳에서 접근 할 수 있다.
클래스와 인터페이스 내에서 접근 제한 사항
top-level 클래스와 인터페이스는 package-private 과 public 두 가지이고 public 으로 선언하면 공개 API 가 되며, package-private 으로 선언하면 해당 패키지 안에서만 이용이 가능하다.
패키지 외부에서 사용할 것이 아니라면 package-private 로 선언하자. 반면 public 으로 선언한다면 API 가 되므로 하위 호환을 위해 영원히 관리해주어야 한다.
한 클래스에만 사용하는 private-package top-level 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static 으로 중첩시키자.
public 일 필요가 없는 클래스의 접근 수준을 package-private top-level 클래스로 좁히는 일이다. public 클래스는 그 패키지의 API 인 반면, package-private 톱레벨 클래스는 내부 구현에 속하기 때문이다.
만약, 공개 API 를 제외한 나머지 멤버들은 private 로 만들자. 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private 로 풀어두자. 권한을 자주 풀어준다면, 컴포넌트를 더 분해해야 되는 것은 아닌지 고민해야 한다.
public 클래스에서 멤버 접근 수준을 package-private 에서 protected 로 바꾸는 순간 그 멤버에 접근할 수 있는 대상 범위가 넓어지므로 public 클래스의 protected 멤버는 공개 API 이므로 영원히 지원돼야 한다.
내부 동작 방식을 API 문서에 적어 사용자에게 공개해야 할 수도 있다. Protected 멤버의 수는 적을수록 좋다.
참고로, 멤버 접근성을 못 좁히는 방해 제약도 있다. 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스보다 좁게 설정할 수 없다. (리스코프 치환 원칙)
public 클래스의 인스턴스 필드는 되도록 public 이 아니어야 한다.
클래스의 필드가 가변 객체를 참조하거나, final 이 아닌 인스턴스 필드를 public 으로 선언하면 그 필드에 담긴 값을 제한할 힘을 잃게 된다. 그 필드와 관련된 모든 것은 불변식을 보장할 수 없게 된다.
심지어 가변 필드가 수정될 때 다른 작업을 할 수 없게 되므로 thread-safe 하지도 않다. public 지시어는 될 수 있으면 삼가자.
예외는 있다. 상수의 경우에는 관례대로 public static final 지시어를 사용해 필드를 공개해도 좋다. 이런 필드는 반드시 기본 타입 값이나 불변 객체를 참조해야 한다.
그렇다고 절대로 아래와 같이 배열을 public static final 로 선언하면 안 된다. 배열의 참조를 변경할 수 없겠지만, 배열 내 내용을 변경할 수 있는 보안 허점이 있다.
문제점
public static final Thing[] VALUES = {...};
해결책 1
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
배열을 private 로 만들고 public 불변 리스트로 변환하여 추가한다.
해결책 2
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
return PRIVATE_VALUES.clone(); //방어적 복사본
}
얼핏 보면 Object 의 equals 메서드와 동일해 보이는데 단순 동치성 비교를 넘어서서 순서까지 비교할 수 있고 제네릭하다는 특징이 있다. 그래서 Comparable 인터페이스를 구현한 클래스들은 자연적인 순서(natural order)가 있다.
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
이 예제는 TreeSet 에 명령 인자들을 추가하는데 출력했을 때 알파벳순으로 출력된다. String 클래스 자체가 Comparable 인터페이스를 구현한 덕분이다. 이처럼 자바 플랫폼 라이브러리에 있는 값 클래스와 열거 타입이 Comparable 을 구현하고 있어 손쉽게 정렬을 할 수 있다.
Comparable 의 compareTo 메서드도 equals 와 유사한 규약을 가지고 있다.
compareTo 메서드 일반 규약
'이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 타입이 다른 객체가 주어진다면 ClassCastException 예외를 던진다.'
대칭성
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
-> 여기서 sgn 은 부호함수로 표현식이 음수면 -1, 0 이면 0, 양수이면 1을 반환한다.
hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯, compareTo 규약을 지키지 못하면 비교를 활용하는 클래스들과 어울리지 못 한다. TreeSet 과 TreeMap 같은 정렬이 보장되어 있는 컬렉션들, 유틸리티 클래스인 Collections 와 Array 도 compareTo 규약을 지켜야만 제대로 동작한다.
마지막 4번째 규약은 꼭 지키라고 권장하고 있다.
빈 HashSet 인스턴스를 생성한 다음 new BigDecimal("1.0") 과 new BigDecimal("1.00") 을 차례대로 추가했다고 가정해보자. 이 두 BigDecimal 을 equals 메서드로 비교하면 서로 다르기 때문에 HashSet 은 2 개의 원소를 가진다. 하지만 TreeSet 에 담았다면 하나의 원소를 가지게 된다. equals 결과값과 compareTo 결과값이 다르면 나중에 컬렉션을 옮길 때 문제가 발생할 수 있다.
compareTo 메서드 올바르게 사용하기
public int compareTo(PhoneNumber pn) {
int result = Short.compare(this.areaCode, pn.areaCode);
if(result == 0) {
result = Short.compare(this.prefix, pn.prefix);
if(result == 0) {
result = Short.compare(this.lineNum, pn.lineNum);
}
}
}
Comparable 인터페이스는 제네릭 인터페이스이므로 인수 타입이 컴파일타임에 정해진다. 타입을 확인하거나 형변환할 필요가 없다. 인수의 타입이 잘못되었다면 컴파일 자체가 안 되어 Object 처럼 형변환을 안 해도 된다.
자바 7 이전에는 정수 기본 타입을 비교할 때 <, > 연산자를 사용하고 실수 기본 타입 필드를 비교할 때 정적 메서드인 Double.compare 와 Float.compare 을 사용하라고 권고했었다. 그런데 자바 7 도입 이후 박싱 타입 클래스에서 기본 타입에 대한 정적 메서드를 제공하고 있어 그냥 compare 로만 비교하면 된다.
위 예제에서 비교할 필드가 여러 개라면, 가장 핵심이 되는 필드부터 비교해나가자.
비교 결과가 0 이 아니라면 순서가 결정되었으니 바로 리턴하고, 같다면 그 다음 주요 필드를 계속해서 비교하면 된다.
자바 8에서는 Comparator 인터페이스가 비교자 생성 메서드를 제공하고 있다.
약간의 성능 저하가 있지만, 함수형 프로그래밍처럼 메서드 연쇄 호출을 할 수 있어 아래와 같이 코딩하기도 한다.