일하던 중 특정 객체들을 담은 List 한 개와 다른 List를 비교해서 객체의 필드들이 똑같으면 제거해야하는 상황이 있었습니다. 처음에 든 생각은 List를 Set에 담으면 문제를 해결할 수 있을 것이라고 생각했습니다. 하지만 원하는 결과를 얻지 못했습니다. 그 이유를 정리해보겠습니다.
두 변수의 값이 같은지 다른지 동등 여부를 비교할 때 사용하는 것이 equals() 메서드입니다. == 연산자의 경우 객체의 주소값을 비교합니다. 그래서 비교하는 객체가 동일한 객체인지를 판별합니다. Primitive Type의 객체에 대해서는 값 비교가 가능하고, Reference Type에 대해서는 주소 비교를 수행합니다.
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2); // true
문자열이 아닌 객체를 비교할 때, == 와 equals()는 똑같습니다. equals()로 비교할 때, 문자열이 아닐 경우 두 객체의 주소값이 같은지 아닌지 여부로 판단합니다.
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Example {
public static void main(String[] args) {
Student st1 = new Student("김김김", 20);
Student st2 = new Student("김김김", 20);
System.out.println(st1 == st2); // == 은 객체 타입인경우 주소값을 비교합니다. 서로 다른 객체는 다른 주소를 가지고 있기 때문에 false가 출력됩니다.
System.out.println(st1.equals(st2)); // equals 또한 객체 타입인 경우 주소값을 비교하기 때문에 false가 출력됩니다.
}
}
사용자 입장에서 st1와 st2는 김김김이라는 이름과 20살의 나이를 갖는 똑같은 객체이지만, 컴퓨터의 관점에서는 서로 각기 다른 객체를 초기화해 힙 영역에 따로 저장해두고 있으니 다른 객체입니다.
두 객체가 같은 데이터를 저장하고 있으면 같은 객체로 판단하도록 컴퓨터에게 명령하고 싶으면 어떻게 해야할까요?
equals()를 오버라이딩해서 주소값이 아닌 객체 필드값을 비교하도록 재정의해주면 됩니다.
import java.util.Objects;
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 만일 현 객체 this와 매개변수 객체가 같을 경우 true
if (!(o instanceof Student)) return false; // 만일 매개변수 객체가 Student 타입과 호환되지 않으면 false
Student student = (Student) o; // 만일 매개변수 객체가 Student 타입과 호환된다면 다운캐스팅(down casting) 진행
return Objects.equals(this.name, student.name) && Objects.equals(this.age, student.age); // this객체 이름, 나이와 매개변수 객체 이름, 나이가 같을경우 true, 다를 경우 false
}
}
public class Main {
public static void main(String[] args) {
Student st1 = new Student("김김김", 20);
Student st2 = new Student("김김김", 20);
System.out.println(st1.equals(st2)); // true
}
}
String 클래스의 equals()도 사실 위와 같은 방식으로 재정의 된 것입니다.
hashCode 메서드는 객체의 주소값을 이용하여 해싱 기법을 통해 해시코드를 만든 후 반환합니다. 그렇기 때문에 서로 다른 두 객체는 절대 같은 해시코드를 가질수 없습니다. 해시코드는 엄밀히 말하면 객체ㅍ의 주소값은 아니고, 주소값을 이용하여 만든 고유한 숫자값입니다.
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
public class Example {
public static void main(String[] args) {
Student st1 = new Student("김김김", 20);
Student st2 = new Student("김김김", 20);
System.out.println(st1.hashCode()); // 2060468723
System.out.println(st2.hashCode()); // 622488023
}
}
객체의 필드값을 비교하기위해 equals()를 오버라이딩 했으면 hashCode() 또한 같이 오버라이딩해야합니다. 왜냐하면 equals()의 결과가 true인 두 객체의 해시코드도 반드시 같아야한다는 자바의 규칙때문에 그렇습니다.
이것이 equals()와 hashCode()를 같이 재정의해야하는 이유입니다.
요약!! hash 값을 사용하는 Collection Framework(HashSet, HashMap, HashTable)을 사용할 때 문제가 발생합니다.
public class Example {
public static void main(String[] args) {
Student st1 = new Student("김김김", 20);
Student st2 = new Student("김김김", 20);
System.out.println(st1.equals(st2));
List<Student> list = new ArrayList<>();
list.add(st1);
list.add(st2);
System.out.println(list.size()); // 2
Set<Student> set = new HashSet<>(list);
System.out.println(set.size()); // 2
}
}
equals()만 오버라이딩하고, 위 코드를 작성하면 list의 사이즈 2, set의 사이즈는 1로 예상했습니다. 하지만 list의 사이즈만 예상값과 같고, set의 값은 예상값과 다릅니다. st1과 st2는 데이터적으로는 같지만, 해시코드가 같기 때문에 중복된 데이터가 컬렉션에 추가된 것입니다.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Student)) return false;
Student student = (Student) o;
return Objects.equals(this.name, student.name) && Objects.equals(this.age, student.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class Example {
public static void main(String[] args) {
Student st1 = new Student("김김김", 20);
Student st2 = new Student("김김김", 20);
System.out.println(st1.equals(st2));
List<Student> list = new ArrayList<>();
list.add(st1);
list.add(st2);
System.out.println(list.size()); // 2
System.out.println(st1.hashCode()); // 1373169045
System.out.println(st2.hashCode()); // 1373169045
Set<Student> set = new HashSet<>(list);
System.out.println(set.size()); // 1
}
}
Objects.hash()는 매개변수로 주어진 값들을 이용해서 고유한 해시 코드를 생성합니다. 즉, 동일한 값을 가지는 객체의 필드로 해시코드를 생성하게 되면, 동일한 해시코드를 가질 수 있게 되고, Set 자료형에 중복된 데이터로 판단하여 한번만 추가된 것을 확인할 수 있습니다.