코딩은 마라톤

[이펙티브자바] 아이템 10, 11, 12 요약 정리 본문

Language/Java

[이펙티브자바] 아이템 10, 11, 12 요약 정리

anxi 2024. 6. 1. 21:21

5월 31일 금요일....

어제 너무 피곤해서 퇴근하자마자 뻗었네요.....

작심3일이 돼버렸지만 다시 파이팅해서 읽어보겠습니다ㅜㅜ!

 

아이템 10. equals는 일반 규약을 지켜 재정의하라

equals() 메서드는 뭘까?

equals 메서드는 객체끼리 내용을 비교할 수 있는 메서드이고, Object 클래스에 정의되어있다.

 

equals 메서드를 재정의하지 않는 것이 좋을 경우

  • 각 인스턴스가 본질적으로 고유할 경우
    • 값을 표현하는 게 아닌 동작하는 객체를 표현하는 클래스일 경우 (ex.Thread)
  • 인스턴스의 논리적 동치성(logical equality)를 검사할 일이 없을 경우
    • java.util.regex.Pattern은 equals를 재정의해서 두 Pattern의 인스턴스가 같은 정규표현식을 나타내는지 검사하는 방법(논리적 동치성 검사)이다. 이런 검사가 필요 없을 경우 Object의 기본 equals로 해결한다.
  • 상위 클래스에 재정의한 equals가 하위 클래스에도 딱 들어맞을 경우
    • Set 구현체는 AbstractSet이 구현한 equals를 상속받아 사용한다.
    • List 구현체는 AbstractList로부터, Map은 AbstractMap으로부터 상속받아 쓴다.
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없을 경우

equals 메서드를 재정의해야할 경우

  • 객체 식별성(두 객체가 물리적으로 같은지)이 아니라 논리적 동치성을 확인해야하는 경우
    equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때
    • equals를 재정의하지 않으면 == 비교를 하기 때문에 객체 식별성 비교를 하게 된다.

equals 메서드를 재정의할 때 지켜야 할 일반 규약

전제 : null이 아닌 모든 참조 값 x, y, z

  • 반사성 :  x에 대해, x.equals(x)는 true
  • 대칭성 : x, y에 대해, x.equals(y)가 true -> y.equals(x) 또한 true
  • 추이성 : x, y, z에 대해, x.equals(y) : true && y.equals(z) : true -> x.equals(z) : true
  • 일관성 : x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true || false 를 반환
  • null-아님 : x에 대해, x.equals(null) : false

양질의 equals 메서드 구현 방법 

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
    • ==(동일성) : 객체 식별성 (주소값 비교)
    • equals(동등성) : 객체가 가진 값 비교
  2. instatnceof 연산자로 입력이 올바른 타입인지 확인한다.
  3. 입력을 올바른 타입으로 형변환한다. (instanceof 검사를 했기 때문에 100% 성공)
  4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
  5. 다 구현 했다면 세 가지를 따져본다. (대칭성, 추이성, 일관성)
  6. equals를 재정의할 때 hashCode도 반드시 재정의한다.
  7. 너무 복잡하게 해결하려 들지 말자. (필드들의 동치성만 검사해도 equals 규약을 지킬 수 있다.)
  8. Object 외의 타입을 매겨변수로 받는 eqauls 메서드는 선언하지 말자.
    • 구글에서 만든 AutoValue 프레임워크나 IDE에게 equals 재정의를 맡기는 것이 낫다.

결론 : 꼭 필요한 경우가 아니면 equals를 재정의하지 말자!

 

아이템 11. equals를 재정의하려거든 hashCode도 재정의하라

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.

그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생할 수 있다.

 

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

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

 

hashCode 재정의를 잘못했을 경우 크게 문제가 되는 조항이다. 즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다. (동등성)

 

public class PhoneNumber {
	
    private final short areaCode, prefix, lineNum;
    
    @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;
    }

 

