회사 업무 중에 Spring 내부를 파보다가 Java의
==
과equals
를 만났습니다.
몇년간 Kotlin만 사용해오다가, 문득 자바 코드를 보니 기억이 잘 나지 않았습니다.
그래서 이번 기회에 자바와 코틀린의 동등성과 동일성에 대한 것에 hashCode를 한 스푼 추가해 정리를 해두려고 합니다.
동등성이란?
두 객체가 내부에 동일한 데이터를 가지고 있는지 의미합니다.
동일성이란?
두 객체가 참조하고 있는 주소값이 같은지를 의미합니다.
자바에서 참조 타입인 경우에 동등성은 equals
를 사용해서 확인합니다.
간단한 예시를 하나 보겠습니다.
public class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
Student라는 클래스는 이름(name)과 나이(age) 필드를 가지고 있습니다.
여기서 두개의 인스턴스를 생성해보고, 동등성을 확인해보겠습니다.
Student ekko1 = new Student("ekko", 33);
Student ekko2 = new Student("ekko", 33);
System.out.println(ekko1.equals(ekko2));
결과는 어떻게 나올까요?
두 인스턴스가 모두 같은 필드값을 가지고 있으니까 true일까요?
결과는 false입니다.
그 이유는 아래와 같이, Object의 equals이 기본적으로 동일성(==
)을 검사하기 때문입니다. (참조하고 있는 주소가 같은지 검사)
여기서 우리는 Student의 동등성을 확인할 수 있도록 보장하려면 equals를 재정의해야겠구나
라는 생각이 듭니다. equals & hashCode를 override하는 코드를 Student class에 추가해줬습니다.
// Student class 내부
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && name.equals(student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
그리고 다시 ekko1과 ekko2의 동등성을 확인하는 코드를 실행해보면, true가 출력됩니다.
여기서 hashCode는 왜 override하는 것인지 궁금하실 겁니다.
자바에서는 equals를 override할 때 반드시 hashCode도 override해야합니다.
기본적으로 Object의 hashCode는 객체의 주소값을 int 형으로 반환합니다.
hashCode를 override하지 않으면 hash를 사용하는 Collection 등을 사용할 때 문제가 발생할 수 있습니다.
예를 들어 hash를 사용하는 Collection을 사용할 때, hashCode를 override하지 않으면 동등성을 가진 두 객체가 중복되어 들어갈 수 있습니다. 또한 contains로 확인해도 마찬가지다.
// hashCode를 override하지 않은 경우
Student ekko1 = new Student("ekko", 33);
HashSet<Student> students = new HashSet<>();
students.add(ekko1);
System.out.println(students.contains(new Student("ekko", 33))); // false
Student class에서 hashCode를 오버라이드하지 않아서 false가 출력된다.
JVM 언어에서는 hashCode가 지켜야 하는 "equals가 true를 반환하는 두 객체는 반드시 같은 hashCode를 반환해야 한다"
라는 제약이 있는데, Student class는 이를 어기고 있다.
따라서 equals를 오버라이드 했으므로 hashCode도 오버라이드 해야한다.
자바의 참조 변수에 대한 동일성은 ==
으로 확인합니다.
두 객체가 참조하고 있는 주소가 같다면 true, 아니라면 false로 나올 것입니다.
자바의 동일성은 간단히 예시만 보고 넘어가겠습니다.
Student ekko1 = new Student("ekko", 33);
Student ekko2 = new Student("ekko", 33);
Student ekko3 = ekko1;
System.out.println(ekko1 == ekko2); // false
System.out.println(ekko1 == ekko3); // true
ekko1과 ekko2는 메모리의 서로 다른 위치에 존재하므로, 주소값이 달라 false이며.
ekko1과 ekko3는 참조하는 주소값이 같으므로 true가 나옵니다.
코틀린에서는 동등성을 비교하기 위해 ==
을 사용합니다.
코틀린의 ==
은 내부적으로 equals
를 호출해서 객체를 비교합니다.
따라서 클래스가 equals
를 오버라이드하면 ==
을 통해 안전하게 그 클래스의 인스턴스를 비교할 수 있습니다. (override하지 않으면 동일성 비교함 주의)
예시를 보겠습니다.
class Student(
val name: String,
val age: Int
)
val ekko1 = Student("ekko", 33)
val ekko2 = Student("ekko", 33)
val ekko3 = ekko1
println(ekko1 == ekko2) // false
println(ekko1 == ekko3) // true
Student class를 생성하고, 동일한 필드값을 가지는 ekko1, ekko2 두 객체를 생성했습니다.
첫번째로 ekko1 == ekko2
를 보겠습니다.
Kotlin의 ==
은 내부적으로 equals
를 호출하는데, equals를 오버라이딩하지 않아서 동일성을 확인하므로 false가 출력됩니다.
두번째로 ekko1 == ekko3
는 같은 메모리 주소를 참조하므로 true가 출력됩니다.
이제 equals와 hashCode를 override한 코드를 보겠습니다.
class Student(
val name: String,
val age: Int
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Student
if (name != other.name) return false
if (age != other.age) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + age
return result
}
}
val ekko1 = Student("ekko", 33)
val ekko2 = Student("ekko", 33)
val ekko3 = ekko1
val ekko4 = Student("jungle", 31)
println(ekko1 == ekko2) // true
println(ekko1 == ekko3) // true
println(ekko1 == ekko4) // false
equals를 오버라이드했으니, 코드와 주석으로 된 결과를 보시면 당연한 결과로 보입니다.
참고로 여기서는 일반 class를 사용했으나, data class를 사용하면 equals, hashCode, copy, toString가 자동으로 생성됩니다.
동일성은 참조하는 주소값이 같은지 확인하는 것이라고 했습니다.
코틀린에서는 동일성을 확인하기 위해 ===
연산자를 사용합니다.
이 연산자는 자바에서 객체의 참조를 비교할 때 사용하는 ==
연산자와 같습니다.
class Student(
val name: String,
val age: Int
)
val ekko1 = Student("ekko", 33)
val ekko2 = Student("ekko", 33)
val ekko3 = ekko1
println(ekko1 === ekko2) // false
println(ekko1 === ekko3) // true
==
비교를 하므로 오버라이드가 필요==
연산자 사용==
연산자 사용, ==
연산자는 내부적으로 equals
를 호출. ===
연산자 사용