[Java] equals, hashcode 에 대해서 알아보자

곰민·2022년 11월 26일
2
post-thumbnail
post-custom-banner

equals() 와 hashcode() 없이는 상당히 많은 if 문을 객체의 모든 field를 비교하기 위해서 사용해야 한다.
Java에서 객체 비교를 효율적이고 쉽게 해주는 equals와 hashcode()에 대해서 공부해 보자.


equals()와 hashcode()에 대해서


🚀 Overrding equals() and hashcode() in Java


equals()와 hashcode()는 모든 Java 객체의 상위 클래스인 Object Class에 정의되어 있다.
모든 java class는 equals()와 hashcode()를 포함하고 있지만 제대로 작동하기 위해서는 Overriding 해야 한다.
Overriding 이 equals() 및 hashcode()에서 어떻게 작동하는지 확인해 보자.

public boolean equals(Object obj) {
        return (this == obj);
}
  • Object class에 equals() method는 최근 인스턴스가 이전에 전달받은 Object와 같은지 아닌지를 체크한다.
@HotSpotIntrinsicCandidate
public native int hashCode();
  • hashcode() 매서드가 Overriding 되지 않았다면 Object 클래스의 default method가 호출된다.
    • default method는 C와 같은 다른 언어로 실행되고 object의 메모리 주소와 관련된 일부 코드를 반환한다는 것을 의미하는 native method입니다.
    • object의 유일한 integer 값을 반환해 준다.

🤔 equals(), hashcode() 둘 다 재정의 되지 않았다면


위 메서드가 대신 호출됨.
이 경우에는 equals() 와 hashcode()의 진짜 목적인 두 개 이상의 객체가 동일한 값을 갖는지 확인하는 것을 수행하지 않습니다.

  • equals()를 재정의 안할 시.
      return (this == obj);
    • 위의 object 클래스의 메서드를 사용하게 되며 단순하게 == 비교를 하게 된다.
  • hashcode() 재정의를 안할 시.
    • hashcode()를 재정의 하지 않는다면.
      new 연산자를 통해 새로운 객체를 만들 때마다 늘 다른 hashcode를 리턴하여 HashMap의 key로 인스턴스를 사용할 시 각각 다 다르다고 나온다.
  • equals()를 재정의하고 hashcode()를 재정의 안할시.
    • equals()에서 동치성을 보장한 객체들이 new 연산자를 통해서 다르다고 나온다.

equals()를 재정의 하는 경우 hashcode()는 항상 재정의 하는것이 좋다.


Java equals() method를 Override 할 때


Java에서 objects를 비교하기 위해서 equals() method를 사용할 때
equals()는 object의 속성값을 비교한다.

  • 예시
public class EqualsAndHashCodeExample {

    public static void main(String... equalsExplanation) {
        System.out.println(new Simpson("Homer", 35, 120)
                 .equals(new Simpson("Homer",35,120)));
        
        System.out.println(new Simpson("Bart", 10, 120)
                 .equals(new Simpson("El Barto", 10, 45)));
        
        System.out.println(new Simpson("Lisa", 54, 60)
                 .equals(new Object()));
    }
	
    static class Simpson {

        private String name;
        private int age;
        private int weight;

        public Simpson(String name, int age, int weight) {
            this.name = name;
            this.age = age;
            this.weight = weight;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            Simpson simpson = (Simpson) o;
            return age == simpson.age &&
                    weight == simpson.weight &&
                    name.equals(simpson.name);
        }
    }

}
  • override된 equals에서
    1. 첫 번째 비교 equals()에서 현재 object 인스턴스를 전달된 object 와 비교하며 두 객체가 같다면 true를 리턴한다.
    2. 두 번째 비교에서 equals()는 전달된 object가 null 인지, 또는 다른 클래스로 입력되었는지 확인한다.
    클래스가 다른 경우 객체는 동일하지 않다.
    3. 마지막으로 equals()는 object의 필드를 비교한다.
    두 object의 필드 값이 같으면 object는 동일하다.

  • main 문에서의 object 비교에 대해서
    • 1번째 비교

      System.out.println(new Simpson("Homer", 35, 120).equals(new Simpson("Homer", 35, 120)));
      • object는 동일하므로 true를 리턴한다.
    • 2번째 비교

      System.out.println(new Simpson("Bart", 10, 45).equals(new Simpson("El Barto", 10, 45)));
      • 거의 동일하지만 이름이 “Bart”, “El barto”로 다르므로 false 리턴한다.
    • 마지막으로 Simpson 과 object 클래스 비교

      System.out.println(new Simpson("Lisa", 54, 60).equals(new Object()));
      • 클래스 타입이 다르기 때문에 false를 리턴한다.

