새소식

독서/이펙티브 자바 3판

Ch02 모든 객체의 공통 메서드

  • -

step10: equals는 일반 규약을 지켜 재정의하라

 

equals를 재정의 하지 않아도 되는 경우

 

1st 각 인스턴스가 본질적으로 고유한 경우 - 값이 아닌 동작을 표현하는 클래스의 경우 - Thread

2nd 인스턴스의 논리적 동치성을 검사할 일이 없는 경우

3rd 상위 클래스에서 재정의한 equlas가 하위 클래스에서도 적용 되는 경우 - Set, Map, List의 경우

4th 클래스가 private 이거나, package-private여서 equals를 호출할 일이 없는 경우

5th 싱글턴을 보장하는 클래스 (인스턴스 통제 클래스, Enum인 경우) - 객체간 동등성, 동일성이 보장

 

 

 

equals를 재정의 하는 경우 지켜야 할 규약

 

equals를 재정의 해야 하는 경우는 객체 동일성을 확인해야 하는 경우가 아니라 논리적 동치성을 비교하도록 재정의 되어있지 않았을 경우이다.

 

1st 반사성 (reflexivity)

null이 아닌 모든 참조 값 x에 대해 x.equals(x)를 만족해야 한다. 다시 말해 객체는 자기 자신과 비교했을 때 같아야 한다는 뜻이다.

 

2nd 대칭성 (symmetry)

null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)가 true라면, y.equals(x)가 true를 만족해야 한다.

 

3rd 추이성 (transitivity)

null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true 이면 x.equals(z)도 true를 만족해야 한다.

 

4th 일관성 (consistency)

null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 false를 반환한다.

 

=> 대안 방안으로 상속 대신 컴포지션을 사용하면 상속 때문에 발생하는 대칭성, 추이성, 리스코프 치환원칙에 위배되지 않게 코드를 작성할 수 있다. ex) 하위 클래스에 상위 클래스 필드를 private으로 선언하는 것

 

 

not null

 

null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.

@Override 
public boolean equals(Object o) {
  if(o == null) return false; //불필요
  return this.x == o.x;
}

< 명시적 null 검사 -> 불필요 >

@Override
public boolean equals(Object o) {
  if(!(o instanceof MyClass)) return false; //묵시적 null검사
  MyClass clazz = (MyClass) o;
  return this.x == clazz.x;
}

< 묵시적 null 검사 -> 위보다 나은 방향 > 

 

 

위의 내용을 종합해서 양질의 equals 메서드 구현 방법을 정리하면

 

1st == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.

2nd instance of 연산자로 입력이 올바른 타입인지 확인한다.

3rd 입력을 올바른 타입으로 형변환한다.

4th 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.

 

추가로 float와 doulbe을 제외한 기본 타입 필드는 == 연산자로 비교, 참조 타입필드는 각각의 equals 메서드로 비교하지만, float와 double의 필드는 각각 정적 메서드인 Float.compare(float,double) 와 Double.compare(double, double) 로 비교한다. 앞서 float와 double을 특별 취급하는 이유는 Float.NaN, -0.0f 특수한 부동소수 값등을 다뤄야 하기 때문이다.

 

만약 equals를 구현한다면 3가지만 자문해보자 -> 대칭적인가? 추이성이 있는가? 일관적인가?

(구글이 만든 AutoValue 프레임워크는 equals 의 코드 작성을 대신 해준다.)

 

 

정리

꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 많은 경우에 Object와 equals가 대부분의 비교를 정확히 수행해준다. 재정의해야 한다면 그 클래스의 핵심 필드 모두를 빠짐없이, 위 다섯 가지 규약을 확실히 지켜가며 비교해야 할 것이다.

 

 

step11: equals를 재정의하려거든 hashCode도 재정의하라

 

위를 고려하지 않는다면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap 이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.

 

아래는 Object 명세에서 발췌한 규약이다.

 

1) equals 비교에 사용되는 정보가 변경되지 않는다면, 애플리케이션이 실행되는 동안 그 객체의 hashcode 메서드는 몇번을 호출해도 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행했을 경우 값이 달라져도 상관없다.

 

2) equals(object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.

 

3) equlas(object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른값을 반환해야 해시테이블의 성능이 좋아진다.

 

여기서 재정의를 잘못했을 때 크게 문제가 되는 조항은 2번째 이다. 즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다. 아이쳄 10에서 보앗듯 equals는 물리적으로 다른 두 객체를 논리적으로는 같다고 말할 수 있지만, Object의 기본 hashCode 메서드는 이 둘이 전혀 다르다고 판단하여 규약과 달리 서로 다른 값을 반환한다.

 

정리

equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다. 위의 사항을 잘 지키되, 아이템 10에서 얘기한 AutoValue 프레임워크를 사용하면 멋진 equlas와 hashCode를 자동으로 만들어준다. 

 

 

 

step12: toString을 항상 재정의하라

 

toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다. 좋은 toString은 이 인스턴스를 포함하는 객체에서 유용하게 쓰인다. 실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다. 또한 toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야하 하고, 포맷을 명시하든 아니든 사용자의 의도는 명확히 밝혀야 한다. 그러므로 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자. 

 

정리

 

모든 구체 클래스에서 Object의 toString을 재정의하자. 상위 클래스에서 이미 알맞게 재정의한 경우는 예외다. toString은 해당 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 한다.

 

 

 

step13: clone 재정의는 주의해서 진행하라

 

 

 Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다. 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고' 라는 것이다. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외이다.

 

 

 

step14: Comparable을 구현할지 고려하라

 

 

 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자. 모든 객체에 대해 전역 동치관계를 부여하는 equals 메소드와 달리, compareTo는 타입이 다른 객체를 신경 쓰지 않아도 된다.

 

 

1st 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 것이다.

 

2nd 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.

 

3rd 마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 것이다.

 

 위 3가지 규약은 compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑같이  반사성, 대칭성, 추이성을 충족해야 함을 뜻한다. 추가로 compareTo의 마지막 규약은 필수는 아니지만 꼭 지키길 권한다. 마지막 규약은 compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 것이다.

 

 compareTo 메서드 작성 요령은 equals와 비슷하다. Comparable은 타입을 인수로 받는 제너릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해진다. 입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다. 인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다. compareTo 메서드는 각 필드가 동치인지를 비교하는게 아니라, 그 순서를 비교한다.

 

 

정리

 순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <와 >연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

 

'독서 > 이펙티브 자바 3판' 카테고리의 다른 글

CH6 람다와 스트림  (0) 2023.05.15
CH05 열거 타입과 애너테이션  (0) 2023.05.05
CH04 제네릭  (0) 2023.05.02
Ch03 클래스와 인터페이스  (0) 2023.04.25
Ch01 객체 생성과 파괴  (0) 2023.04.05
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.