ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Equals와 HashCode
    JAVA 2023. 11. 27. 16:43

    Equals Method

    두 참조 변수 값이 같은지 다른지 동등 여부를 비교할 때 사용한다.

    • String 타입의 변수를 비교할 경우
    String s1 = "Hello";
    String s2 = "Hello";
    
    System.out.println(s1 == s2); // 주소 비교 false
    System.out.println(s1.equals(s2)); // 값 비교 true
    
    • 객체를 비교할 경우 ( ==이나 equals()나 똑같다 )
    class Person {
        String name;
    
        public Person(String name) {
            this.name = name;
        }
    
    public class Example {
        public static void main(String[] args) {
            Person person1 = new Person("홍길동");
            Person person2 = new Person("홍길동");
    
            System.out.println(person1 == person2); // == 은 객체타입인경우 주소값을 비교한다. 서로다른 객체는 다른 주소를 가지고 있기 때문에 false가 출력됨
    
            System.out.println(person1.equals(person2)) // equals또한 객체타입인경우 주소값을 비교하기 때문에 false가 출력된다.
        }
    }
    

    equals Overriding

    • person1 인스턴스와 person2 인스턴스는 각기 다른 객체를 초기화하여 Heap영역에 따로 저장해두고 있으니 두 객체를 비교하면 주소가 일치하지 않아 false
    • 따라서 만일 객체 자료형을 비교를 할 때, 주소 값이 아닌 객체의 필드값을 기준으로 동등 비교 기준을 변경하고 싶다면, equals 메서드를 오버라이딩해서 재정의를 해준다.

    Object 메서드

    메소드 설명

    protected Object clone() 해당 객체의 복제본을 생성하여 반환함.
    boolean equals(Object obj) 해당 객체와 전달받은 객체가 같은지 여부를 반환함.
    protected void finalize() 해당 객체를 더는 아무도 참조하지 않아 가비지 컬렉터가 객체의 리소스를 정리하기 위해 호출함.
    Class<T> getClass() 해당 객체의 클래스 타입을 반환함.
    int hashCode() 해당 객체의 해시 코드값을 반환함.
    void notify() 해당 객체의 대기(wait)하고 있는 하나의 스레드를 다시 실행할 때 호출함.
    void notifyAll() 해당 객체의 대기(wait)하고 있는 모든 스레드를 다시 실행할 때 호출함.
    String toString() 해당 객체의 정보를 문자열로 반환함.
    void wait() 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.
    void wait(long timeout) 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지날 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.
    void wait(long timeout, int nanos) 해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지나거나 다른 스레드가 현재 스레드를 인터럽트(interrupt) 할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.

    String Class의 equals Overriding

    • 직접 문자열 값을 문자 배열로 만들어 각 배열 요소 문자값을 하나 하나 비교함으로써 동등 유무를 걸러냄

    hashCode 메서드

    • 객체의 주소 값을 이용해서 hashing 기법을 통해 해시 코드를 만든 후 반환한다.
    • 따라서 다른 두 객체는 같은 해시 코드를 가질 수 없게 된다. 그래서 객체의 지문이라고도 한다.
    • 해시코드는 주소값은 아니고, 주소값으로 만든 고유한 숫자값이라고 하는게 옳다.
    class Person {
        String name;
    
        public Person(String name) {
            this.name = name;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Person p1 = new Person("홍길동");
            Person p2 = new Person("홍길동");
    
            // 객체 인스턴스마다 각기 다른 주해시코드(주소))를 가지고 있다.
            System.out.println(p1.hashCode()); // 622488023
            System.out.println(p2.hashCode()); // 1933863327
        }
    }
    

    hashCode 오버라이딩

    • equals와 hashCode 메서드는 같이 오버라이딩 해줘야한다.
      • eqauls()의 결과가 true인 두 객체의 해시코드는 반드시 같아야한다는 자바의 규칙이 있다.
      • 두 메서드를 재정의 하지 않을시, hash 값을 사용하는 Collection Framework을 사용할 때 문제가 된다.

    equals만 재정의할 경우

    class Person {
        public String name;
    
        public Person(String name) {
            this.name = name;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Person p = (Person) o;
            return Objects.equals(name, p.name);
        }
    }
    
    public class ClassTest {
        public static void main(String[] args) throws Exception {
            Person p1 = new Person("홍길동");
            Person p2 = new Person("홍길동");
    
            // 두 객체의 해시 코드
            System.out.println(p1.hashCode()); // 460141958
            System.out.println(p2.hashCode()); // 1163157884
    
            // 해시코드가 달라도, equals를 재정의 했기 때문에 동등함
            System.out.println(p1.equals(p2)); // true
    
            // 리스트를 생성하고 두 객체 데이터를 추가한다.
            List<Person> people = new ArrayList<>();
            people.add(p1);
            people.add(p2);
            
            // 그리고 리스트의 길이를 출력한다.
            System.out.println(people.size()); // 2
        }
    }
    
    public static void main(String[] args) {
        Set<Car> cars = new HashSet<>();
        cars.add(new Car("foo"));
        cars.add(new Car("foo"));
    
    		// size 2가 된다.
    		// p1, p2는 논리적으로 같다고 정의하였지만 해시코드가 다르기 때문에 중복된 데이터가
    		// 컬렉션에 추가된다.
        System.out.println(cars.size());
    }
    

    hashCode와 equals 동작 순서

    • 가장 먼저 데이터가 추가된다면, 그 데이터의 hashCode()의 리턴 값을 컬렉션에 가지고 있는 지 비교한다.
    • 만일 해시코드가 같다면 그제서야 다음으로 equals() 메서드의 리턴 값을 비교하게 되고, true이면 논리적으로 같은 객체라고 판단한다.

    equals와 hashCode 동시 재정의

    • Objects.hash() 메서드
      • 매개변수로 주어진 값들을 이용해서 고유한 해시 코드를 생성한다.
      • 즉 동일한 값을 가지고 필드로 해시코드를 생성하게 되면, 동일한 해시코드를 가질 수 있게 되어, 이 해시코드 값을 기준으로 재정의한 equals() 가 동등 비교에 이용한다고 보면 된다.

    객체의 hashCode는 고유하지 않다.

    • 자바에서는 포인터를 철저히 숨겼기 때문에 직접적인 객체의 주소 원래값을 얻을수는 없다. 그래서 자바에서는 참조 변수의 주소값을 표현하기 위해 해시코드를 이용한다.
    • 보통 해싱 알고리즘 상 서로 다른 두 주소값을 가지고 있는 객체는 결코 같은 해시코드를 가질 수 없다. 하지만 예외가 있다, hashCode() 메서드가 int형 정수를 반환한다는 점이다.
    • 만일 자신의 컴퓨터가 32bit 사양이라면 이는 문제가 되지 않지만 64비트 컴퓨터에서 돌아가는 JVM은 기본적으로 64bit 주소체계를 기본으로 하는 데, 만일 64bit의 주소값을 hashCode를 이용해 반환하면 메서드의 타입에 따라 32bit로 강제 캐스팅(long → int)이 되기 때문에 값이 겹칠수도 있다는 케이스가 존재한다. 이를 Hash Collisions 해시충돌이라 한다.
    • 반환 타입을 long으로 업그레이드를 하지 않은 이유는 호화성을 위해 나둔것이다.

    객체의 hashCode 중복 확인해보기

    import java.util.HashMap;
    import java.util.Map;
    
    public class Test1 {
        public static void main(String[] args) {
            Map<Integer, Object> objectMap = new HashMap<>(); // {해시코드 : 객체} 형태의 Map 자료형
            int j = 1;
    
            // int형 범위(21억번) 돌림
            for (long i = Integer.MIN_VALUE; i < Integer.MAX_VALUE + 1L; i++) {
                Object obj = new Object(); // 객체 생성
                int hashCode = obj.hashCode(); // 해시코드 얻기
    
                // Map에 해당 해시 코드가 존재하는지 확인 (만일 있다면 출력)
                Object obj2 = objectMap.get(hashCode);
                if (obj2 != null) {
                    System.out.println((i - Integer.MIN_VALUE + 1L) + "번째 루프");
                    System.out.println("새롭게 만든 객체 해시코드 : " + obj.hashCode());
                    System.out.println("기존에 있는 객체 해시코드 : " + obj2.hashCode());
                    System.out.println("해시코드가 같아도 obj2 == obj 는 : " + (obj2.equals(obj))); // 해시코드가 같아도 객체의 주솟값은 원래 다르니 false 출력
                    System.out.println("");
                    
                    if (j >= 4) return; // 4번만 출력
                    j++;
                } else {
                    objectMap.put(hashCode, obj); // Map에 해시코드와 객체 등록
                }
            }
        }
    }
    

    문제가 있는가?

    • 문제 없다 왜냐면 보통 객체를 비교할 때는 hashCode → equals 같이 정의하게 되면 해시코드가 같으면 equals 메서드를 통해 두 객체의 진짜 주소를 직접적으로 비교하도록 구성 되어 있다.

    참고 문헌

    https://inpa.tistory.com/entry/JAVA-☕-equals-hashCode-메서드-개념-활용-파헤치기

    https://inpa.tistory.com/entry/JAVA-☕-객체의-hashCode는-고유하지-않다-❌

Designed by Tistory.