equals() vs ==


== 연산자와 equals() method가 겉으로 보이기에 좀 비슷해 보일 수 있을지 모르나.
사실은 다르게 작동한다.

== 연산자는 두 개의 object 가 같은 object를 참조하는지 (references) 를 비교한다.

// 1번비교에서 생성한 simpson으로 homer 인스턴스 2개를 만들었다고 가정
System.out.println(homer == homer2);

1번째 비교에서 new 연산자를 사용하여 두 개의 서로 다른 인스턴스를 만들었고
new 연산자 때문에 homer와 homer2는 서로 다른 memory heap 영역을 참조하고 있기 때문에 결과 값이 false입니다.

equals() 메서드를 재정의한 경우

System.out.println(homer.equals(homer2));
  • 이경우 두 객체의 필드값이 같기 때문에 결과는 true입니다.
    • name : Homer , age : 35 , weight : 120

hashcode() Override로 객체의 고유함을 식별


object를 비교할 때 performance, 성능 최적화를 하기 위해 hashcode()를 사용한다.
hashcode()는 object의 유일한 id 값을 리턴해주고 object의 전체 상태를 더욱 쉽게 비교할 수 있게 만들어준다.

object의 hashcode가 또 다른 object의 hashcode와 같지 않다면 equals() 매서드를 실행시킬 이유가 없다.
하지만 만약에 두 object가 같지 않은데 hashcode가 만약에 같다면
values값, 필드 값들이 같은지 아닌지를 확인하기 위해 반드시 equals() 매서드를 재정의 해서 실행시켜야 한다.

  • hashcode() 예시
public class HashcodeConcept {

    public static void main(String... hashcodeExample) {
        Simpson homer = new Simpson(1, "Homer");
        Simpson bart = new Simpson(1, "Homer");

        boolean isHashcodeEquals = homer.hashCode() == bart.hashCode();

        if (isHashcodeEquals) {
            System.out.println("Should compare with equals method too.");
        } else {
            System.out.println("Should not compare with equals method because " +
                    "the id is different, that means the objects are not equals for sure.");
        }
    }

     static class Simpson {
        int id;
        String name;

        public Simpson(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Simpson simpson = (Simpson) o;
            return id == simpson.id &&
                    name.equals(simpson.name);
        }

        @Override
        public int hashCode() {
            return id;
        }
    }
}
  • 항상 같은값을 반환하는 hashcode()valid 할 수는 있어도 효과적이지는 않다.
  • 위의 예시에서는 항상 true를 리턴하기 하기 때문에, equals() 메서드는 항상 실행되고 성능 향상은 없다.

equals() 와 hashcode()를 collections과 사용하는 경우


중복허용을 하지 않는 Set의 경우

  • HashSet
  • TreeSet
  • LinkedHashSet
  • CopyOnWriteArraySet
  • Set은 오직 Unique한 요소만 insert 할 수 있기 때문에 예를 들어 HashSet에 element를 넣고 싶다면
    equals()와 hashcode()를 반드시 가장 먼저 사용해서 element가 unique 한지 판단하게 된다.
  • Hashset에 새 요소가 추가되기 전에 HashsSet에 지정된 collection에 이미 존재하는지 체크하는 로직입니다.
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
       break;
       p = e;
  • equals()와 hashcode()가 재정의 되어있지 않다면 중복되는 요소를 insert할 risk가 해당 코드에 생기게 된다.

🔥 접두사가 “Hash” 인 coolection 을 사용하는 경우
예를 들어 HashSet, HashMap, Hashtable, LinkedHashMap인 경우
기능이 정상적으로 작동하게 하려면
hashcode()와 equals() 매서드를 재정의 해줘야 합니다.


🚀 equals(), hashcode() 사용에 대한 가이드라인


같은 unique한 hashcode ID를 갖고 있는 경우 equals() 메서드를 사용하고
hashcode ID가 다른 경우 equals() 메서드를 사용하지 않는 것이 좋다.

만약 hashcode() 결과 값이equals() 사용 여부
return trueequals() 사용 한다
return falseequals() 사용하지 않는 것이 좋음
  • Set이나 Hash Collection에서의 성능적 이점을 가질 수 있다.

