Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.
제3장 모든 객체의 공통 메서드
- Object 의 final 이 아닌 메서드 : equals, hashCode, toString, clone, finalize
- 재정의 시 지켜야 하는 일반 규약 설명
- 언제 어떻게 재정의해야 하는지 설명
- finalize 는 8번 항목에서 다뤘으므로 제외. Comparable.compareTo 는 포함
10) equals 는 일반 규약을 지켜 재정의 하라
다음 사항에 해당된다면 equals 메서드는 재정의 하지 않는게 좋다.
- 각 인스턴스가 본질적으로
고유
하다 - 인스턴스의
논리적 동치성
을 검사할 일이 없다 - 상위 클래스에서 재정의한 equals 가 하위 클래스에도 적합하다
- 클래스가 private 이거나 package-private 이고, equals 메서드를 호출할 일이 없다
equals 메서드는 동치관계를 구현하며, 다음을 만족해야 한다 (규약)
- null-아님 : x.equals(null) == false
- 반사성(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(x) == true 를 반복해서 호출해도 항상 true 또는 false
- equals 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다
- 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다
코드 10-1 잘못된 코드 : 대칭성 위배
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requiredNonNull(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(this.s);
}
*/
}
/////////////////////
CaseInsensitiveString cis = new CaseInsensitiveString("polish");
String s = "polish";
cis.equals(s) == true // OK
s.equals(cis) != true // wrong!
1
- equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다
코드 10-2 잘못된 코드 : 대칭성 위배
1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
p.equals(cp) == true;
cp.equals(p) != true; // 클래스 종류가 다르다고 false 반환
코드 10-3 잘못된 코드 : 추이성 위배
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override public boolean equals(Object o) {
if( !(o instanceof Point) )
return false;
// o 가 일반 Point 면 색상을 무시하고 비교한다
if( !(o instanceof ColorPoint) )
return o.equals(this);
// o 가 ColorPoint 면 색상까지 비교한다
return super.equals(o) && ((ColorPoint) o).color == this.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) != true // color 가 다르다고 false 반환
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
코드 10-4 잘못된 코드 : 리스코프 치환 원칙 위배
1
2
3
4
5
6
7
8
9
@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;
}
// 괜찮아 보이지만 실제로 활용할 수는 없다
// Point 하위 클래스는 정의상 Point 이므로 어디서든 Point 로서 활용될 수 있어야 한다
리스코프 치환 원칙
어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.
코드 10-5 equals 규약을 지키면서 값 추가하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.requiredNonNull(color);
}
// '항목 18' 참조 : 상속 대신 컴포지션을 사용하라
// 이 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);
}
}
equals 메소드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다
- instanceof 연산자로 입력이 올바른 타입인지 확인한다
- 입력을 올바른 타입으로 형변환 한다
- 입력 객체와 자기 자신의 대응되는
핵심
필드들이 모두 일치하는지 하나씩 검사한다 - 어떤 필드를 먼저 비교하느냐에 따른 성능(비용) 문제도 고민한다
코드 10-6 전형적인 equals 메서드의 예
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}
주의사항
1
2
3
4
- equals 를 재정의 할 땐 hasCode 도 반드시 재정의 하자
- 너무 복잡하게 해결하려 들지 말자
- Object 외의 타입을 매개변수를 받는 equals 는 만들지 말자
- - ex) equals(MyClass o) : `재정의`가 아니라 `다중정의`이다!
11) equals 를 재정의 하려거든 hasCode 도 재정의 하라
hashCode 일반 규약
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안은 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
- equals(Object) 가 두 객체를 같다고 판단했다면, 두 객체의 hashCode 는 똑같은 값을 반환해야 한다
- equals(Object) 가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode 가 서로 다른 값을 반환할 필요는 없다
코드 11-1 최악의 (하지만 적법한) hashCode 구현 : 사용 금지!
1
@Override public int hashCode() { return 42; }
좋은 hashCode 를 작성하는 요령
- int 변수 result 를 선언한 후 값 c 로 초기화
- c 는 핵심 필드의 해시코드 계산값 (2-a 계산식 이용)
- 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행
- a. 해당 필드의 해시코드 c 를 계산
- 기본타입 필드라면 Type.hashCode(f) 를 수행
- 참조타입 필드면서 equals 메서드가 이 필드의 equals 를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode 를 재귀적으로 호출한다
- 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다
- 필드값이 null 이면 0 을 사용
- b. 단계 2-1 에서 계산한 해시코드 c 로 result 를 갱신
- result = 31 * result + c
- a. 해당 필드의 해시코드 c 를 계산
- result 를 반환한다
코드 11-2 전형적인 hashCode 메서드
1
2
3
4
5
6
@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 Guava 를 이용한 한 줄짜리 hashCode 메서드 : 성능이 살짝 아쉽다
1
2
3
@Override public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
클래스가 불변이고 해시코드를 계산하는 비용이 크다면 캐싱하는 방식을 고려해야 한다
코드 11-4 해시코드를 지연 초기화하는 hashCode 메서드 : 스레드 안정성까지 고려해야 한다
1
2
3
4
5
6
7
8
9
10
11
12
13
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 을 항상 재정의 하라
toString 의 일반 규약
간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다
toString 이 잘 구현된 클래스는 사용하기에 훨씬 즐겁고 디버깅하기 쉽다
1
2
// phoneNumber.toString() 이 쓰인다
System.out.println( phoneNumber + "에 연결할 수 없습니다");
13) clone 재정의는 주의해서 진행하라
코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
1
2
3
4
5
6
7
8
// 이 메서드가 동작하려면 PhoneNumber 선언부에 Cloneable 을 구현한다고 추가해야 한다
@Override publicc PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다
}
}
clone 메서드는 사실상 생성자와 같은 효과를 나타낸다. 즉, clone 은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Stack {
private Object[] elements;
private int size = 0;
... // 생략
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone(); // size 는 정상 복사됨
result.elements = elements.clone(); // Object[] 의 복사본 만들기 (재귀적 호출)
// elements 가 final 이면 작동하지 않는다
return result;
} catch( CloneNotSupportedException e){
throw new AssertionError();
}
}
}
직렬화와 마찬가지로, Cloneable 아키텍처는 ‘가변객체를 참조하는 필드는 final로 선언하라’는 일반 용법과 충돌한다. 그래서 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.
코드 13-3 잘못된 clone 메서드 : 가변상태를 공유한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
}
}
... // 생략
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone(); // deepCopy 가 필요하다!!
return result;
} catch( CloneNotSupportException e){
throw new AssetionError();
}
}
}
코드 13-4 복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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() {
// **NOTE: next 리스트가 길면 스택 오버플로를 일으킬 위험이 있다
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
... // 생략
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone(); // New
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( CloneNotSupportException e){
throw new AssetionError();
}
}
}
코드 13-5 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다
1
2
3
4
5
6
7
Entry deepCopy() {
Entry result = new Entry(key, value, next);
// **NOTE: 한꺼번에 할당하는 대신 점진적으로 할당하도록 수정
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 메서드
1
2
3
@Override protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
코드 13-7 복사 생성자
1
public Yum(Yum yum) { ... }
코드 13-8 복사 팩토리
1
public static Yum newInstance(Yum yum) { ... }
핵심정리
- Cloneable 이 몰고 온 모든 문제를 되짚어봤을 때, interface 이든 class 이든 안쓰는게 낫다
- final 클래스라면 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 드물게 허용해야 한다
- 기본 원칙은 ‘복제 기능은 생성자와 팩토리를 이용하는게 최고’라는 것
- 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 합당한 예외라 할 수 있다
14) Comparable 을 구현할지 고려하라
1
2
3
public interface Comparable<T> {
int compareTo(T t);
}
compareTo 는 두가지만 빼면 Object 의 equals 와 같다.
- compareTo 는 단순 동치성 비교에 더해
순서
까지 비교할 수 있으며 - compareTo 는
제네릭
하다- ex) Arrays.sort(a)
1
2
3
4
5
6
7
8
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
// Set 이 comparable 하기 때문에 Collections 사용 가능 (순서 비교)
Collections.addAll(s, args); // 알파벳 순으로 정렬되어 추가됨
System.out.println(s);
}
}
Comparable
을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 좁쌀만한 노력으로 코끼리만한 큰 효과를 얻는 것이다.
compareTo 메서드의 일반 규약
- 이 객체와 주어진 객체의 순서를 비교한다.
- 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다
- 비교할 수 없는 타입의 객체가 주어지면 ClassCastException 을 던진다
- 부호함수 sgn 을 이용한 정의
- 대칭성 : 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))- ex) BigDecimal(“1.0”) 과 BigDecimal(“1.00”) 은 equals 로는 다르지만 compareTo 로는 같다
compareTo 메서드는 각 필드가 동치인지를 비교하는게 아니라 그 순서를 비교한다
코드 14-1 객체 참조 필드가 하나뿐인 비교자
1
2
3
4
5
6
7
8
// CaseInsensitiveString 타입하고만 비교할 수 있다는 의미
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
... // 생략
}
코드 14-2 기본 타입 필드가 여럿일 때의 비교자
1
2
3
4
5
6
7
8
9
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 비교자 생성 메서드를 활용한 비교자
1
2
3
4
5
6
7
8
9
// 자바 8에서 지원 시작. 약간의 성능 저하가 있다 (10% 정도)
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);
}
자바 8에서 Comparator 는 수많은 보조 생성 메서드들로 무장하고 있다. comparingInt 처럼 float, double 용도 제공하고 있고 객체 참조용 비교자 생성 메서드도 준비되어 있다. 이를 이용하면 이름만으로도 정적 비교자 생성 메서드들을 사용할 수 있어 코드가 훨씬 깔끔해진다.
코드 14-4 해시코드 값의 차를 기준으로 하는 비교자 : 추이성을 위배한다!
1
2
3
4
5
6
7
// **NOTE: 이 방식은 사용하면 안된다!!
// 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
}
코드 14-5 정적 compare 메서드를 활용한 비교자
1
2
3
4
5
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
}
코드 14-6 비교자 생성 메서드를 활용한 비교자
1
2
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
끝! 읽어주셔서 감사합니다.