반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자바 디자인패턴
  • 프로그래머의 뇌
  • 싱글톤 패턴
  • 자바
  • 디자인패턴
  • 참조 해제
  • Javadoc 자바독 자바주석 주석 Comment
  • 토비의 스프링
  • item9
  • 싱글턴
  • 이펙티브 자바 item9
  • 메이븐 골
  • rust
  • serverless computing
  • 싱글톤
  • 알고리즘
  • 작업기억공간
  • 느린 쿼리
  • 자바스크립트 면접
  • item8
  • 플라이웨이트패턴
  • 자바스크립트 인터뷰
  • try-with-resources
  • 스프링 검증
  • Pre-rendering
  • 러스트
  • 자료구조
  • 추상 팩터리 패턴
  • Java
  • 도커공식문서
  • 이펙티브자바
  • Next.js
  • next js app
  • prerendering
  • pnpm
  • 메이븐 페이즈
  • 자바스크립트
  • 외래키 제약조건
  • 슬로우 쿼리
  • 서버리스 컴퓨팅
  • 자바 검증
  • 빈 검증
  • 객체복사
  • 팩터리 메서드 패턴
  • 이펙티브 자바
  • NEXT JS
  • 메이븐 라이프사이클
  • item7
  • bean Validation
  • MSSQL

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

Java/이펙티브 자바

이펙티브 자바, 쉽게 정리하기 - item 11. equals를 재정의하려거든 hashCode도 재정의하라

2021. 12. 29. 22:40

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

hashCode 일반 규약

  • equals()를 재정의한 클래스 모두에서 hashCode()도 재정의해야 한다.
    • 그렇지 않으면, HashMap 혹은 HashSet의 원소가 되었을 때 문제가 발생할 수 있다.
  • equals() 비교에 사용되는 필드가 변하지 않았다면, hashCode() 메서드는 몇번을 호출하든, 항상 같은 값을 반환해야 한다.
    • 단, 애플리케이션을 재시작한 경우에는 달라질 수 있다.
  • equals(Object)가 두 값을 같다고 판단했다면, hashCode()의 반환 값도 같아야 한다.
  • equals(Object)가 두 객체를 다르다고 판단했더라도, hashCode()가 달라질 필요는 없다.
    • 단, 해시테이블 성능 최적화를 위해서 다르게 나오는 것이 좋다.

equals()만 오버라이드 하였을 때

public class Item11Test {
    static class PhoneNumber {
        public final String areaCode;
        public final String prefix;
        public final String lineNum;

        public PhoneNumber(String areaCode, String prefix, String lineNum) {
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNum = lineNum;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            PhoneNumber that = (PhoneNumber) o;
            return Objects.equals(areaCode, that.areaCode) && Objects.equals(prefix, that.prefix) && Objects.equals(lineNum, that.lineNum);
        }
    }

    @Test
    public void hashMapTest1() {
        HashMap<PhoneNumber, String> hashMap = new HashMap<>();

        PhoneNumber jennyPhoneNumber1 = new PhoneNumber("010", "0001", "0000");
        hashMap.put(jennyPhoneNumber1, "제니");
        String s1 = hashMap.get(jennyPhoneNumber1);
        System.out.println("s1 = " + s1); // 제니

        PhoneNumber jennyPhoneNumber2 = new PhoneNumber("010", "0001", "0000");
        String s2 = hashMap.get(jennyPhoneNumber2);
        System.out.println("s2 = " + s2); // null

        boolean equals = jennyPhoneNumber1.equals(jennyPhoneNumber2);
        System.out.println("equals = " + equals); // true

        int hashCode1 = jennyPhoneNumber1.hashCode();
        int hashCode2 = jennyPhoneNumber2.hashCode();
        System.out.println("hashCode1 = " + hashCode1); // hashCode1 = 611563982
        System.out.println("hashCode2 = " + hashCode2); // hashCode2 = 336484883
    }
}
  • equals()만 오버라이드 하는 경우 equals() 값은 올바르게 판명된다.
  • HashMap에서는 내용이 같음에도 같은 키라고 인정받지 못한다.

