728x90
반응형
3장 - 모든 객체의 공통 메서드
💡 equals는 일반 규약을 지켜 재정의하라
“꼭 필요한 경우가 아니면
equals
를 재정의하지 말자”
1. 재정의를 최대한 피하자
equals
메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있음- 문제를 회피하는 가장 쉬운 길은 아예 재정의하지 않는 것
- 많은 경우에
Object
의equals
가 비교적 정확히 수행해 줌
2. equals 재정의가 필요한 상황
- 객체 식별성이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의
equals
가 논리적 동치성을 비교하도록 재정의되지 않았을 때.- 객체 식별성 : 두 객체가 물리적으로 같은가(주소),
==
연산자로 확인 - 논리적 동치성 : 값이 같은가?,
equals
메서드로 확인
- 객체 식별성 : 두 객체가 물리적으로 같은가(주소),
- 주로
Integer
,String
처럼 값을 표현하는 클래스가 이에 해당 함.
3. 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, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true면 x.equals(z)도 true다. |
일관성(consistency) | null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다. |
null-아님 | null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다. |
4. AutoValue 프레임워크
- equals 일반 규약을 철저히 지키며
equals
를 재정의하는 일은 지루하고 쉽지 않다. - 구글의 AutoValue 프레임워크는 이 작업을 대신해 줌.
- 클래스에 어노테이션 하나만 추가하면
equals
메서드를 알아서 작성해주며, 우리가 작성하는 것과 근본적으로 똑같은 코드를 생성해 줌.
→ 부주의한 실수를 방지하기 위해 AutoValue 프레임워크를 적극 활용하자.
💡 equals를 재정의하려거든 hashCode도 재정의하라
“
equals
를 재정의한 클래스 모두에서hashCode
도 재정의해야 한다”
1. hashCode 재정의 필요성
equals
를 재정의한 클래스에서hashCode
를 재정의하지 않으면,hashCode
일반 규약을 어기게 되어 해당 클래스의 인스턴스를HashMap
이나HashSet
같은 컬렉션의 원소로 사용할 때 문제가 됨hashCode
를 재정의 할 시, 반드시 아래 일반 규약을 따라야 함
설명 | |
---|---|
1 | equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다. |
2 | equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다. |
3 | equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다. |
2. AutoValue 프레임워크
hashCode
일반 규약을 지키며 재정의하는 일은 어렵지는 않지만 조금 따분한 일
→ AutoValue 프레임워크를 사용하면 멋진 equals
와 hashCode
를 자동으로 만들어 줌! 적극활용하자
💡 toString을 항상 재정의하라
“모든 구체 클래스에서
Object
의toString
을 재정의하자”
1. Object의 기본 toString 메서드
- 단순히 클래스_이름@16진수로_표시한_해시코드 반환(유익하지 않은 정보)
toString
의 규약 : 모든 하위 클래스에서 이 메서드를 재정의하라- 우리가 직접 호출하지 않아도, 다른 어딘가에서 쓰일 수 있기 때문
- ex)
println
,printf
, 문자열 연결 연산자(+
), 디버거가 객체를 출력할 때
- ex)
→ 간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 제공하기 위해 재정의하자
2. 재정의 방법
- 객체가 가진 주요 정보 모두를 반환하는 게 좋음
- 포맷을 명시하든 아니든 의도를 명확히 해야함.
- 포맷을 명시한 경우
- 장점 : 표준적, 명확, 사람이 읽을 수 있음
- 단점 : 평생 그 포맷에 얽매임
- 포맷을 명시하기로 했다면 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋음
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "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() { ... }
- 포맷 명시여부에 관계없이
toString
이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자. (getter)
3. AutoValue 프레임워크
- AutoValue 프레임워크는
toString
도 생성해 줌. - AutoValue 프레임워크는 각 필드의 내용을 멋지게 나타내 주기는 하지만 클래스의 ‘의미’까지 파악하지는 못 함
- 예를 들어 전화번호 같은 포맷(XXX-YYY-ZZZZ)에 따라 반환해야하는
toString
에는 적합하지 않고, 일반적인 정보들만 보여주는 경우라면 적합함.
→ 상황에 따라 적절히 사용하자
💡 clone 재정의는 주의해서 진행하라
“복제 기능은 생성자와 팩터리를 사용하는 게 최고”
1. Cloneable 인터페이스와 clone 메서드
Cloneable
인터페이스- 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(mixin interface)
Object
의protected
메서드인clone
의 동작 방식을 결정
Object
의clone
메서드Cloneable
을 구현한 클래스의 인스턴스에서clone
을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환
2. Cloneable & clone의 문제점
- 인터페이스 구현를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위
- but,
Cloneable
의 경우에는 상위 클래스에 정의된protected
메서드의 동작 방식을 변경하는 것
- but,
Cloneable
아키텍처는 ‘가변 객체를 참조하는 필드는final
로 선언하라’는 일반 용법과 충돌한다.- 복제할 수 있는 클래스를 만들기 위해 일부 필드에서
final
한정자를 제거해야 할 수도 있음
- 복제할 수 있는 클래스를 만들기 위해 일부 필드에서
Cloneable
을 구현한 클래스는clone
메서드를public
으로 제공하며, 생성자를 호출하지 않고도 객체를 생성할 수 있게 됨Object
의clone
메서드는 동기화를 신경 쓰지 않았다. 그러니super.clone
호출 외에 다른 할 일이 없더라도clone
을 재정의 후 동기화해줘야 함
3. clone 재정의 방법
Cloneable
구현- 접근제한자는
public
, 반환 타입은 클래스 자신으로 변경하여 재정의 - 메서드 내부에서는 가장 먼저
super.clone
을 호출한 후 필요한 필드를 전부 적절히 수정
4. 복사 생성자와 복사 팩터리
- 복사 생성자와 복사 팩터리는 언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식)을 사용하지 않음
- 엉성하게 문서화된 규약(
clone
메서드의 일반 규약)에 기대지 않고, 정상적인final
필드 용법과도 충돌하지 않으며, 불필요한 검사 예외를 던지지 않고, 형변환도 필요하지 않음. - 복사 생성자 예시
// 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자
public Yum(Yum yum) { ... };
- 복사 팩터리 예시
// 복사 생성자를 모방한 정적 팩터리
public static Yum newInstance(Yum yum) { ... };
→ 새로운 인터페이스나 클래스를 만들 때, Cloneable
사용을 절대 지양하자. 대신 복사 생성자와 복사 팩터리를 제공하자
💡 Comparable을 구현할지 고려하라
“순서를 고려해야 하는 값 클래스를 작성한다면
Comparable
을 구현하자”
1. Comparable 인터페이스
public interface Comparable<T> {
int compareTo(T t);
}
Comparable
인터페이스를 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻 함Comparable
인터페이스를 구현한 객체 들의 배열은 아래 예시처럼 손쉽게 정렬할 수 있음.
Arrays.sort(a);
- 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 역시 쉽게 할 수 있음.
2. compareTo 메서드 일반 규약
요건 | 설명 |
---|---|
대칭성 | Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compartTo(y)) == -sgn.(y.compareTo(x))여야 한다(따라서 x.compareTo(y)는 y.compareTo(x)가 예외를 던질때에 한해 예외를 던져야 한다). |
추이성 | Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0이다. |
반사성 | Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))다. |
동치성 테스트의 결과가 equals와 동일한가 | 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다. ”주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다.” |
3. 비교자
- compareTo 메서드에서 관계 연산자
<
연산자나>
를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니, 비추천 - 객체 참조를 비교할 때
- 자바가 제공하는 비교자 사용, ex.
String.CASE_INSENSITIVE_ORDER
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
// 자바가 제공하는 비교자를 사용해 클래스를 비교한다.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
... // 나머지 코드 생략
}
- 비교자 생성 메서드 사용
public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
//비교자 생성 메서드를 활용한 비교자
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);
}
... // 나머지 코드 생략
}
- 기본 타입을 비교할 때 : 박싱된 기본 타입들의
compare
메서드 사용
// 기본 타입 필드가 여럿일 때의 비교자
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;
}
728x90
반응형
'📚 Reading > Tech' 카테고리의 다른 글
이펙티브 자바 - 7장. 람다와 스트림 (0) | 2023.01.24 |
---|---|
이펙티브 자바 - 6장. 열거 타입과 애너테이션 (0) | 2023.01.24 |
이펙티브 자바 - 5장. 제네릭 (0) | 2023.01.24 |
이펙티브 자바 - 4장. 클래스와 인터페이스 (0) | 2023.01.24 |
이펙티브 자바 - 2장. 객체 생성과 파괴 (0) | 2023.01.24 |