객체 비교 시
hashcode() 비교를 할 때 false를 리턴한다면 equals() 메서드는 반드시 false를 리턴해야 한다.
왜냐면 hashcode가 다르다면 확실히 objects는 같지 않기 때문이다.

hashcode() return “true”equals() method should return
truetrue or false
falsefalse

equals() 메서드가 true로 반환될 시 모든 값과 속성에서 동일함을 의미하고
이 경우 hashcode()도 return true 여야 한다.

equals() 매서드 반환값hashcode() method should return
truetrue
falsetrue or false

🚀 예제를 통해 활용해보자


  • 예제를 통해 equals() 메서드의 비교 결과를 추측하고, Set Collection 의 size를 추측해 봅시다.
public class EqualsHashCodeChallenge {

    public static void main(String... doYourBest) {
        System.out.println(new Simpson("Bart").equals(new Simpson("Bart")));
        Simpson overriddenHomer = new Simpson("Homer") {
            public int hashCode() {
                return (43 + 777) + 1;
            }
        };

        System.out.println(new Simpson("Homer").equals(overriddenHomer));

        Set set = new HashSet(Set.of(new Simpson("Homer"), new Simpson("Marge")));
        set.add(new Simpson("Homer"));
        set.add(overriddenHomer);

        System.out.println(set.size());
    }

    static class Simpson {
        String name;

        Simpson(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object obj) {
            Simpson otherSimpson = (Simpson) obj;
            return this.name.equals(otherSimpson.name) &&
                    this.hashCode() == otherSimpson.hashCode();
        }

        @Override
        public int hashCode() {
            return (43 + 777);
        }
    }

}
  • 정답은?
A) True, True, 4

B) True, False, 3

C) True, False, 2

D) False, True, 3

풀이


System.out.println(new Simpson("Bart").equals(new Simpson("Bart")));
  • 첫 번째 비교의 경우 object의 상태 값이 완전히 일치하고, hashcode() 또한 같은 값을 return 하기 때문에 true
System.out.println(new Simpson("Homer").equals(overriddenHomer));
  • 두 번째 비교의 경우 equals() 메서드 비교에서 hashcode()는 overriddenHomer 참조 변수에서 overrding이 된다.
    두 개의 simpson 객체에서 Homer라는 같은 이름을 갖고는 있지만 hashcode() 매서드가 다른 값을 overriddenHomer에서 반환하므로
    equals() 메서드는 hashcode에 대한 비교를 포함하기 때문에 false를 반환한다.
Set set = new HashSet(Set.of(new Simpson("Homer"), new Simpson("Marge")));
set.add(new Simpson("Homer"));
set.add(overriddenHomer);
  • Set Collection에는 Homer, Marge인 Simpson Object가 들어가 있고
    Set이기 때문에 이미 들어가 있는 Homer는 추가적으로 들어가지 않고
    hashcode()값이 다른 overriddenHomer가 들어가게 됨.
    그래서 Set의 size는 3이 된다.

답 : B

🚀 일반적으로 하는 실수들


  • equals() 메서드와 hashcode() 메서드와 같이 재정의 하지 않는 경우
  • HashSet 과 같은 Hash Collections을 사용할 때 equals()와 hashcode()를 재정의 하지 않는 경우
  • hashcode() 메서드에서 object 개별 unique한 코드 값을 반환하지 않고
    constant value 즉 상수값을 반환하는 경우
  • equals와 ==연산자를 interchangeably 즉 동일하게 유사하게 사용하는 경우
    == 연산자는 object의 참조 값에 대한 비교, equals() 비교는 object의 value를 비교합니다.

🚀 equals()와 hashcode()에서 기억해야 할 것들


  • hash collection 에서 equals()와 hashcode()가 overriding 되지 않는다면
    collection 객체는 중복된 요소를 갖게 될 수 있다.
  • object 비교에서 hashcode() 반환 값이 false인 경우 equals()도 false이다.
  • equals() 메서드는 object의 전체 상태, 필드값을 비교해야 한다.
  • equals() 매서드를 재정의 할 때 hashcode() 메서드를 항상 재정의 하는 것이 좋다.

참조


https://www.infoworld.com/article/3305792/comparing-java-objects-with-equals-and-hashcode.html
https://mangkyu.tistory.com/101
이펙티브자바

profile
더 나은 개발자로 성장하기 위해 퀘스트 🧙‍♂🧙‍♂ 깨는 중입니다. 관심사는 back-end와 클라우드입니다.
post-custom-banner

0개의 댓글