ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • equals / hashCode
    ☕️ java 2022. 12. 7. 18:06

    equals ❓ hashCode ❓


    equalshashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다.

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

     

    equals()

    Object의 equals()

    public class Object {
        @IntrinsicCandidate
        public Object() {}
    		
        ... 
    
        public boolean equals(Object obj) {
            return (this == obj);
        }
        ...
    }
    
    • 기본적으로 equals() 메소드는 2개의 객체가 동일한지 검사하기 위해 사용한다.
    • 2개의 객체가 참조하는 것이 동일한지를 확인 - 동일성(Equality)을 비교하는 것

    즉, 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.

    프로그래밍을 하다보면 동일한 객체가 메모리 상에 여러개 띄워져있는 경우가 있다.

    • 해당 객체는 서로 다른 메모리에 띄워져있으므로 동일한(Equality)객체가 아니다.
    • 프로그래밍 상으로는 같은 값을 가지므로 같은 객체로 인식되어야 한다 - 동등성(Identity)

    따라서, 우리는 값으로 객체를 비교하도록 equals 메소드를 오버라이딩해준다.

    String s1 = new String("Test");
    String s2 = new String("Test");
    
    System.out.println(s1 == s2);			// false
    System.out.println(s1.equals(s2));		// true;
    
    // equals, overridden in String Class 
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        
        return false;
    }
    
    • 위 코드에서 s1.equals(s2) 의 결과는 true 이다.
    • 이유 : String 클래스에서 equals() 메소드를 오버라이드해 객체가 같은 값을 갖는지 동등성(Identity)을 비교하도록 처리했기 때문이다.

     

    equals 메서드 단계별 구현 방법

    1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인
    2. instanceof 연산자로 입력이 올바른 타입인지 확인
    3. 입력을 올바른 타입으로 형변환
    4. 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사

     

    hashCode()

    public class Object {
        @IntrinsicCandidate
        public Object() {}
        ...
    
        @IntrinsicCandidate
        public native int hashCode();
        ...
    }

    hashCode() 메소드는 실행 중(Runtime) 객체의 유일한 integer 값을 반환한다.

    Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환한다.

    • 항상 그런 것은 아니다.

     

    hashCode는 HashTable 과 같은 자료구조를 사용할 때 데이터가 저장되는 위치를 결정하기 위해 사용된다.

     

    equals 와 hashCode의 관계

    동일한 객체는 동일한 메모리 주소를 갖는다는 것을 의미한다.

    따라서, 동일한 객체는 동일한 해시코드를 가져야 한다.

    equals() 메소드를 오버라이드 한다면, hashCode() 메소드도 함께 오버라이드 되어야 한다.

     

    equals 와 hashCode의 관계

    • 프로그램을 실행하는 동안 equals() 에 사용된 정보가 수정되지 않았다면, hashCode는 항상 동일한 정수값을 반환해야 한다.
      • Java 프로그램을 실행할 때 마다 달라지는 것은 상관 ❌
    • 두 객체가 equals() 에 의해 동일하다 → 두 객체의 hashCode() 값도 일치
    • 두 객체가 equals() 에 의해 동일하지 않다 → 두 객체의 hashCode() 값은 일치하지 않아도 된다.

     

    📌 equals, hashCode 의 override


    equals() 를 오버라이드 해야 하는 이유

    public class TestObject {
        private Integer id;
    }
    

     

    public class EqualsTest {
        public static void main(String[] args) {
            TestObject t1 = new TestObject();
            TestObject t2 = new TestObject();
     
            t1.setId(100);
            t2.setId(100);
     
            System.out.println(t1.equals(t2));  //false
        }
    }
    • 같은 id 값을 가지는 객체를 서로 다른 과정에 의해 생성했을 경우
    • 2개의 객체는 같은 id를 갖기 때문에 equals 연산시 true를 반환해야 한다.
      • 하지만 false 를 반환한다.

     

    → 이런 문제를 해결하기 위해서 객체에 대한 equals() 메소드를 오버라이드 해야 한다.

    public boolean equals(Object o) {
        if(o == null) {
            return false;
        }
        if (o == this) {
            return true;
        }
        if (getClass() != o.getClass()) {
            return false;
        }
         
        TestObject t = (TestObject) o;
        return (this.getId() == t.getId());
        
    }
    

    equals 에 의한 문제는 해결된 것 처럼 보이지만,

    ⚠️ 해당 객체를 HashSet과 같은 자료구조에 저장하려고 하면 또 다른 문제가 발생한다.

     

    hashCode의 오버라이드 필요성

    HashTable 이나 HashSet, HashMap 같은 자료구조는 자료를 저장하기 위한 위치를 선택하기 위해 hashCode를 이용한다.

    public class EqualsTest {
        public static void main(String[] args) {
            TestObject t1 = new TestObject();
            TestObject t2 = new TestObject();
     
            t1.setId(100);
            t2.setId(100);
     
            //Prints 'true' now!
            System.out.println(t1.equals(t2));
     
            Set<TestObject> objects = new HashSet<TestObject>();
            objects.add(t1);
            objects.add(t2);
             
            System.out.println(objects);  //Prints two objects
        }
    }
    

    Object 클래스의 hashCode() 메소드는 해당 메모리 주소값을 반환한다.

    • 따라서, 위 코드의 t1, t2는 다른 해시값을 반환한다
    • ⇒ HashSet에는 2개의 객체가 서로 다른 위치에 저장되게 된다.

    이러한 문제를 해결하기 위해 hashCode 메소드도 객체 클래스에 오버라이드하여 수정해주어야 한다.

    @Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        result = PRIME * result + getId();
        return result;
    }

    ⇒ hashSet 에도 1개의 데이터만 저장되게 된다.

     

    외부 라이브러리를 통한 override

    Apache Commons 의 라이브러리를 사용하면 HashCodeBuilderEqualsBuilder를 사용할 수 있다.

    • 더욱 간편하게 메소드들을 오버라이드할 수 있다.
    import org.apache.commons.lang3.builder.EqualsBuilder;
    import org.apache.commons.lang3.builder.HashCodeBuilder;
    
    public class TestObject {
        private Integer id;
        private String firstname;
        private String lastName;
        private String department;
        
        ...
     
        @Override
        public int hashCode() {
            final int PRIME = 31;
            return new HashCodeBuilder(getId()%2==0?getId()+1:getId(), PRIME).toHashCode();
        }
     
        @Override
        public boolean equals(Object o) {
        if (o == null)
           return false;
            
        if (o == this)
           return true;
            
        if (o.getClass() != getClass())
           return false;
         
        TestObject t = (TestObject) o;
         
        return new EqualsBuilder().
                  append(getId(), t.getId()).
                  isEquals();
        }
    }

    equals(), hashCode() 오버라이드 가이드라인

    • hashCode와 equals를 생성하기 위해서는 같은 속성(attribute)를 이용하라
      • ex) id
    • equals()는 일관성되어야 한다.
    • a.equals(b) == true 라면 a.hashCode() == b.hashCode() 역시 true 여야 한다.
    • 두 메소드는 함께 항상 오버라이드 되어야 한다.

     

    ORM 에서의 hashCode, equals

    ORM를 사용하고 있다면, hashCode와 equals를 오버라이드 메소드 내부에서 Getter를 사용하자.

    • ORM에 의해 필드가 lazy loaded 되어 getter를 부르기 전에 사용할 수 없을 수도 있기 때문
    // 클래스의 정보가 lazy loaded 되면 id 에 0이 할당
    t1.id == t2.id //( 0 == 0 ) 으로 처리가 될 수 있다.
    
    // 이 것을 수정
    t1.getId() == t2.getId() 
    // ORM에 의해 id에 값이 할당된 후 getId()가 호출가능하므로 오작동을 멈출 수 있다.
    

     

    참고자료

    https://mangkyu.tistory.com/101

    https://velog.io/@lovi0714/Equals와-Hashcode를-override-해야하는-이유

    '☕️ java' 카테고리의 다른 글

    Reflection API ?  (0) 2023.02.09
    커스텀 validation 만들기  (0) 2023.01.28
    CheckedException 과 UncheckedException  (0) 2022.10.10
    가비지 컬렉션(GC) ❓  (0) 2022.10.10
    Java version 별 특징  (0) 2022.10.09

    댓글

Designed by Tistory.