glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-009] @Override 어노테이션과 HashSet 동작 탐구 #176

Closed glenn-syj closed 1 month ago

glenn-syj commented 1 month ago

Based on: #172 by @yngbao97

들어가며

@Override 어노테이션이 있을 때와 없을 때에 대해서 HashSet의 add 메서드가 어떻게 동작하는지 실험을 통해서 잘 보여주셨는데요. 그런데 사실은 어노테이션보다는 파라미터 타입 때문에 실험이 잘못된 결과를 보여준 것 같습니다.

HashSet 또 다른 실험


package java_test;

import java.util.HashSet;

class Person {
    String name;
    int birth;

    Person(String name, int birth) {
        this.name = name;
        this.birth = birth;
    }

    // 이름의 길이와 생년월일이 같다면 같은 객체로 인식하도록 임시로 정했다.
    public int hashCode() {
        return name.length()*birth;
    }

    // Object 내의 equals와 동일한 코드
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return name.equals(person.name);
    }
}

public class HashSetExample {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();

        Person p1 = new Person("John", 25);
        Person p2 = new Person("John", 25);

        set.add(p1);
        set.add(p2);

        // 중복 처리가 되었는지 확인
        System.out.println(set);
    }
}

Person 내 equals 메서드를 Object 클래스의 equals와 동일하게 바꾼 위 코드의 경우, set에 단 한 개의 원소만이 있다고 뜹니다. 즉, 처음에 equals 메소드로 중복 처리를 못한 까닭은 어노테이션이라기보다는 메서드 자체에 있다고 할 수 있겠습니다. 이는 아래 코드에서 더 명확해 집니다.

package java_test;

import java.util.HashSet;

class Person {

    private String name;
    private int birth;

    public Person(String name, int birth) {
        this.name = name;
        this.birth = birth;
    }

    // 이름의 길이와 생년월일이 같다면 같은 객체로 인식하도록 임시로 정했다.
    public int hashCode() {
        return name.length()*birth;
    }

    // Object의 equals를 override하지 않고 다르게 이용
    public boolean equals(Object p) {

        if ((Person) p instanceof Person) {
            Person p1 = (Person) p;
            return p1.name.equals(name);
        }

        return false;
    }

}
public class HashSetExample {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();

        Person p1 = new Person("John", 25);
        Person p2 = new Person("John", 25);

        set.add(p1);
        set.add(p2);

        // 중복 처리가 되었는지 확인
        System.out.println(p1.equals(p2));
        System.out.println(set);
    }
}

위 코드에서는 equals가 @yngbao97 님의 예시에 맞게 수정되었습니다. (실험을 위한 코드니, 예외 처리는 넘어가주시길 바랍니다!) 그러니까, 이름으로 같은지 아닌지를 판단하게 됩니다. 결론적으로는 다음과 같은 결과를 출력합니다.

true
[java_test.Person@64]

이를 다시 말하자면, 결국 @Override 어노테이션과 상관없이 HashSet이 이용하는 메서드의 정의명이나 반환값, 파라미터 등의 메타데이터가 결과에 영향을 미침을 알 수 있습니다.

나가며

@yngbao97 님께서 실험해주지 않으셨다면, 저도 이렇게 실험을 해 볼 일이 없을 것 같아 다시 한 번 감사를 드리고 싶네요!

더 재밌는 건 HashSet도 내부에서 HashMap을 이용해 중복 처리를 진행하더라구요. 당연하게도 HashMap에서는 Key의 hash값을 비교하는 데 Object를 파라미터로 받는 equals를 쓰는 것이구요. 여기에서 쓰이는 equals는 클래스에서 새로 정의된 같은 메타 데이터의 equals가 있다면 해당 스코프의 equals가 우선순위를 차지합니다.

결론적으로는, @Override 어노테이션은 프로그래머의 실수를 컴파일 타임에 방지할 수 있는 기능을 제공한다고 보아도 좋겠습니다.

yngbao97 commented 1 month ago

@glenn-syj 님, 중요한 부분을 다시한번 깔끔하게 정리해주셔서 감사해요!! 실험을 통해 @Override 어노테이션은 프로그래머의 실수를 컴파일 타임에 방지하기 위한 기능이라는 것을 인지했음에도 의도가 글에 잘 표현되지 못했던 것 같습니다! 추후에 다시 글을 읽을 때 잘못 이해할 수 있는 오해의 소지가 있었는데, 필요한 부분을 짚어주셨네요ㅠ 추가 실험과 깔끔한 정리 감사드립니다😁👍🏻