본문 바로가기
BackEnd/Java

equals() 메서드와 hashCode() 메서드

by 규난 2023. 9. 16.
728x90

이번 포스트에서는 자바의 equals() 메서드와 hashCode()메서드에 대해서 알아보도록 하겠습니다.

 

자바를 사용하여 개발을 하는 개발자분들은 equals() 메서드를 한 번씩 봤거나 사용해 봤을 겁니다.

equals() 메서드는 두 객체가 같은 객체인지 비교할 때 사용하는 메소드입니다.

 

밑의 간단한 Member 클래스를 살펴보겠습니다.

public class Member {
    
    private String name;
    private String email;

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Member 클래스의 equals() 메서드가 존재하지 않는 거처럼 보이지만 기본적으로 자바의 모든 클래스는 Object 클래스를 자동으로 상속받게 되는데 부모 클래스인 Object 클래스에 equals() 메서드가 존재하기 때문에 자식 클래스(여기서는 Member 클래스)에서 override를 하지 않는 이상 Object에서 정의해둔 equals() 메서드를 그대로 사용하게 됩니다.

Object 클래스에 정의되어 있는 equals() 메서드

 

이제 같은 값을 가지는 Member 객체 두 개를 생성하고 equals() 메서드를 사용해서 같은 객체인지 비교해보도록 하겠습니다.

public class Main {

    public static void main(String[] args) {
        Member m1 = new Member("m1", "m1@gmail.com");
        Member m2 = new Member("m1", "m1@gmail.com");

        System.out.println(m1.equals(m2)); // false!!!
    }
}

개발자가 위 코드를 봤을 때 m1 객체의 값과 m2의 객체의 값이 같기 때문에 같은 객체임을 알 수가 있습니다.

하지만 실행 결과는 어떻게 나올까요??? 결과는 false가 나오게 됩니다.

 

false가 나오는 이유는 equals() 메서드를 override를 해주지 않았기 때문입니다.

자세히 설명을 하면 Member 클래스에서 equals() 메서드를 override 하지 않으면 Object 클래스에 정의되어 있는 equals() 메서드를 사용하게 되는데 이때 객체의 메모리 주소를 가지고 두 객체의 동일성을 비교하기 때문에 Member 클래스의 인스턴스 변수들의 값이 같더라도 서로 다른 생성자를 사용해 객체를 생성한 경우 메모리 주소 값이 달라 두 객체는 서로 다르다는 결과가 나오게 됩니다.

 

이러한 이유 때문에 객체에 대해서 서로 동일한 객체임을 판별하고 싶은 경우에는 반드시 equals() 메서드를 override 해줘야 합니다.

 

그럼 Member 클래스에서 equals() 메서드를 어떻게 override 해야 할까요??

equals() 메서드를 override 하기 전에 반드시 지켜야 할 다섯 가지 조건이 있습니다.

  1. 재귀(reflexive)
    • null이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true여야만 한다. 즉 자신을 equals() 메서드를 사용하여 비교 시 결과는 항상 true 여야만 한다.
  2. 대칭(symmetric)
    • null이 아닌 x, y 객체가 존재 시 x.equals(y)의 결과가 true 이면 y.equals(x) 결과도 true 여야만 한다.
  3. 타동적(transitive)
    • null이 아닌 x, y, z 객체가 존재 시 x.equals(y)의 결과가 true고, y.equals(z)의 결과가 true 이면 x.equals(z)의 결과도 true 여야만 한다.
  4. 일관(consistent)
    • null이 아닌 x, y 객체가 존재 시  객체가 변동되지 않은 상황에서 equals() 메서드를 몇 번을 호출하더라도 결과는 항상 true or false로 일관성이 있어야 한다.
  5. null과의 비교
    • null이 아닌 x 객체의 x.equals(null)의 결과는 항상 false여야만 한다.

개발자가 직접 위 다섯 가지 조건을 지키면서 equals() 메서드를 override 해줄 수 있지만 클래스의 인스턴스 변수들이 많아질수록 실수할 수 있는 가능성이 높아지기 때문에 IDE의 도움을 받아 equals() 메서드를 override 하는 것을 추천드립니다.

 

Intelli J IDE의 도움을 받아 override 한 equals() 메서드의 코드입니다.

이렇게 IDE의 도움을 받으면 위 다섯 가지 조건을 지키면서 equals() 메서드를 override 할 수 있습니다.

public class Member {

    private String name;
    private String email;

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // getter 생략 ...


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Member member = (Member) o;

        if (name != null ? !name.equals(member.name) : member.name != null)
            return false;
        return email != null ? email.equals(member.email) : member.email == null;
    }
}

equals() 메서드를 override 하고 위 코드를 다시 실행하면 어떤 결과가 나올까요?? 결과는 예상한 대로 true가 나오게 됩니다.

이렇게 두 객체가 같은 객체임을 판별해야 하는 경우에는 반드시 equals() 메서드를 override 해서 사용해서 사용해야 합니다.

equals() 메서드를 override 한 결과

 

equals() 메서드를 override 할 경우 꼭 hashCode() 메서드도 override 해야 한다는 말을 한 번쯤 들어봤을 겁니다.

정말로 꼭 hashCode() 메서드도 같이 override 해줘야 할까요?? 저의 생각은 "정말 꼭 override를 해야 하는 건 아니지만 하는 게 좋다"입니다.

