응애개발자
article thumbnail
Published 2024. 5. 5. 16:10
[Java] equals(), hashCode() CS/Java
728x90

📌 equals(), hashCode()

equals와 hashCode는 모든 객체의 부모 객체인 Object클래스에 정의되어 있습니다. 따라서 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있습니다.

 

📌 equals()

==연산자와 equals() 메서드의 가장 큰 차이점은 == 연산자는 비교하고자 하는 두개의 대상의 주소값을 비교하는데 반해  equals()는 비교하고자 하는 두개의 대상의 값 자체를 비교합니다. (일치 = true, 불일치 = false) 

 

 

String에서의 equals() 예시 코드

public class Hello {
    public static void main(String[] args) {
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = new String("Hello");
        String s4 = new String("Hello");

        System.out.println(s1==s2);
        System.out.println(s1.equals(s2));
        System.out.println(s3==s4);
        System.out.println(s3.equals(s4));
    }
}


//출력 결과
true
true
false
true

s1 == s2 출력이 true

s1과 s2 둘 다 같은 리터럴 "Hello"를 참조하고 있습니다. Java에서 문자열 리터럴은 스트링 풀에서 관리되므로, 동일한 리터럴은 메모리 상에서 같은 위치를 참조합니다. 따라서 s1과 s2는 동일한 메모리 주소를 가리키기 때문에 == 연산 결과는 true가 됩니다.

 

s1.equals(s2) 출력이 true

equals() 메소드는 객체의 내용을 비교합니다. s1과 s2 둘 다 "Hello"라는 같은 문자열 내용을 가지고 있기 때문에, .equals()는 내용이 동일하다고 판단하여 true를 반환합니다.

 

s3 == s4 출력이 false

s3와 s4는 new String("Hello")를 사용하여 각각 생성된 별도의 String 객체입니다. 이 방식으로 생성된 문자열은 스트링 풀을 사용하지 않고, 힙 영역에 각각 독립적인 객체로 메모리가 할당됩니다. 따라서 s3와 s4는 서로 다른 메모리 주소를 가리키게 되므로, == 연산 결과는 false입니다.

 

s3.equals(s4) 출력이 true

equals() 메소드는 String 클래스에서 문자열의 내용을 비교하기 위해 오버라이드 되어 있습니다. s3와 s4가 참조하는 문자열 내용은 둘 다 "Hello"입니다. 따라서 .equals()는 두 문자열의 내용이 동일하다고 판단하여 true를 반환합니다.

 

String에서의 equals()

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        return (anObject instanceof String aString)
                && (!COMPACT_STRINGS || this.coder == aString.coder)
                && StringLatin1.equals(value, aString.value);
    }

이렇듯String에서 equals() 메서드는 Object에 있는 equals() 메서드를 오버라이드(재정의) 하여 객체 자신과의 비교타입 검사를 수행하고, 문자열의 인코딩 방식과 실제 내용이 같은지를 체크하여 두 문자열이 같은지 판단합니다. 이러한 방식은 문자열 비교에 있어서 효율성과 정확성을 보장합니다.

더보기
String equals() 메서드 동작 과정
  1. 자기 자신과의 비교:
    • 먼저 메서드는 객체가 자기 자신과 비교되고 있는지 확인합니다 (if (this == anObject)). 이것은 최적화 단계로, 같은 객체의 참조인 경우 빠르게 true를 반환합니다. 이 경우 추가적인 비교를 수행할 필요가 없습니다.
  2. 타입 체크:
    • 입력된 객체가 String의 인스턴스인지 확인합니다 (anObject instanceof String). 이 조건이 false일 경우, 비교 대상이 String이 아니므로 false를 반환합니다.
  3. 코드 유닛 체크 (Java 9 이상에서 COMPACT_STRINGS 옵션 관련):
    • Java 9부터 문자열을 보다 메모리 효율적으로 저장하기 위해 도입된 COMPACT_STRINGS 옵션에 따라 문자열 데이터가 Latin-1 또는 UTF-16 중 어느 하나의 형식으로 저장됩니다. 이 필드는 문자열의 인코딩 타입을 체크하여 불일치하는 경우 더 이상 비교를 진행하지 않고 false를 반환합니다.
  4. 내용 비교:
    • 실제 문자열 데이터 (value)를 비교합니다. 이 비교는 문자열의 길이와 각 문자의 값을 비교하여 전체 문자열이 동일한지 확인합니다. 여기서 StringLatin1.equals(value, aString.value)는 두 문자열 배열의 내용이 같은지 비교합니다.

 

Object에서의 equals() 예시 코드

package Example;

public class Hello {
    public static void main(String[] args) {
        Human h1 = new Human("hello",30);
        Human h2 = new Human("hello",30);

        System.out.println(h1==h2);
        System.out.println(h1.equals(h2));
    }
}

//출력 결과
false
false

 

h1 == h2 출력이 false

그렇다면 Object에서도 마찬가지로 h1과 h2는 ==으로 했을때 각각 다른 메모리 주소를 참조하게 됩니다. 따라서 h1 == h2는 두 참조가 같은 객체를 가리키는지 비교하기 때문에 false를 반환합니다.

 

