본문 바로가기

이팩티브 자바

[Effective] equals & hashCode

이번 글에서는 equalshashCode에 대해서 다뤄볼 예정이다.

equals 메서드란? 

두 인스턴스의 주소 값을 비교하여 같은 인스턴스인지를 확인하고, 같다면 true 다르다면 false를 반환한다.

 

public class EX {
    public static void main(String[] args) {
        Person test1 = new Person("Test1");
        Person test2 = new Person("Test1");

        System.out.println(test1.equals(test1)); // true
        System.out.println(test2.equals(test1)); // false
    }

    public static class Person {
        private String name;

        public Person(final String name) {
            this.name = name;
        }
    }
}

test1과 test2는 이름은 같지만, 객체의 주소가 다르기 때문에 equals 비교 시 false가 출력된다.

 

equals 메서드 재정의해야 하는 상황

❕❕ 싱글톤 객체가 아니면서, 논리적인 동치성을 확인하고 싶을 때, 재정의한다.

즉, 다른 객체이지만 같은 정보를 가지고 있다면 동일하다고 판단해야 할 때!

 

equals의 일반 규약

null이 아닌 모든 참조 값 x, y, z에 대해서,

반사성: x.equals(x) // true -> 자기 자신과 같아야 한다.

대칭성: x.equals(y) // true 일 때, y.equals(x) // true -> 서로에 대한 동치 여부가 같아야 한다.

추이성: x.equals(y) // true 이고, y.equals(z) // true이면, x.equals(z) // true -> x.equals(y)가 참이고 y.equals(x)가 참이면, x.equals(z)는 참이다.

일관성: x.equals(y) // true 반복해도 값이 동일 -> x.equals(y)가 참일 때, 반복해서 호출해도 결과는 항상 true여야 한다.

non-null: x.equals(null) // false -> NullpointerException이 발생하지 않고 false를 반환해야 한다.

 

equals의 좋은 재정의 방법

public class EX {
    public static void main(String[] args) {
        Person test1_1 = new Person("Test1", 10);
        Person test1_2 = new Person("Test1", 10);
        Person test1_3 = new Person("Test1", 10, "SOMTHING");

        System.out.println(test1_1.equals(test1_1)); // true
        System.out.println(test1_1.equals(test1_2)); // true
        System.out.println(test1_2.equals(test1_1)); // true
        System.out.println(test1_1.equals(test1_3)); // false
    }

    public static class Person {
        private String name;
        private int age;
        private String department = null;

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

        public Person(final String name, final int age, final String department) {
            this.name = name;
            this.age = age;
            this.department = department;
        }

        @Override
        public boolean equals(final Object o) {
            // 1. == 연산자를 통해 자기 자신의 참조인지 확인
            if (o == this) {
                return true;
            }

            // 2. instanceof 연산자로 입력이 올바른 타입인지 확인
            if (!(o instanceof Person)) {
                return false;
            }

            // 3. 입력을 올바른 타입으로 형변환
            Person person = (Person) o;

            // 4. 필드가 참조 타입일 경우 equals()로 비교
            if (!this.name.equals(person.name)) {
                return false;
            }

            // 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
            if (this.age != person.age) {
                return false;
            }

            // 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
            return Objects.equals(this.department, person.department);
        }
    }
}

 

equals 재정의 시 참고 사항

◼ 논리적인 동치성을 비교하는 것이 아니면 재정의하지 말자.

◼ 재정의하는 경우 위의 일반 규약을 지키면서, 핵심 필드를 전부 포함시키자.

◼ 핵심 필드를 포함시킬 때, 다를 경우가 많은 필드부터 비교하는 것이 성능에 좋다.

◼ 객체의 논리적인 상태와 관련 없는 필드는 비교하면 안된다.

 

 

hashCode 메서드란? 

객체를 식별할 수 있는 유니크 값을 반환하는 메서드로, 반환된 값은 중복되지 않는 고유의 값이다.

 

public class EX {
    public static void main(String[] args) {
        Person test1_1 = new Person("Test1", 10);
        Person test1_2 = new Person("Test1", 10);

        System.out.println(test1_1.equals(test1_1)); // true
        System.out.println(test1_1.equals(test1_2)); // true

        System.out.println(test1_1.hashCode()); // 2003749087
        System.out.println(test1_2.hashCode()); // 1324119927
    }

    public static class Person {
        private String name;
        private int age;
        private String department = null;

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

        public Person(final String name, final int age, final String department) {
            this.name = name;
            this.age = age;
            this.department = department;
        }

