자바 개발자로 취업을 준비하면서 모의 면접을 보게 되면 가장 많이 받는 질문 중 하나가 equals()
와 hashCode()
를 같이 재정의하는 이유일 것입니다.
솔직히, 이제는 이 질문이 너무 유명해져서 면접 때는 잘 안 나올 것 같지만 반드시 짚고 넘어가야 하는 개념이기 때문에 포스팅을 해봤습니다!
특히, Object
클래스와 String
클래스에서의 equals()
메소드의 작동 원리가 조금 다리기 때문에 이 부분도 공부해보겠습니다.
Object
클래스의 equals()
먼저 Object
클래스의 equals
메소드의 내부 먼저 보겠습니다
Object.java
public boolean equals(Object obj) {
return (this == obj);
}
Object
클래스의 equals
는 객체의 메모리 주소가 같은지의 여부를 확인합니다.
따라서 만약에 새로운 객체를 만들고 객체의 내용이 같은지 판단하기 위해서는 equals()
메소드를 재정의해야 합니다.
class Node{
String name;
Node(String name){
this.name = name;
}
}
public class Main {
public static void main(String[] args){
Node node1 = new Node("hello");
Node node2 = new Node("hello");
System.out.println(node1.hashCode()); // 1159190947
System.out.println(node2.hashCode()); // 925858445
System.out.println(node1.equals(node2)); // false
System.out.println(node1 == node2); // false
}
}
new()
키워드로 객체를 생성할 때 메모리의 heap 영역에 새로운 객체가 들어갑니다. 따라서 node1과 node2는 같은 내용을 가진 객체이지만 new()
키워드로 각각 생성됐기 때문에 다른 객체가 됩니다.
여기서 내용이 같을 경우에 따른 동등성을 판별하기 위해서는 equals()
메소드를 재정의해야 합니다.
class Node{
String name;
Node(String name){
this.name = name;
}
@Override
public boolean equals(Object o){
if (this == o){
return true;
}
if (o == null || o.getClass() != this.getClass()){
return false;
}
Node node = (Node) o;
return Objects.equals(name, node.name);
}
}
public class Main {
public static void main(String[] args){
Node node1 = new Node("hello");
Node node2 = new Node("hello");
System.out.println(node1.hashCode()); // 1159190947
System.out.println(node2.hashCode()); // 925858445
System.out.println(node1.equals(node2)); // true
System.out.println(node1 == node2); // false
}
}
equals()
를 재정의했기 때문에 node1과 node2는 논리적으로는 동일하다고 판별이 될 수 있습니다.
하지만 여기서 문제가 발생할 수 있습니다.
HashMap
, HashSet
과 같은 hash 값을 사용하는 Collection
을 쓰면 여전히 작동이 잘될까요?
class Node{
String name;
Node(String name){
this.name = name;
}
@Override
public boolean equals(Object o){
if (this == o){
return true;
}
if (o == null || o.getClass() != this.getClass()){
return false;
}
Node node = (Node) o;
return Objects.equals(name, node.name);
}
}
public class Main {
public static void main(String[] args){
Node node1 = new Node("hello");
Node node2 = new Node("hello");
System.out.println(node1.hashCode()); // 1159190947
System.out.println(node2.hashCode()); // 925858445
System.out.println(node1.equals(node2)); // true
System.out.println(node1 == node2); // false
Set<Node> hSet = new HashSet<Node>();
hSet.add(node1);
hSet.add(node2);
System.out.println(hSet.size()); // 2
}
}
우리가 원하는 HashSet
의 사이즈는 1일 겁니다. 왜냐하면, 우리는 node1과 node2를 동일하게 바라보고 싶기 때문입니다.
하지만, 결과는 2가 나오면서 node1과 node2가 동등하지 못하다고 판별하고 있습니다.
이런 현상이 발생하는 이유는 우리가 hashCode()
를 재정의하지 않았기 때문입니다.
hashCode에 대해서 설명하면 Object 클래스의 hashCode 메소드는 객체의 고유한 주소 값을 int 값으로 변환합니다.
즉, 객체가 다르면 다른 hashCode값을 리턴합니다.
HashMap
, HashSet
과 같이 hash 값을 사용하는 경우 객체가 논리적으로 동일한지 비교하기 위해서는 hashCode()
리턴 값의 일치 여부를 먼저 확인한 뒤 equals 여부를 확인합니다.
현재의 node1과 node2의 hashCode는 1159190947, 925858445으로 다르기 때문에 HashSet
에서 두 Node 객체가 논리적으로 다르다고 판별하고 있는 것입니다.
class Node{
String name;
Node(String name){
this.name = name;
}
@Override
public boolean equals(Object o){
if (this == o){
return true;
}
if (o == null || o.getClass() != this.getClass()){
return false;
}
Node node = (Node) o;
return Objects.equals(name, node.name);
}
@Override
public int hashCode(){
return Objects.hash(name);
}
}
여기서는 int hashCode()
를 재정의받아서 Objects.hash(value)
를 return
해주면 됩니다.
JDK 1.8
부터 hash()
메소드가 추가됐는데, Objects.hash()
의 역할은 해당 파라미터에 대한 해시 값을 만들어주는 역할을 합니다.
내용이 동일한 객체의 경우 동일한 hashCode()
를 반환하는 형태로 코드가 변하기 때문에 HashSet
의 사이즈는 1이 됩니다.
System.out.println(hSet.size()); // 1
hash 값을 사용하는 인터페이스나 구현체 클래스가 없다면 equals()
와 hashCode()
를 같이 재정의할 필요가 없을지도 모르지만, 그래도 안전하게 개발을 하고 싶다면 같이 재정의하는 방식으로 설계할 필요가 있습니다.
말씀하신대로 사골처럼 나오는 질문이죠! 여기서 +@의 심화적 내용도 알고 계시면 좋을 것 같습니다.