이렇게 생각한 이유는 만든 클래스가 미래에도 hash 값을 사용하는 자료구조(HashSet, HashMap, HashTable)를 사용하는 일 없을 거야라고 한다면 hashCode() 메서드를 override 하지 않아도 되지만 미래에 요구 사항이 어떻게 바뀔지 모르기 때문에 hashCode() 메서드도 override를 해두는 것이 좋다고 생각했기 때문입니다.

 

equals() 메서드만 override 한 경우 hash 값을 사용하는 자료구조를 사용했을 경우 어떤 일이 발생하는지 예제를 보면서 확인해 보도록 하겠습니다. 현재 Member 클래스는 equals() 메서드만 override 하고 있고 hashCode() 메서드는 override 하지 않았습니다.

import java.util.HashMap;
import java.util.HashSet;

public class Main {

    public static void main(String[] args) {
    	Member m1 = new Member("m1", "m1@gmail.com");
        Member m2 = new Member("m1", "m1@gmail.com");
        
    	HashSet<Member> memberSet = new HashSet<>() {{
            add(m1);
            add(m2);
        }};
        System.out.println("memberSet size: " + memberSet.size());

        HashMap<Member, Integer> memberMap = new HashMap<>() {{
            put(m1, 1);
            put(m2, 2);
        }};
        System.out.println("memberMap size: " + memberMap.size());
    }
}

 

이 상태에서 밑의 코드를 실행하면 HashSet과 HashMap의 size를 출력하면 어떤 결과가 나오게 될까요??

hashCode() 메서드를 override 하지 않은 경우의 size 출력 값

Member 객체 m1, m2가 같은 객체임에도 불구하고 memberSet과 memberMap의 size를 출력 시 2가 출력됩니다.

이러한 결과가 나온 이유는 hash 값을 사용하는 자료구조를 사용하는 경우 값을 저장하기 전에 hashCode() 메서드를 사용해 key에 대한 hash 값을 가져오고 해당 자료구조를 순회하면서 hash 값이 같은지 확인 후 equals() 메서드를 사용해 key가 같은 객체임을 판단하는 과정을 거치게 되는데 이때 hash 값이 다르기 때문에 equals() 메서드를 사용해 두 객체에 대한 대한 결과가 true로 나오게 되더라도 다른 객체로 인식이 되어 위와 같은 결과가 나오게 되는 것입니다.

 

밑의 사진은 HashMap의 put() 메서드의 내부 동작을 캡처한 사진입니다.

put() 메서드 바디에 putVal() 메서드의 매개변수를 보시면 hash(key)라는 메서드를 볼 수 있는데 이 메서드의 내부 코드를 보면 hashCode() 메서드를 호출하는 것을 볼 수 있습니다. 그리고 putVal() 메서드의 else 문안의 코드를 보시면 hash 값과 equals() 메서드를 사용하여 key 값이 동일한 객체인지 판별하는 코드를 보실 수 있습니다.

HashMap의 put(), hash(), putVal() 메서드

 

equals() 메서드 처럼 hashCode() 메서드를 override 하기 전에  지켜야 하는 조건이 있습니다.

  1. 자바 애플리케이션이 수행되는 동안에 어떤 객체에 대해서 이 메소드가 호출될 때에는 항상 동일한 hash 값을 리턴해 주어야 한다.
  2. 어떤 두 개의 객체에 대하여 equals() 메서드를 사용하여 비교한 결과가 true일 경우에, 두 객체의 hashCode() 메서드를 호출하면 동일한 hash 값을 리턴해야 한다.
  3. 두 객체의 equals() 메서드를 사용하여 비교한 결과가 false 여도 hashCode() 메소드를 호출한 결과의 hash 값이 무조건 달라야 할 필요는 없다. 하지만 이런 경우에는 서로 다른 hash 값을 제공하면 hash table의 성능을 향상시키는데 도움 된다.

equals() 메서드와 마찬가지로 hashCode() 메서드도 IDE 도움을 받아 override를 할 수 있습니다.

Intelli J IDE의 도움을 받아 override 한 hashCode() 메서드의 코드입니다.

이렇게 IDE의 도움을 받으면 위 조건을 지키면서 hashCode() 메서드를 override 할 수 있습니다.

public class Member {

    private String name;
    private String email;

    public Member(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // getter 생략 ...
    // equals() 메서드 생략 ...

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (email != null ? email.hashCode() : 0);
        return result;
    }
}

hashCode() 메서드를 override 하고 위 코드를 다시 실행하면 어떤 결과가 나올까요??

결과는 예상한 대로  memberSet, memberMap의 size 출력시 1이 나오게 됩니다.

hashCode() 메서드를 override 한 경우의 size 출력 값

 

항상 아무 생각 없이 써오던 equals(), hashCode() 메서드에 대해서 공부해보니 이 두 메서드에 대해서 override가 왜 중요한지 깨닫게 되는 좋은 시간이었습니다.

728x90

'BackEnd > Java' 카테고리의 다른 글

자바 Enum 타입 속 모든 비밀: 정의, 컴파일러, 싱글톤  (0) 2023.10.25
BigDecimal 사용 이유  (0) 2023.10.19
Java Virtual Machine  (2) 2023.10.14
Stream  (0) 2023.08.20
람다 표현식  (0) 2023.08.16