동등성(equality)과 동일성(identity)
동일성(identity)
- 자바에서 동일성은 비교되는 두 변수의 값이 같은지를 비교한다.
- 물리적으로 같으냐를 생각해도 좋을 듯하다.
- ‘==’ 연산자로 비교를 진행한다.
동등성(equality)
- 자바에서 동등성은 두 객체의 내용이 같은지를 의미한다.
- 동일성과 비교해 논리적인 같음을 비교한다.
- 모든 자바 클래스는 Object 클래스를 상속받는데 Object 의 equals 로 동등성을 비교한다.
class Person{
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return Objects.equals(name, person.name);
}
}
new Person("hi").equals(new Person("hi")); => 동등함을 보장
eqauls만으로 동등성이 완벽히 보장이 될까
- 위에서 설명했듯이 동등성은 논리적인 같음을 의미한다.
- 하지만 eqauls 만 재정의한다고 모든 경우의 동등성을 보장하지 않을 수 있다.
public class Main {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
for (int i = 0; i < 100; i++) {
var a = new Person("hi");
set.add(a);
}
System.out.println(set.size());
}
}
- Set 자료형은 동등하다면 중복 저장하지 않는 특성을 가졌다.
- equals 를 재정의한 Person 이 동등함을 보장한다면 객체를 100 번 더한다면 set의 크기는 1이 되어야 한다.
- 100이 저장됐다. 이유는 Hash 관련 컬랙션의 내부 구현과 관련되어 있다.
HashMap 구현
- HashMap에 객체를 put 하면 위 방식대로 저장이 이루어진다.
- 우선 key 객체의 hashCode 값을 기반으로 Map 내부적인 연결리스트가 원소인 배열 크기로 나머지 연산을 통해 배열의 인덱스를 구한다.
- 배열 원소마다 연결리스트로 관리되며 순환하여 동등한 객체가 있는지 검사한다.
- 검사 로직은 다음과 같다.
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
- hash 값이 같고 equals 연산으로 같음을 보장해야 동등하다고 판단한다.
- HashMap은 동등하다면 value를 덮어씌우고 동등한 key 객체가 없으면 연결리스트 Leaf 에 새로운 노드를 추가하는 구조이다.
- 따라서 HashSet에 equals를 재정의한 내용이 같은 객체를 100번 추가했을 때 size가 1이 아닌 100이 나온이유가 hash 값이 달라서이다.
추가학습) hashCode를 재정의하지 않는다해도 hash 값이 같을 수 있다.
public class Main {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
HashMap<String, String> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
var a = new Person("hi");
set.add(a);
}
System.out.println(set.size());
}
- 아직 hashCode 값을 재정의하지 않고 반복 횟수를 키웠다.
- 1000000 의 크기가 나와야하지 않을까?
- 이상한 값이 나온 것을 볼 수 있다.
- 이유는 해시 충돌 문제 때문이다.
- 따라서 구현에 따라 다음과 같이 Hash 관련 컬렉션이 인식한다.
equals 구현 , hashCode 미구현 => 동등하지 않음 (불규칙적으로 가끔 동등할 수 있음, 예측불가)
equals 미구현 , hashCode 구현 => 동등하지 않음
equals 구현, hashCode 구현 => 동등함
equals 와 hashCode는 같이 재정의하자
- equals 만 구현하면 두 객체가 동등함을 기본적인 상황에서는 보장할 수 있다.
- 하지만 Hash 관련 컬렉션이나 hash 관련 연산이 이루어질 때 예측 못할 오류를 발생시킬 수 있다.
- hash 관련 연산이 없을거 같다고 하더라도 리팩터링이나 구현이 변경하여 예측못한 오류를 발생할 수 있기 때문에 hashCode와 같이 재정의하는 것이 좋다고 생각한다.