본문 바로가기
Java 웹 프로그래밍

equals()와 hashCode()

by irerin07 2020. 6. 16.
728x90

 

https://github.com/irerin07/interview_prep/blob/master/Java/Java_interview.md#equals-hashcode

equals, hashcode

  • Java.lang.object에는 굉장히 중요한 두 메소드가 정의되어 있다. public boolean equals(Object obj)와 public int hashCode().
  • equals()는 두 Object가 같은 내용인지 비교한다.
    class Money {
      int amount;
      String currencyCode;
    }
    

Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");
boolean balanced = income.equals(expenses);

```

  • 위의 코드는 true를 반환할 것 같지만 실제로는 false를 반환한다.

    • 그 이유는 실제로 구현되어 있는 equals의 코드를 보면 알 수 있다.
    • public boolean equals(Object obj) {
              return (this == obj);
      }
    • 단순히 ==를 사용해서 비교하고 있다.
    • == 연산자는 두개의 비교 대상들이 원시타입(primitive type)인 경우에는 값이 같은지 비교를 하지만 그 외 객체, reference type인 경우엔 주소가 같은지 비교한다.
    • 그러니 false가 나올수 밖에...
  • 그럼 어떤 방식으로 비교를 해야 할까?

    • equals()를 @Override하면 문제를 해결할 수 있다.
    • @Override
      public boolean equals(Object o) {
          if (o == this)
              return true;
          if (!(o instanceof Money))
              return false;
          Money other = (Money)o;
          boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
            || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
          return this.amount == other.amount && currencyCodeEquals;
      }
    • 위와 같이 equals () 메소드를 @Override하면 객체의 정체성만 고려하는것이 아니라 두 가지 관련 속성의 값도 고려하도록 할 수 있다.
  • equals() Contract

    • reflexive: 모든 객체 x에 대해 x.equals(x)는 true를 반환해야합니다.
    • symmetric: 두 개의 객체 x와 y에 대해 x.equals(y)가 true를 반환한다면 y.equals(x)역시 true를 반환해야합니다.
    • transitive: 여러 객체 x, y 및 z의 경우 x.equals(y)가 true를 반환하고 y.equals(z)가 true를 반환하면 x.equals(z)는 true를 반환해야합니다.
    • consistent: equals() 메소드 구현에 사용되는 오브젝트 특성이 수정되지 않는 한 x.equals(y)를 여러 번 호출하더라도 항상 동일한 결과가 리턴되어야합니다.
    • 객체 클래스 equals() 메소드 구현은 두 레퍼런스가 동일한 객체를 가리키는 경우에만 true를 반환합니다.
  • 상속으로 인한 symmetric 위반

    • class Voucher extends Money {
      
          private String store;
      
          @Override
          public boolean equals(Object o) {
              if (o == this)
                  return true;
              if (!(o instanceof Voucher))
                  return false;
              Voucher other = (Voucher)o;
              boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
                || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
              boolean storeEquals = (this.store == null && other.store == null)
                || (this.store != null && this.store.equals(other.store));
              return this.amount == other.amount && currencyCodeEquals && storeEquals;
          }
      
          // other methods
      }

      위의 코드는 Money를 상속받는 Voucher클래스이다.
      단순히 보기에는 아무 문제가 없어보인다. 실제로 Money와 Money를 비교하고, Voucher와 Voucher를 비교하면 아무 문제가 없다
      하지만 Money와 Voucher를 비교하면 어떨까?

      Money cash = new Money(42, "USD");
      WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon");
      
      voucher.equals(cash) => false // As expected.
      cash.equals(voucher) => true // That's wrong.

      보이는대로 symmetric(두 개의 객체 x와 y에 대해 x.equals(y)가 true를 반환한다면 y.equals(x)역시 true를 반환해야합니다.)을 위반하고 있다.

      그럼 어떻게 해야 좋을까? 이 문제를 고치는 방법으로 다음과 같이 코드를 작성할 수 있다.

    • class Voucher {
      
          private Money value;
          private String store;
      
          Voucher(int amount, String currencyCode, String store) {
              this.value = new Money(amount, currencyCode);
              this.store = store;
          }
      
          @Override
          public boolean equals(Object o) {
              if (o == this)
                  return true;
              if (!(o instanceof Voucher))
                  return false;
              Voucher other = (Voucher) o;
              boolean valueEquals = (this.value == null && other.value == null)
                || (this.value != null && this.value.equals(other.value));
              boolean storeEquals = (this.store == null && other.store == null)
                || (this.store != null && this.store.equals(other.store));
              return valueEquals && storeEquals;
          }
      
          // other methods
      }

      위의 코드는 Money 클래스를 상속받는 대신 Moeny 클래스의 속성을 사용해서 Voucher클래스를 만든것이다.
      이제는 symmetric을 위배하지 않고 잘 작동할것이다.

  • hashCode()

    • 두 Object가 같은 객체인지 비교한다.

    • 해싱 알고리즘을 사용하여 정수값을 반환한다.

    • equals() 메서드를 통해 같은 객체임이 확인 된 두 객체는 반드시 똑같은 hash code를 반환해야 한다.

    • hashCode() contract

      • 자바 어플리케이션이 동작할때, 동일한 객체가 호출 될 때마다 hashCode()는 해당 코드에 어떤 변경이 있지 않은 이상 항상 동일한 값을 일관되게 반환해야합니다. 이 값은 다른 자바 어플리케이션에서 실행한 값과 반드시 동일할 필요는 없습니다.
      • euqals()메소드를 통해 두 객체가 같은 객체임이 확인된 경우 hashCode()역시 동일한 값을 생성해야 합니다.
      • euquals()를 통해 다른 객체임이 확인되었지만 hashCode()에서 반드시 서로 다른 정수값을 반환할 필요는 없습니다. 하지만 서로 다른 객체에 대해 고유한 정수값을 생성하는 것이 해시테이블의 성능향상에 도움이 됩니다.
    • 단순하고 실용적이라 할 수 없는 hashCode() 구현체

    • public class User {
      
          private long id;
          private String name;
          private String email;
      
          // standard getters/setters/constructors
      
          @Override
          public int hashCode() {
              return 1;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null) return false;
              if (this.getClass() != o.getClass()) return false;
              User user = (User) o;
              return id == user.id 
                && (name.equals(user.name) 
                && email.equals(user.email));
          }
      
          // getters and setters here
      }
    • 위의 코드는 모든 객체를 같은 bucket에 저장하기 때문에 hash Table의 활용도를 0에 가깝게 낮춰버린다.

    • 다음은 intellij에서 제공하는 hashCode()이다

    • @Override
      public int hashCode() {
          int result = (int) (id ^ (id >>> 32));
          result = 31 * result + name.hashCode();
          result = 31 * result + email.hashCode();
          return result;
      }
    • 더 자세한 내용은 이펙티브 자바에서 확인할 수 있다.

    • https://es.slideshare.net/MukkamalaKamal/joshua-bloch-effect-java-chapter-3

  • 해시 충돌

    • 아무리 효율적인 해싱 알고리즘을 사용한다 하더라도 동일하지 않은 두 객체에 대해 같은 해시코드를 가질수도 있다.
      • 그렇기에 두 객체는 해시 테이블 키가 다르더라도 동일한 해시 코드로 인해 같은 bucket을 가리킬것이다.
    • 두 개 이상의 객체가 동일한 버킷을 가리키는 경우 linked list에 저장이 된다.
      • 이런 경우 해시 테이블은 linked list의 배열이 되며 동일한 해시를 가진 각 객체는 배열의 버킷 인덱스번째(array[bucket_index]) linked list에 추가된다.
    • 즉, hashCode()구현은 굉장히 신중하고 효율적으로 이루어져야 한다.
    • Java 8은 HashMap 구현에 흥미로운 개선 방법을 내놓았는데, 버킷 크기가 특정 임계 값을 초과하면 연결된 목록이 트리 맵으로 바뀐다. 이를 통해 비관적 O(n) 대신 O(logn)의 시간복잡도(검색)를 달성 할 수 있습니다.
728x90

'Java 웹 프로그래밍' 카테고리의 다른 글

[Querydsl] Querydsl 정리글  (0) 2020.07.20
깃 현재 브랜치를 보여주는 명령어  (0) 2020.06.24
오늘의 뻘짓  (0) 2020.06.02
JWT (JSON Web Token) - 3  (0) 2020.04.28
JWT (JSON Web Token) - 2  (0) 2020.04.28