본문 바로가기
Java

[Java] equals(), hashCode(), toString() 재정의 이유와 방법 (2)

by 노력남자 2021. 4. 16.
반응형

이번 포스팅에선 hashCode(), toString() 재정의 이유와 방법에 대해 알아보겠습니다.

 

equals()에 대해 궁금하신 분은 이전 포스팅을 참고바랍니다.

 

 

[Java] equals() (+ == 와 차이점), hashcode(), toString() 재정의 이유와 방법 (1)

많은 분들은 그냥 equals(), hashcode(), toString() 이거 그냥 쓰면 되는 거 아니야?? 왜 재정의를 해야 해? 사실 저거 뭐 어떻게 쓰는지도 몰라~라고 하시는 분들이 있습니다. 저도 예전엔 그러기도 했구

effortguy.tistory.com

 

hashCode()

 

뭐하는 메소드인가요?

 

참조형 객체는 Object를 기본으로 상속받고 있는데 Object에 있는 hashCode()를 보면 객체의 해시코드를 가지고 온다고 나와있습니다.

 

 

어디서 사용하나요?

 

대표적으로 Map의 구현체인 HashMap, HashTable, LinkedHashMap에서 사용합니다.

 

Map에 키 타입을 primitive가 아닌 참조형으로 줬을 때 사용됩니다.

 

Student를 키 타입으로 하는 HashMap의 put 메소드로 예를 들어보겠습니다.

 

put 로직은 객체의 hashcode()로 값이 같은지 확인 후 equals()로 한 번 더 확인을 합니다.

 

@Test
void test_student_hashcode() {
    Student student = Student.builder().id("effort").name("guy").build();
    Student student2 = Student.builder().id("effort").name("guy").build();

    Map<Student, String> studentMap = new HashMap();

    studentMap.put(student, student.getName());
    studentMap.put(student2, student.getName());

    assertThat(studentMap.size(), is(2));
}

 

hashCode 규약

 

hashcode 재정의 이유를 보기 전에 규약부터 보고 가겠습니다.

 

1) equals() 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashcode()는 항상 같은 값을 반환해야한다.

2) equals()로 비교했을 때 같다고 판단했다면, 두 객체의 hashCode는 같은 값을 반환해야 한다.

3) equals()가 두 객체를 다르다고 판단한 경우 꼭 hashCode가 다를 필요는 없다. 다른 값을 반환하는 게 좋다.

 

hashCode 재정의 이유

 

equals()를 재정의 하면 hashCode()를 재정의 해야합니다.

 

이유는 규약 2)를 보시면 equals()로 비교했을 때 같다고 판단했다면, 두 객체의 hashCode는 같아야 한다고 했습니다.

 

만약 Student 객체의 equals()를 id와 name이 같으면 같은 객체라고 수정하고 hashCode는 수정하지 않으면, 여전히 다른 객체라고 판단합니다. << 규약 2) 위반

 

논리적 동치성이 같은 객체인데도 studentMap.size()는 2가 나옵니다. << 규약 2) 위반

 

@Test
void test_student_hashcode() {
    Student student = Student.builder().id("effort").name("guy").build();
    Student student2 = Student.builder().id("effort").name("guy").build();

    Map<Student, String> studentMap = new HashMap();

    studentMap.put(student, student.getName());
    studentMap.put(student2, student.getName());

    assertTrue(student.equals(student2));
    assertThat(studentMap.size(), is(1)); // 테스트 실패 
}

 

hashCode 재정의 방법

 

1) int 변수 result를 선언한 후 1로 초기화 한다.

2) 해당 객체 핵심 필드들을 돌면서 hashcode를 계산후 result = result * 59 + 핵심필드 == null ? 43 : $stringMap.hashCode() 로 계산한다.

 

재정의 코드

 

아래 재정의 코드는 Lombok의 @EqualsAndHashCode 어노테이션 사용 시 생성되는 코드입니다.

 

Lombok도 보면 @Equals, @HashCode 어노테이션이 별개로 있는 게 아니라 같이 있는 걸 볼 수 있는데 그게 다 위에서 설명한 이유 때문입니다.

 

@Getter
@EqualsAndHashCode
public class Student {
    String id;
    String name;
 
    @Builder
    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

public int hashCode() {
    int PRIME = true;
    int result = 1;
    Object $id = this.getId();
    int result = result * 59 + ($id == null ? 43 : $id.hashCode()); // id
    Object $name = this.getName();
    result = result * 59 + ($name == null ? 43 : $name.hashCode()); // name
    return result;
}

 

재정의한 hashCode로 다시 테스트해보겠습니다.

 

이젠 hashCode()로도 논리적으로 같은 값을 판별할 수 있게 됐습니다.

 

@Test
void test_student_hashcode() {
    Student student = Student.builder().id("effort").name("guy").build();
    Student student2 = Student.builder().id("effort").name("guy").build();

    Map<Student, String> studentMap = new HashMap();

    studentMap.put(student, student.getName());
    studentMap.put(student2, student.getName());

    assertTrue(student.equals(student2));
    assertThat(studentMap.size(), is(1)); // << 같은 값으로 판별 2 -> 1
}

 

toString()

 

뭐하는 메소드인가요?

 

이 메소드도 Object에 기본으로 있는 메소드입니다.

 

객체를 String 형태로 쉽게 볼 수 있게 해주는 메소드입니다.

 

개발을 쉽게할 수 있도록 도와주는 메소드입니다.

 

언제 재정의 해야 하나요?

 

객체의 필드 정보를 손 쉽게 보고싶을 때 재정의하면 됩니다.

 

아까 위에서 정의한 Student를 재사용하겠습니다.

 

toString을 재정의하지 않고 Student의 toString()을 찍어보겠습니다.

 

@Test
void test_student_toString() {
    Student student = new Student("effort", "guy");

    System.out.println(student.toString());
}

 

찍으면 그냥 인스턴스명만 나옵니다. 저는 Stduent 안에 있는 id, name 값을 보고싶습니다.

 

 

재정의 방법

 

toString()은 딱히 재정의 방법이 정해져있지 않습니다.

 

재정의 코드

 

아래 재정의 코드는 Lombok의 @ToString 어노테이션 사용 시 생성되는 코드입니다.

 

@Getter
@EqualsAndHashCode
@ToString
public class Student {
    String id;
    String name;

    @Builder
    public Student(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

public String toString() {
	return "Student(id=" + this.getId() + ", name=" + this.getName() + ")";
}

 

재정의된 toString()을 다시 찍어보겠습니다.

 

이제 제가 원하는대로 나오네요.

 

 

toString()은 앞서 설명드린 equals(), hashCode()보단 가벼운 메소드입니다. 하지만 개발하면서 엄청 편하게 쓸 수 있는 메소드기 때문에 꼭 재정의 해서 쓰시길 바랍니다.

 

equals(), hashCode(), toString() 메소드 정리가 끝났습니다.

 

equals(), hashCode() 같은 경우 HashMap, HashTable, LinkedHashMap 이외에도 다른 곳에서도 쓰일 수 있으니 재정의해서 쓰는게 좋을 거 같습니다. 당연히 equals(), hasCode() 가 재정의 되어있겠지 하고 호출하는 라이브러리가 있을 수도 있기 때문이죠.

 

정리하는데 생각보다 오래 걸리네요. 다음 포스팅에서 만나요~

 

참고

  • Joshua Bloch, Effective Java 3/E, 인사이트, 2018
반응형

댓글