
Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다. Object에서 final이 아닌 메서드는 모두 재정의를 염두에 두고 설계되었다.
final이 아닌 메서드
equals
hashCode
toString
clone
fainalize
하지만 메서드를 일반 규약에 맞지 않게 재정의하면 클래스를 오작동하게 만들 수 있다.
문제를 회피하는 가장 쉬운 길은 재정의를 하지 않는 것 이다. 그러면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다.
다음은 재정의를 하지 않는 것이 최선인 상황들이다.
값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스가 여기에 해당한다.
정규표현식을 검사하는 Pattern의 인스턴스는 논리적으로 동치성을 검사한다. 하지만 설계자는 클라이언트가 이 방식을 원하지 않거나 애초에 필요하지 않다고 판단할 수도 있다. 필요하지 않을 경우 Object의 기본 equals만으로 해결된다.
Set과 Map 구현체들은 각각 AbstractSet, AbstractList이 구현한 equals를 상속받아 그대로 사용한다.
@Override public boolean equals(Object o){
throw new AssertionError(); // 호출 금지!
}
두 객체가 물리적으로 같은지를 비교하는 객체 식별성이 아닌 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때이다
값 클래스란(Integer, String)처럼 값을 표현하는 클래스들이 위에 해당된다.
두 값 객체를 equals로 비교하는 프로그래머는 객체가 같은지가 아니라 값이 같은지를 알고 싶어 할 것이다.
값 클래스야도 같은 값 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스(Enum)라면 equals를 재정의하지 않아도 된다.
equals 메서드를 재정의할 때 반드시 따라야할 일반 규약 : 동치관계
- 반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.
- 대칭성(symmetry): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)는 true면 y.equals(x)도 true이다.
- 추이성(transitivity): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)는 true이고, y.equals(x)도 true면, x.equals(z)는 true이다.
- 일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
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의 존재를 몰라 대칭성을 위반하게 된다.
@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
CaseInsensitiveString의 equals를 String 과 연동하겠다는 것을 버려야한다.
첫 번째 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체가 같다야 한다는 뜻이다.
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;
}
Point의 equals는 색상을 무시하고, ColorPoint의 equals는 입력 매개변수의 클래스 종류가 달라 두 객체를 비교하면 매번 false만 반환할 것이다.
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(){
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
}
위 코드는 대칭성은 지켜주지만, 추이성을 깨버린다. p1과 p2, p2와 p3는 색상을 무시했지만, p1과 p3비교에서는 색상까지 고려했기 때문이다. 또한, 이 방식은 무한 재귀에 빠질 위험도 있다.
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
@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;
}
얼핏 equals 안의 instanceof 검살르 getClass 검사로 바꾸면 규약도 지키고 값도 추가하면서 구체 클래스를 상속할 수 있다고 생각할 수 있지만, Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다.
컴포지션:
클래스 필드 내에 private or 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);
}
/* 이 ColorPoint의 Point 뷰를 반환한다. */
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);
}
...
}
Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고, ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰(view) 메서드를 public으로 추가하는 식이다.
java.sql.Timestamp도 java.util.Date를 확장한 후 nanoseconds 필드를 추가했다. 그 결과 Timestamp의 equals는 대칭성을 위배하여 Date객체와 한 컬렉션에 넣거나 서로 섞어 사용하면 엉뚱하게 동작할 수 있다.
일관성(consistency) - 두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 영원히 같다.
null-아님 - 모든 객체가 null과 같지 않아야 한다.
@Override
public boolean equals(Object o) {
if(!(o instanceof MyType)) {
return false;
}
MyType myType = (MyType) o;
}
위처럼 묵시적 null 검사를 사용하면 첫 번째 피연산자가 null일 경우 false를 반환한다.
==연산자를 사용해 입력이 자기 자신의 참조인지 확인한다. 자기 자신이면 true를 반환한다. 단순한 성능 최적화용으로 비교 작업이 복잡한 상황일 때 값어치를 한다.instanceof 연산자로 입력이 올바른 타입인지 확인한다. 어떤 인터페이스는 자신을 구현한 클래스 끼리도 비교할 수 있도록 equals 규약을 수정하기도 한다. 이런 인터페이스를 구현한 클래스라면 equals에서 (클래스가 아닌) 해당 인터페이스를 사용해야 한다. ex) Set, List, Map, Map.Entry 등instanceof 연산자로 입력이 올바른 타입인지 검사 했기 때문에 이 단계는 100% 성공한다.true를 반환하고, 하나라도 다르면 false를 반환한다.float와 double(부동 소수를 다뤄야 하기 때문)을 제외한 기본 타입 필드는 == 연산자로 비교하고, 참조 타입 필드는 각각의 equals 메서드로, float와 double 필드는 각각 정적 메서드인 Float.compare(), Double.compare()로 비교한다.
배열 필드는 원소 각각을 앞서의 지침대로 비교한다. 배열의 모든 원소가 핵심 필드라면 Arrays.equals 메서드들 중 하나를 사용하자.
null도 정상 값으로 취급하는 참조 타입 필드도 있다. 이런 필드는 정적 메서드인 Objects.equals()로 비교해 NullPointerException 발생을 예방하자.
비교하기가 아주 복잡한 필드일 경우 그 필드의 표준형(canonical form)을 저장해둔 후 표준형끼리 비교하면 훨씬 경제적이다. (불변 클래스에 제격)
필드 비교 순서에 따라 equals의 성능을 좌우한다.
equals를 다 구현했다면 대칭적인지, 추이성이 있는지, 일관적인지 자문을 해보자.
equals를 재정의할 땐 hashCode도 반드시 재정의하자.
너무 복잡하게 해결하려 들지 말자.
Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자.
Object 명세에서 발췌한 규약
equals비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의hashCode메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. (애플리케이션을 다시 실행한다면 이 값이 달라져도 상관 없음)
equals가 두 객체가 같다고 판단했다면, 두 객체의hashCode는 똑같은 값을 반환해야 한다.
equals가 두 객체를 다르다고 판단했더라도, 두 객체의hashCode는 서로 다른 값을 반환할 필요는 없다.단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707,867, 5309), "제니");
이 코드에 map.get(new PhoneNumber(707,867, 5309))를 실행하면 "제니"가 아닌 null을 반환한다.
여기에는 2개의 PhoneNumber 인스턴스가 사용되었는데, 해시코드를 재정의하지 않아 논리적 동치인 두 객체(넣을 때 사용, 가져올 때 사용)가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못한다.
HashMap은 해시코드가 서로 다른 엔트리끼리는 동치성 비교를 시도조차 않도록 최적화 되어있다.
result를 선언한 후 값을 c로 초기화한다.equals 비교에 사용되는 필드를 말한다.Type.hashCode(f)를 수행한다. 여기서 Type은 해당 기본타입의 박싱 클래스다.equals 메소드가 이 필드의 equals를 재귀적으로 호출한다. 계산이 복잡해질 것 같으면, 이 필드의 표준형을 만들어 그 표준형의 hashCode를 호출한다. 필드의 값이 null이면 0을 사용한다.Arrays.hashCode를 사용한다.result를 갱신한다.result = 31 * result + c;result를 반환한다.파생 필드는 해시코드 계산에서 제외해도 된다. 또한 equals 비교에 사용되지 않은 필드는 반드시 제외해야 한다. (두 번째 규악을 어기게될 위험 있음)
2.b의 곱셈을 31 * result는 필드를 곱하는 순서에 따라 result 값이 달라지게 한다. 31로 정한 이유는 홀수이면서, 소수 이기 때문이다. 만약 이 숫자가 짝수이고 오버플로가 발생한다면 정보를 잃게 된다.
클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기 보다 캐싱을 고려해야 한다.
성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다.
toString은 클래스이름@16진수로표시한_해시코드를 반환할 뿐이다. 일반 규약에 따라 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.
toString의 규악은 모든 하위 클래스에서 이 메서드를 재정의하라 고 한다.
실전에서 toString은 그 객체가 가진 주요 정보를 모두 반환하는 게 좋다.
toString을 구현할 때면 반환값의 포멧을 문서화할 지 정해야 한다.
포멧 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.
정적 유틸리티 클래스는 toString을 제공할 필요가 없으며, 대부분의 열거타입도 자바가 이미 완벽한 toString을 제공하므로 따로 제정의하지 않아도 된다.
Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이다. 하지만 clone메서드는 Object에 선언이 되어 있고 심지어 protected 이다.
Cloneable 인터페이스는 Object의 clone의 동작 방식을 결정한다. Cloneable 을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
clone메서드가 super.clone이 아닌 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않을 것이다. 하지만 이 클래스의 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져 결국 하위 클래스의 clone메서드가 제대로 동작하지 않게 된다.
결국 clone 메서드를 내부에서 super.clone 메서드를 통해 재정의 해야한다.
@Override
public PhoneNumber clone(){
try{
return (PhoneNumber) super.clone();
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
쓸데없는 복사를 지양한다는 관점에서 보면 불변 클래스는 굳이 clone 메서드를 제공하지 않는 것이 좋다.
super.clone 메서드를 사용하면 위와 같이 구현을 할 수 있다. PhoneNumber의 clone 메서드는 Object와 다르게 PhoneNumber를 반환하게 했다. 공변 반환 타이핑을 권장한다.
공변 변환 타이핑(Convariant Return Type)
재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입이 될 수 있다.
try-catch 블록으로 감싼 이유는 clone 메서드가 검사 예외인 CloneNotSupportedException을 던지도록 선언되었기 때문이다. super.clone은 무조건 성공하여 비검사 예외를 했어야 했다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = 0;
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
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();
}
}
배열의 clone은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 clone메서드를 사용하라고 권장한다. 하지만 elements 필드가 final이었다면 final 필드는 새로운 값을 할당할 수 없기때문에 위 clone 메서드 방식은 작동하지 않는다.
Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 복사 생성자와 복사 팩터리를 위해 더 나은 객체 복사 방식을 제공할 수 있다.
복사 생성자란 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.
복사 생성자와 그 변형인 복사 팩터리는 Cloneable/clone 방식보다 나은 면이 많다.
언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식)을 사용하지 않으며, 엉성하게 문서화된 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않으며, 불필요한 검사 예외를 던지지도 않고, 형변환이 필요하지 않다.
Comparable 인터페이스의 유일무이한 메서드인 compareTo가 있다. compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현한 클래스의 인스턴스에는 자연적인 순서가 있음을 뜻하며, Arrays.sort 메서드를 통해 손쉽게 정렬할 수 있다.
자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현하여 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
equals와 마찬가지로 반사성, 대칭성, 추이성을 충족해야하며, 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가 했다면 compareTo 규약을 지킬 방법이 없다. 우회 방법으로 컴포지션을 사용한다.