개발 일지/Java

[Java] equals 와 hashCode

배발자 2022. 12. 18.
반응형

 

 

equals와 hashCode 는 최상위 클래스의 Object에서 정의되어있다. 

그렇기 때문에 모든 객체는 Object 클래스에서 정의된 equals와 hashCode 함수를 상속받고 있다.

 

equals 

 

정확히 equals를 직역해보면 동일한가라는 뜻이다.  

한 번 Obejct 클래스에 정의된 equals 메소드를 살펴보자. 

 

//equals 메소드
public boolean equals(Object obj) {
    return (this == obj);
}

 

equals의 구현부를 보면 "this==obj" 로 정의되어있다. 

이 말의 즉슨, 2개의 객체가 동일한지 검사한다는 것이고 2개의 객체가 참조하는 것이 동일하냐? 그것을 묻는 것이다. 

쉽게 말해 두 개의 객체가 같은 메모리 주소가 맞냐? 그걸 묻는 것이다. 

 

그런데 궁금한 것이 생겼다. 

코딩 테스트를 할 때 보면 String 타입의 변수에 문자열을 담아서 equals 메소드를 많이 사용하곤 한다. 

뭐.. String s1 = "321" , String s2 = "321" 이런식으로 담았다면 힙 메모리의 상수풀에 "321" 만 저장되기 때문에 같은 메모리 주소를 가리키고 있긴하다. 

 

하지만, 만약 String s1 = new String ("321"), String s2 = new String ("321") 로 힙 메모리에 할당 시켜놨을 때는 equals 메소드를 사용하면 과연 동일한 객체일까? 서로 다른 힙 메모리 주소에 할당하는데 메모리 주소가 같을리가 있나?? 

 

일단 코드를 돌려보자.

 

String s1  =new String("321"); 
String s2 = new String("321"); 
System.out.println(s1.equals(s2));

// 출력해보면 true 가 나온다.

 

정답은 같다. 어어? 이상하다. 왜 같지?

갑자기 머리가 복잡하다. 이유를 한 번 살펴보도록 하자. 

 

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;
}

 

위의 코드 세상은 어디냐면 String 클래스이다. 

분명 Object 클래스에서 equals 메소드는 한 줄로 정의되어 있었는데 String 클래스에서 equals는 상당히 길다. 

 

뭔가 감이 오는가? 

그렇다. 최상위 클래스 Object 의 equals 메소드를 오버라이딩해서 재정의를 해줬다는 뜻이다. 

 

즉, String 타입을 사용해 생성한 변수들끼리 비교를 할 때 오버라이딩 한 저 equals 메소드를 통해 동등성을 비교하는 것이다. 다시 말해서 주소가 같은지를 따지는 것이 아니라 "같은 값" 을 가지는지 확인을 한다는 것이다. 

 

public class Car {

	String name; 
	String number;
	
	public Car(String name, String number) {
		super();
		this.name = name;
		this.number = number;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Car other = (Car) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (number == null) {
			if (other.number != null)
				return false;
		} else if (!number.equals(other.number))
			return false;
		return true;
	} 
}
public static void main(String[] args) {

    Car car1 = new Car("Porsche" , "1234"); 
    Car car2 = new Car("Porsche" , "1234"); 

    System.out.println(car1.equals(car2));
    //true 

}

 

equal 메소드는 두 객체간의 동일한 값을 확인하기 위해서이다. 

즉, 내용물이 같아야한다는 뜻인데 Car 클래스 객체를 두개 생성해서 equals 연산을 한다면 "false" 라는 값이 나올 것이다. 

 

하지만 equals 메서드를 재정의를 해준다면 두 객체의 내용물을 확인해서 동일한 값을 갖는 객체인지 확인할 수 있다. 

 

public static void main(String[] args) {


    Car car1 = new Car("Porsche" , "1234"); 
    Car car2 = new Car("Porsche" , "1234"); 

    Map <Car, Integer> map = new HashMap<>(); 

    map.put(car1, 1); 
    map.put(car2, 1); 

    System.out.println(map.size());
    //2 출력됨
}

 

하지만 여기서 중요한 점이 있다. 

동일한 내용을 갖는 객체 두 개를 map 자료구조에 넣는다면 같은 key 라고 생각해도 되는 것이 아닌가? 

