[EffectiveJava] 3장 모든 객체의 공통 메서드
10. equals는 일반 규약을 지켜 재정의하라
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지
}
// 10-1 잘못된 코드 - 대칭성 위배!
public final class CaseInsensitiveString{
private final String s;
public CaseInsensitiveString(String s){
this.s = Object.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 cis = new CaseInsensitiveString("Polish");
String s = "polish";
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
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;
}
...// 나머지 코드는 생략
}
// 10-2 잘못된 코드 - 대칭성 위배!
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
// 10-3 잘못된 코드 - 추이성 위배!
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
// o가 일반 point면 색상을 무시하고 비교한다.
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 p1 = new Point(1, 2);
ColorPoint p2 = new ColorPoint(1, 2, Color.BLUE);
// 10-4 잘못된 코드 - 리스코프 치환 원칙 위배
@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;
}
// 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다.
private static final Set<point> unitCircle= Set.of(
new Point(1,0), new Point(0,1),
new Point(-1,0), new Point(0,-1)
);
public static boolean onUnitCircle(Point p){
return unitCircle.contains(p);
}
public class CounterPoint extends point{
private static final AtomicInteger conter = new AtomicInteger();
public CounterPoint(int x,int y){
super(x,y);
counter.incrementAndGet();
}
public static int numberCreated() { return conter.get(); }
}
// 10-5 equals 규약을 지키면서 값 추가하기
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(!(obj instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
... // 나머지 코드는 생략
}
// 명시적으로 null 검사(비권장)
@Override public boolean equals(Object o){
if(o == null)
return false;
...
}
//instanceof로 묵시적으로 null 검사를 할 수 있음(권장)
@Override public boolean equals(Object o){
if(!(obj instanceof MyType))
return false;
MyType mt (MyType) o;
...
}
// 10-6 전형적인 equals 메서드의 예
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;
}
... // 나머지 코드는 생략
}
// 잘못된 예 - 입력 타입은 반드시 Object여야 한다!
@Override boolean equals(MyClass o) {
...
}
// 여전히 잘못된 예 - 컴파일 되지 않음
@Override public boolean equals(MyClass o) {
...
}
11. equals를 재정의하려거든 hashCode도 재정의하라
문제가 되는 상황
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
// 11-1 최악의 (하지만 적법한) hashCode 구현 - 사용금지
@Override public int hashCode() { return 42; }
좋은 해시코드를 만드는 방법
result = 31 * result + c;
// 11-2 전형적인 hashCode 메서드
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
// 11-3 한줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다.
@Override public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
// 11-4 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다.
private int hashCode; // 자동으로 0으로 초기화된다.
@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;
}
12. toString을 항상 재정의하라
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ"형태의 12글자로 구성된다.
* XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
* */
@Override public String toString() {
return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
/**
* 이 약물에 관한 대략적인 설명을 반환한다.
* 다음은 이 설명의 일반적인 형태이나,
* 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
* "[약물 #9: 유형=사랑, 냄새=테레빈유, 겉모습=먹물]" */
@Override
public String toString() {...}
13. clone 재정의는 주의해서 진행하라
// 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
// 13-2 가변 상태를 참조하는 클래스용 clone 메서드
@Override public Stack clone(){
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
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;
}
}
... // 나머지 코드는 생략
}
// 13-3 잘못된 clone 메서드 - 가변 상태를 공유한다!
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch(CloneNotSupportedException e) {
throw new Assertion();
}
}
// 13-4 복잡한 가변 상태를 갖는 클래스용 재귀적 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 Assertion();
}
}
... // 나머지 코드는 생략
}
// 13-5 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다.
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;
}
// 13-6 하위 클래스에서 Cloneable을 지원하지 못하게 하는 clone 메서드
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
// 13-7 복사 생성자
public Yum(Yum yum) {..}
// 13-8 복사 팩터리
public static Yum newInstance(Yum yum) { ... }
14. Comparable을 구현할지 고려하라
Arrays.sort(a); // 쉽게 정렬할 수 있다.
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
public interface Comparable<T> {
int compareTo(T t);
}
// 14-1 객체 참조 필드가 하나뿐인 비교자
public final class CaseInsensitiveString implements Comparable<CaseInsensiviveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.comapre(s, cis.s);
}
... // 나머지 코드 생략
}
// 14-2 기본 타입 필드가 여럿일 때의 비교자
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;
}
// 14-3 비교자 생성 메서드를 활용한 비교자
private static final Comparator<PhoneNumber> COMPARATOR = Comparator.comparingInt(
(PhoneNumber pn) -> pn.areaCode)
.thenComparing(pn -> pn.prefix)
.thenComparing(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
// 14-4 해시코드 값의 차를 기준으로 하는 비교자 - 추이성을 위배한다!
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
// 14-5 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
// 14-6 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());