만약 equal 메서드는 재정의했지만 hashCode를 재정의하지 않았을 경우, 값은 다음과 같이 표현된다.

 

public static void main(String[] args) {

    PhoneNumber pn1 = new PhoneNumber(82, 123, 4567);
    PhoneNumber pn2 = new PhoneNumber(82, 123, 4567);

    System.out.println(pn1.hashCode()); // hashCode of pn1 : 1975012498
    System.out.println(pn2.hashCode()); // hashCode of pn2 : 1808253012
  }

 

두 객체의 동등성이 보장되므로 hashCode 또한 같아야하는데 그렇지 않다.

따라서 equals 메서드를 재정의했다면 hashCode 메서드 또한 재정의 해줘야 한다.

 

좋은 hascCode 메서드 작성 요령

  • 좋은 해시 함수는 서로 다른 인스턴스에 다른 해시코드를 반환한다.
  • 핵심 필드 : equals 비교에 사용된 필드
  1. int 변수 result를 선언한 후 값 c로 초기화한다. 이때 c는 해당 객체의 첫번째 핵심 필드를 계산한 해시코드다. 
  2. 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.
    1. 해당 필드의 해시코드 c를 계산한다.
      1. 기본 타입 필드라면, Type.hashCode(f)를 수행한다. 여기서 Type은 해당 기본 타입의 박싱 클래스다.
      2. 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다. 
      3. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 배열에 핵심 원소가 하나도 없다면 상수(0)를 사용한다. 
        모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
    2. 단계 2.1에서 계산한 해시코드 c로 result를 갱신한다.
      result = 31 * result + c;

  3. result를 반환한다.
    @Override
    public int hashCode() {
      int result = Integer.hashCode(areaCode);
      result = 31 * result + Integer.hashCode(prefix);
      result = 31 * result + Integer.hashCode(lineNum);
      return result;
    }

 

위 방식대로 hashCode 메서드를 재정의하였고, 아래는 그 결과다.

public static void main(String[] args) {

    PhoneNumber pn1 = new PhoneNumber(82, 123, 4567);
    PhoneNumber pn2 = new PhoneNumber(82, 123, 4567);

    System.out.println(pn1.hashCode()); // 87182
    System.out.println(pn2.hashCode()); // 87182
  }

 

위 방식 말고도 Objects 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인 hash를 제공한다/

이 메서드를 활용하면 hashCode 함수를 단 한 줄로 작성할 수 있다.

속도가 느리다는 단점이 있어 성능에 민감하지 않는 상황에서 사용하는 것을 추천한다.

 

    @Override
    public int hashCode() {
      return Objects.hash(areaCode, prefix, lineNum);
    }

 

아이템 12. toString을 항상 재정의하라

toString 메서드는 단순히 "클래스_이름@16진수로_표시한_해시코드" 를 반환한다.

toString의 일반 규약에 따르면 '간결하면서 사람이 읽기 쉬운 형태의 유익한 정보'를 반환해야 한다.

따라서 toString 규약에선  "모든 하위 클래스에서 이 메서드를 재정의하라" 고 말한다.

 

toString 메서드 재정의시 지킬 점

  • 실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다.
    • 반환 값이 Coi=PhoneNumber@12345 보다는 Coi=010-48.. 이 더 낫다.
  • 포맷을 명시하든 아니든 toString 반환 작성 의도는 명확히 밝혀야 한다. 
  • toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자
    • 만약 전화번호의 prefix, areaCode, lineNum을 얻어올 수 있는 API를 제공하지 않는다면 프로그래머는 toString 반환 값을 파싱해서 사용해야하기 때문이다.

toString을 제공할 이유가 없는 경우

  • 정적 유틸리티 클래스
  • 열거 타입 : 자바가 이미 완벽한 toString 제공하기 때문이다.

equals, hashCode와 같이 AutoValue 프레임워크에서 toString도 생성해준다.