그렇다면 동일한 내용을 가지고 있는 객체 두개를 map에 넣었을 때 사이즈를 출력했을 때 1이 나올 것이라고 생각은 했지만 결과는 달랐다. 2가 출력된다. 

 

즉, HashMap 에서는 두 개의 객체를 서로 다른 key 값으로 본다는 것이다. 

이 부분은 hashCode에 대한 개념을 알아야한다. 

 

hashCode

 

int hashCode() 로 정의된 hashCode 메소드는 실행 중에 (Runtime) 객체의 유일한 integer 값을 반환을 하는데, 

Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다. (항상 그런것은 아니라고 한다.)

 

//hashCode 메소드
public native int hashCode();

 

여기서 native 키워드는 메소드가 JNI(Java Native Interface)라는 native code를 이용해 구현되었음을 의미한다.

native는 메소드에만 적용가능한 제어자로, C or C++ 등 Java가 아닌 언어로 구현된 부분을 JNI를 통해 Java에서 이용하고자 할 때 사용된다. 우리같은 일반 개발자는 어디에서도 사용할 수 없다.

 

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

 

아까 위에서 언급한 map 에 두 객체를 key로 갖는 값을 넣었을 때 사이즈가 2가 뜬 이유는 Hash를 사용한 Collection(HashMap, HashTable, HashSet, LinkedHashSet 등)은 key를 결정할때 hashCode()를 사용하기 때문에 그렇다.

 

즉, 언급했던 Collection 자료구조는 자료를 저장하기 위한 위치를 선택하기 위해 hashCode를 이용한다. 

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

 

그렇기 때문에 car1 과 car2 를 key 값으로 넣게 되었을 때 서로 다른 해시값이 반환되면서 다른 위치에 저장되는 것이다. 

 

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    result = prime * result + ((number == null) ? 0 : number.hashCode());
    return result;
}

 

결론은 위의 코드처럼 hashCode 또한 Car 클래스에 오버라이딩해서 재정의를 해줘야 한다.

 

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

 

위의 코드는 String 클래스의 hashCode이며 String 클래스 또한 hashCode를 오버라이딩 했다. 

여기서 특이한 점은 Car 클래스도 그렇고 String 클래스도 그렇고 31 이라는 숫자가 공통적으로 쓰인다. 

 

왜 31일까? 

31은 소수이면서 홀수이기 때문에 선택된 값이다. 만일 그 값이 짝수였고 곱셈 결과가 오버플로되었다면 정보는 사라졌을 것이다. 2로 곱하는 것은 비트를 왼쪽으로 shift하는 것과 같기 때문이다. 소수를 사용하는 이점은 그다지 분명하지 않지만 전통적으로 널리 사용된다. 31의 좋은 점은 곱셈을 시프트와 뺄셈의 조합으로 바꾸면 더 좋은 성능을 낼 수 있다는 것이다(31 * i는 (i « 5) - i 와 같다). 최신 VM은 이런 최적화를 자동으로 실행한다.

 

즉, 소수이면서 홀수이기 때문에 선택되었다. 

소수는 1과 자기 자신을 제외한 숫자이기 때문에 Hash하였을 경우 충돌이 가장 적은 숫자이다. 

 

 

그렇다면 HashMap, HashTable 등은 왜 Hashcode를 쓰는 것일까? 

 

이유는 객체를 비교할 때 드는 비용을 낮추기 위해서이다.

자바에서 2개의 객체가 같은지 비교할 때 equals()를 사용하는데, 여러 객체를 비교할 때 equals() 를 사용하면 Integer 를 비교하는 것에 비해 많은 시간이 소요된다. Java에서 hashCode는 Integer 이며, hashcode를 이용해서 객체를 비교하면 equals() 를 이용하는 것보다 시간이 단축된다. 그래서 HashMap 에서 hashcode를 이용하여 객체를 매핑하여 객체를 찾을 수 있다. 

 

그래서 두 객체의 hashcode가 다르면 두 객체는 바로 같지 않다는 결과가 나온다. 그리고 만약 hashcode 값이 동일할 때는 equals() 로 두 객체가 같은지 비교하는 것이다. 

 

 

equals() 를 재정의한다면 hashCode() 도 재정의 하자.

 

 

 

참고 블로그

 

반응형

댓글