h1.equals(h2) 출력이 false

false가 나온 이유는 Human 클래스에 equals() 메서드가 오버라이드되지 않았기 때문입니다. Java의 기본 Object 클래스의 equals() 메서드는 객체의 참조(identity)를 비교하며, Human 클래스에서 이 메서드를 오버라이드하여 객체의 속성 값(여기서는 이름과 나이)을 기반으로 동등성을 비교하지 않았기 때문에, 기본적으로 h1h2가 서로 다른 객체라고 판단합니다.

 

Object에서의 equals()

  public boolean equals(Object obj) {
        return (this == obj);
    }

 

 

그렇다면 Object에서 h1과 h2는 같은 객체이므로 equals()를 오버라이드 해보겠습니다.

@Override
    public boolean equals(Object o){
        // 주소값이 같은지 판단
        if(this == o){
            return true;
        }
        // 비교값의 타입이 자식인지 판단
        if(o instanceof Human){
            Human h = (Human) o;
            // 내부값이 같은지 판단
            if(h.age == age && h.name.equals(name)){
                return true;
            }
        }
        return false;
    }

이렇게 equals() 메서드를 오버라이드 한 후 비교를 진행하면 동등하다고 판단됩니다. (동등 : 두 값이 같다. 동일 : 주소값이 같다.)

public class Hello {
    public static void main(String[] args) {
        Human h1 = new Human("hello",30);
        Human h2 = new Human("hello",30);

        System.out.println(h1.equals(h2));
    }
}

//출력 결과
true

 

📌 hashCode()

hashCode란 객체를 식별하는 고유한 정수값입니다. hashCode()는 객체의 메모리 번지를 이용해서 hashcode를 만들어 리턴하기 때문에 객체 마다 고유의 다른 값을 가져야 합니다.

더보기

자바에서 "객체의 메모리 번지를 이용하여 hashCode를 만든다"는 표현은 자바의 Object 클래스의 기본 hashCode() 메서드의 구현에 대한 설명입니다. 이 구현은 실제로 각 객체의 메모리 주소를 기반으로 해시 코드를 생성할 수 있습니다. 그러나 정확히 말하자면, 자바는 메모리 주소를 직접적으로 다루지 않으며, 자바가 관리하는 내부 참조 값을 이용합니다.

 

hashCode() 예시 코드

public class Hello {
    public static void main(String[] args) {
        Human h1 = new Human("hello",30);
        Human h2 = new Human("hello",30);

        System.out.println(h1.hashCode());
        System.out.println(h2.hashCode());
    }
}

//출력 결과
1313922862
495053715

이렇듯 두 객체를 equals() 메서드를 재정의해서 동등하다고 판단한다면, 두 객체의 hashCode() 주소값은 반드시 같아야 합니다.(동일성)

package Example;

import java.util.Objects;

public class Human {
    String name;
    int age;

    public Human(String name, int age){
        this.name = name;
        this.age = age;
    }
    
    //equals() 재정의
    @Override
    public boolean equals(Object o){
        // 주소값이 같은지 판단
        if(this == o){
            return true;
        }
        // 비교값의 타입이 자식인지 판단
        if(o instanceof Human){
            Human h = (Human) o;
            // 내부값이 같은지 판단
            if(h.age == age && h.name.equals(name)){
                return true;
            }
        }
        return false;
    }

    //hashCode() 재정의
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

 

이렇게 간편하게 hashCode 메서드를 재정의하여 결과를 다시 출력해본다면

public class Hello {
    public static void main(String[] args) {
        Human h1 = new Human("hello",30);
        Human h2 = new Human("hello",30);

        System.out.println(h1.hashCode());
        System.out.println(h2.hashCode());
    }
}

//출력 결과
-1220934323
-1220934323

이렇게 동일한 hash값이 나오게 됩니다. 만약 민감한 경우에는 직접 hashCode를 재정의해주는게 좋은데 관련 정보는 Guide to hashCode() 를 참고하시면 됩니다.

 

📚총정리

  1. 두 객체가 동등하다고 판단해야 하는 경우 equals() 를 재정의 해야합니다.
  2. equals()를 재정의하면 두 객체를 동일하다고 판단해야 하기 때문에 hashCode()도 재정의를 해주어야 합니다.

 

❓무조건 hashCode()와 equals()를 같이 재정의해야 할까?

‘hash 값을 사용하는 Collection을 사용 하지 않는다면, equals와 hashCode를 같이 재정의하지 않아도 되는건가?‘라고 생각할 수 있습니다. 하지만 무조건 같이 재정의하라고 말할 자신은 없습니다.

 

하지만 요구사항은 항상 변합니다. hash 값을 사용하는 Collection을 사용할 리 없다는 자신의 판단은 틀렸을 가능성이 높습니다.


또, 협업 환경이라면 동료는 당연히 equals와 hashCode를 같이 재정의했을 거라고 생각하고 hash 값을 사용하는 Collection으로 수정할 가능성도 있습니다.

굳이 이런 위험한 코드를 안고 가지말고 equals와 hashCode는 항상 같이 재정의해주는 게 맞다고 생각합니다.

profile

응애개발자

@Eungae-D

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!