동일성(identity)
객체의 동일성이란 두 객체가 주소값까지 동일해 객체의 정보들까지 동일할 수 밖에 없는 완전히 같은 객체임을 의미한다.
동등성(equality)
객체의 동등성은 두 객체가 가지고 있는 정보가 같을 때를 의미한다.
즉 동일성은 객체의 주소값과 정보 모두 같으니.
동일하다는 것은 동등한 것이지만. 동등하다는 것은 동일하다는 것은 아니다.
==
"==" 는 피연산자 둘의 주소값을 비교하여 주소값이 동일할 시 true, 아니라면 false를 반환한다.
즉 동일성을 즉각적으로 비교해주는 연산자이다.
오 reference type 끼리의 "==" 연산은 Heap에서의 주소값으로 비교를 하는군!
그러나 우리는 종종 Objects에 포함되지 않는 primitive type(int, byte, short, long, float, double, boolean, char 자료형)들에도 "==" 연산을 사용하는데 이럴 땐 어떻게 작동할까?
이들은 반복적인 변수 선언에서의 메모리 중복을 피하기 위해 Constant pool에 담겨지는데, 이 constant pool에서 가져오는 것이니 주소값을 비교한다고 보아도 무방하다.
eqauls
equals 메소드가 동등성을 판별해준다. 라고 말하면 엄밀히는 틀린 말이다.
왜냐하면 우리 모든 객체들의 조상 Object class에 정의된 equals를 한번 살펴보자
//Object.java
public boolean equals(Object obj) {
return (this == obj);
}
놀랍게도 "==" 연산을 반환해준다.
최상위 부모 클래스에선 동일성 비교를 한다는 사실은 변함없지만 자식 클래스들은 다양한 방식으로 equals를 재정의하여 사용하고 있다.
가장 먼저 Object 유틸 클래스인 Objects에서는
//Objects.java
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
기본적으로 "==" 연산으로 동일성 비교를 먼저 한 후 인자 a의 reference type만의 equals(만약 재정의되지 않았다면 Object.java의 동일성 비교를 함)를 통해 동등성 비교를 한다.
그럼 이번엔 String 클래스에 정의된 equals를 한번 보자.
//String.java
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value);
}
대부분의 동등성 비교 메소드가 비슷한데. 일단 "==" 연산으로 동일성 비교를 먼저 진행해준다.
두 객체가 동일하다면 동등성은 보나마나 보장되는 것이기 때문이다.
그 다음 instanceof 로 두 객체가 같은 reference type인지 비교 후 가지고 있는 데이터가 동일한 지 각자의 개성에 따라 판단해주는 모양새다.
참고로 String은 String을 위한 contstant pool도 제공되고 있어
String a = "haha";
String b = "haha";
//sout(a.eqauls(b)) 하면 true
이렇게 된다고 한다.
//List.java
boolean equals(Object o);
/**
* Returns the hash code value for this list. The hash code of a list
* is defined to be the result of the following calculation:
* <pre>{@code
* int hashCode = 1;
* for (E e : list)
* hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
* }</pre>
* This ensures that {@code list1.equals(list2)} implies that
* {@code list1.hashCode()==list2.hashCode()} for any two lists,
* {@code list1} and {@code list2}, as required by the general
* contract of {@link Object#hashCode}.
*
* @return the hash code value for this list
* @see Object#equals(Object)
* @see #equals(Object)
*/
다음은 List interface의 equals 메소드이다. 이 친구는 List 내의 모든 원소들의 hashcode들을 이용하여 동등성을 판단해준다고 한다.
여기서 hashcode() 라는 새로운 메소드가 나오는데. 이 친구는 동일성을 판별하는데 도움을 주는 메소드이다.
hashcode()
일단 간략하게 설명하면 hashcode는 객체의 주소값으로 만든 하나의 고유번호이다.
//Object.java
@IntrinsicCandidate
public native int hashCode();
여기서 native 키워드는 'Java가 아닌 언어로 구현된 부분'을 JNI(java native interface)를 통해 java에서 쓰인다는 의미라고 한다.
JVM이 내부동작을 통해 객체 생성시 int 값을 반환해주는 것이다. 따라서 따로 재정의하지 않아도 모든 객체에서 사용이 가능하다.
hashcode는 주소값을 통해 얻은 고유값이라는 의미이기 때문에 hashcode가 같다면 동일한 객체라도 봐도 된다는 것이다.
그러나 여기 예외가 있는데.....
String.java에 재정의된 hashcode 메소드를 살펴보자.
public int hashCode() {
int h = hash;
if (h == 0 && !hashIsZero) {
h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
hashIsZero = true;
} else {
hash = h;
}
}
return h;
}
이를 이해하기엔 내 식견이 너무 짧다... 주석을 보아하니
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
이런식으로 char 하나하나 ascii code로 받아와서 해시코드를 구해준다고 한다.
이 말은 주소값이 다른 String이라도 담긴 데이터가 같다면 hashcode가 동일하다는 것이다!
그런데 이뿐만이 아니다. String에 담길 수 있는 값은 무궁무진하다.
그러나 hashcode는 int형이기 때문에(0부터 0xFFFFFFFF(4,294,967,295)) 데이터가 다르더라도 hashcode는 동일한 경우도 나온다.
이로 인해 hashcode collision이란 현상도 생기게 되는데 글이 너무 길어졌으니 다음에 알아보도록 하자..
틀린 부분이 있다면 지적 부탁드립니다!
출처
Java hashcode() https://brunch.co.kr/@mystoryg/133
equals와 hashcode https://mangkyu.tistory.com/101
String의 hashcode가 유일하지 않은 이유 https://blog.ggaman.com/916