        @Override
        public boolean equals(final Object o) {
            // 1. == 연산자를 통해 자기 자신의 참조인지 확인
            if (o == this) {
                return true;
            }

            // 2. instanceof 연산자로 입력이 올바른 타입인지 확인
            if (!(o instanceof Person)) {
                return false;
            }

            // 3. 입력을 올바른 타입으로 형변환
            Person person = (Person) o;

            // 4. 필드가 참조 타입일 경우 equals()로 비교
            if (!this.name.equals(person.name)) {
                return false;
            }

            // 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
            if (this.age != person.age) {
                return false;
            }

            // 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
            return Objects.equals(this.department, person.department);
        }
    }
}

◼ 해당 코드를 살펴보면, equals 비교 시 참이지만 hashCode를 비교하면 다른 값이 출력된다.

 

hashCode 메서드를 정의하지 않는다면?

public class EX {
    public static void main(String[] args) {
        Person 원본 = new Person("Test1", 10);
        Person 비교대상 = new Person("Test1", 10);
        System.out.println(원본.equals(비교대상)); // true
        
        Set<Person> personSet = new HashSet();
        personSet.add(원본);
        
        System.out.println(personSet.contains(비교대상)); // false
    }

    public static class Person {
        private String name;
        private int age;
        private String department = null;

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

        public Person(final String name, final int age, final String department) {
            this.name = name;
            this.age = age;
            this.department = department;
        }

        @Override
        public boolean equals(final Object o) {
            // 1. == 연산자를 통해 자기 자신의 참조인지 확인
            if (o == this) {
                return true;
            }

            // 2. instanceof 연산자로 입력이 올바른 타입인지 확인
            if (!(o instanceof Person)) {
                return false;
            }

            // 3. 입력을 올바른 타입으로 형변환
            Person person = (Person) o;

            // 4. 필드가 참조 타입일 경우 equals()로 비교
            if (!this.name.equals(person.name)) {
                return false;
            }

            // 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
            if (this.age != person.age) {
                return false;
            }

            // 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
            return Objects.equals(this.department, person.department);
        }
    }
}

◼ 원본과 비교대상은 같다고 판단해서(equals로 인해) HashSet에 원본을 넣고 비교대상이 존재하는지 확인한다면, false가 출력되게 된다.

 

hashCode 메서드를 정의한다면?

public class EX {
    public static void main(String[] args) {
        Person 원본 = new Person("Test1", 10);
        Person 비교대상 = new Person("Test1", 10);
        System.out.println(원본.equals(비교대상)); // true
        
        Set<Person> personSet = new HashSet();
        personSet.add(원본);
        
        System.out.println(personSet.contains(비교대상)); // true
    }

    public static class Person {
        private String name;
        private int age;
        private String department = null;

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

        public Person(final String name, final int age, final String department) {
            this.name = name;
            this.age = age;
            this.department = department;
        }

        @Override
        public boolean equals(final Object o) {
            // 1. == 연산자를 통해 자기 자신의 참조인지 확인
            if (o == this) {
                return true;
            }

            // 2. instanceof 연산자로 입력이 올바른 타입인지 확인
            if (!(o instanceof Person)) {
                return false;
            }

            // 3. 입력을 올바른 타입으로 형변환
            Person person = (Person) o;

            // 4. 필드가 참조 타입일 경우 equals()로 비교
            if (!this.name.equals(person.name)) {
                return false;
            }

            // 5. float과 double 타입이 아닐 경우 == 연산자로 비교를 진행
            if (this.age != person.age) {
                return false;
            }

            // 6. null 값이 정상 값으로 취급된다면, Objects.equals()로 NullPointException 예방
            return Objects.equals(this.department, person.department);
        }
        
        @Override
        public int hashCode() {
            return Objects.hash(this.name, this.age, this.department);
        }
    }
}

◼ 원본 객체를 HashSet에 넣고 비교대상 객체로 포함되어 있는지 확인해도 true가 반환된다. 

 

hashCode 메서드를 재정의할 때 고려사항

◼ Objects.hash() 메서드의 내부 구현이다.

◼ 핵심 필드를 빠트리지 않고 넣고 해당 메서드를 사용하는 것이 좋다.

◼ HashCode 값의 생성 규칙을 API 사용자에게 자세히 공표해서는 안된다.

'이팩티브 자바' 카테고리의 다른 글

[Effective] try-with-resources  (1) 2023.03.03
[Effective] 정적 팩터리 메서드  (0) 2023.03.02