hashCode() 메서드를 구현하였을 때

@Override
public int hashCode() {
    return 40;
}
  • hashCode를 구현했다. 비록 40만 반환하지만, 나름 동작은 한다.
@Test
public void hashMapTest1() {
    HashMap<PhoneNumber, String> hashMap = new HashMap<>();

    PhoneNumber jennyPhoneNumber1 = new PhoneNumber("010", "0001", "0000");
    hashMap.put(jennyPhoneNumber1, "제니");
    String s1 = hashMap.get(jennyPhoneNumber1);
    System.out.println("s1 = " + s1); // 제니

    PhoneNumber jennyPhoneNumber2 = new PhoneNumber("010", "0001", "0000");
    String s2 = hashMap.get(jennyPhoneNumber2);
    System.out.println("s2 = " + s2); // 제니

    boolean equals = jennyPhoneNumber1.equals(jennyPhoneNumber2);
    System.out.println("equals = " + equals);

    int hashCode1 = jennyPhoneNumber1.hashCode();
    int hashCode2 = jennyPhoneNumber2.hashCode();
    System.out.println("hashCode1 = " + hashCode1); // 40
    System.out.println("hashCode2 = " + hashCode2); // 40

    PhoneNumber paulPhoneNumber = new PhoneNumber("010", "0001", "0001");
    hashMap.put(paulPhoneNumber, "폴");

    String paul = hashMap.get(paulPhoneNumber);
    System.out.println("paul = " + paul); // 폴
}
  • 위의 코드가 모두 정상적으로 동작한다.
    • 그 이유는 hashMap이 동일한 key인지 판단할 때, equals()와 hashCode() 두 메서드 모두에 의존하기 때문이다.
      • 하나라도 다르면, 다른 key로 간주한다.

hashCode()에서 계속 똑같은 숫자를 반환해도 동작은 하지만, 원소가 늘어날수록 해시 테이블의 성능이 매우 떨어진다. O(1)의 시간복잡도가 점차 링크드리스트처럼 O(n)이 되어갈 것이다.

좋은 해시함수는 32비트 정수 범위에 인스턴스들을 고루 분배해야 한다.

hashCode() 메서드를 구현하는 요령

핵심필드란 equals() 비교에 사용되는 필드이다.

  • 핵심 필드에 아래 작업을 수행한다.
    • 기본 타입 필드면, Type.hashCode(f)를 수행한다.
      • Type이란, 기본 타입의 박싱 클래스를 말한다.
    • 참조 타입 필드면서, 이 클래스의 equals() 메서드가 이 필드의 equals() 메서드를 재귀적으로 호출한다면, 필드의 hashCode()를 재귀적으로 호출하면 된다. (보통 equals()가 있다는 건 hashCode()도 올바른 방식으로 구현했음을 말한다.)
      • 계산이 더 복잡해질 것 같으면, 표준형을 만들어 표준형의 hashCode()를 호출한다.
      • 필드의 값이 null이면 0을 사용한다.
    • 배열이라면, 핵심원소 각각을 별도 필드처럼 다룬다.
      • 배열에 핵심원소가 하나도 없다면 0(권장) 혹은 다른 상수를 사용한다.
      • 모든 원소가 핵심 원소라면 Arrays.hashCode()를 사용한다.
  • 위의 작업에서 계산한 해시코드로 result를 갱신한다.
    • 첫 필드 값이라면 할당
      • int result = c
    • 첫 필드 값이 아니라면 갱신
      • result = 31 * result + c
  • result를 반환한다.

구현된 해시코드

@Override
public int hashCode() {
    int result = areaCode.hashCode();
    result = 31 * result + prefix.hashCode();
    result = 31 * result + lineNum.hashCode();

    return result;
}
  • 32비트 정수 범위에 균일하게 배치하려고 약간의 연산을 추가한 것 말고는 딱히 어려울 건 없다.
    • 31 * result는 필드를 곱하는 순서에 따라 result 값이 달라지게 만들어준다. 비슷한 필드가 여러개일 때 효과가 크다.
  • 필드의 타입에 따라 구분되는 해시 값을 잘 사용하면 되고, 대부분은 클래스에 이미 구현되어 있다.
  • 이전의 URL 클래스처럼 비결정적 요소가 존재하면 매번 결과가 달라지므로, 규약을 위반하게 되니 주의하자.

equals()에 사용되지 않은 필드는 '반드시' 제거해야 한다. 안그러면 규약을 어기게 된다.

한 줄로 hashCode() 구현하기

Objects.hash(lineNum, prefix, areaCode);
  • Objects에서는 이미 훌륭한 정적 메서드를 제공한다.
    • 성능은 약간 떨어지지만 사용하는데 전혀 문제가 없다.
      • 입력 인수를 넣을 배열을 만들고, 기본 타입이 있다면 박싱하는 정도의 비효율만 있다.

해시코드를 생성하는 비용이 크다면 LazyLoad와 Cache를 적용할 수 있다.

private int hashCode;

@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;
}
  • 해시코드 생성 비용이 크다면, 한번 필드에 값을 저장해놓고 재활용해도 된다.
  • LazyLoad도 되고 있어 불리지 않는 한 쓸모 없는 연산은 하지 않을 것이다.

위와 같이 구현하는 경우 반드시 스레드 안전하게 만들도록 신경써주자. 초기 생성 시에는 싱크를 맞춰주어야 한다.

성능을 높이기 위해 hashCode() 메서드에 들어가는 필드를 빼지 마라

  • 당장 hashCode() 메서드의 성능은 좋아질 수 있으나, 많은 필드를 사용하는 것은 해시 값을 32 비트 정수로 고루 퍼지게 만드는데 도움을 주기 때문에 무작정 필드를 빼지 말자. 해시 테이블에서 심각한 비효율이 날 수 있다.

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자

그래야 클라이언트는 이 값에 의지하지 않고, 계산방식을 바꿀 수도 있다.

핵심 정리

  • equals()를 재정의할 때는 반드시 hashCode()도 재정의하자.
  • hashCode()를 정의할 때도 반드시 일반 규약을 따르자.
  • 다른 인스턴스의 hashCode()는 최대한 32비트 정수 안에서 고르게 나오게 하도록 노력하자.

사실 요즘은 직접 만들기보다 IDE의 도움을 받아 만드는 것이 좋다.

반응형
저작자표시 (새창열림)

'Java > 이펙티브 자바' 카테고리의 다른 글

이펙티브 자바, 쉽게 정리하기 - item 13. clone 재정의는 주의해서 진행하라  (0) 2021.12.30
이펙티브 자바, 쉽게 정리하기 - item 12. toString을 항상 재정의하라  (0) 2021.12.30
이펙티브 자바, 쉽게 정리하기 - item 10. equals는 일반 규약을 지켜 재정의하라  (0) 2021.12.29
이펙티브 자바, 쉽게 정리하기 - item9. try-finally보다는 try-with-resources를 사용하라  (0) 2021.12.28
이펙티브 자바, 쉽게 정리하기 - item8. finalizer와 cleaner 사용을 피하라  (0) 2021.12.27
    'Java/이펙티브 자바' 카테고리의 다른 글
    • 이펙티브 자바, 쉽게 정리하기 - item 13. clone 재정의는 주의해서 진행하라
    • 이펙티브 자바, 쉽게 정리하기 - item 12. toString을 항상 재정의하라
    • 이펙티브 자바, 쉽게 정리하기 - item 10. equals는 일반 규약을 지켜 재정의하라
    • 이펙티브 자바, 쉽게 정리하기 - item9. try-finally보다는 try-with-resources를 사용하라
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바