아이템 41 - hashCode의 규약을 지켜라
hashCode 함수는 수많은 컬렉션과 알고리즘에 사용되는 해시 테이블을 구축할 때 사용됩니다.
해시 테이블
해시 테이블은 컬렉션에 요소를 빠르게 추가하고, 빠르게 추출할 때 사용하기 좋습니다.
해시 함수는 데이터를 추가할 때 특정한 숫자(해시 값)를 만들어서 특정한 버킷에 넣습니다. 이때 같은 데이터는 항상 같은 버킷에 들어갑니다.
데이터를 찾을 때는 해시 값을 게산해서 해당 버킷을 찾은 뒤, 버킷 내부에서 원하는 요소를 찾습니다. 해시 함수는 같은 요소라면 같은 값을 리턴하므로, 다른 버킷을 확인할 필요 없이 바로 원하는 것이 들어 있는 버킷을 찾을 수 있습니다.
가변성과 관련된 문제
해시 테이블을 사용하는 경우에는 요소가 변경되어도 해시 코드는 다시 계산되지 않으며, 버킷 재배치도 이루어지지 않습니다. 그래서 mutable한 객체가 변경되면 기존 해시 코드와 일치하지 않을 수가 있습니다.
data class Person(var name: String, var age: Int)
fun main() {
val set = HashSet<Person>()
val person = Person("Alice", 25)
set.add(person) // 해시 값이 계산되고 버킷에 저장됨
println(set.contains(person)) // ✅ true (정상)
// 🔥 객체의 값을 변경 (해시 값이 바뀔 가능성이 있음)
person.name = "Bob"
println(set.contains(person)) // false (찾을 수 없음!)
}
그래서 해시 등의 자료 구조구조에서는 mutable 객체가 사용되지 않습니다.
hashCode의 규약
hashCode에는 명확한 규약이 있습니다.
- 어떤 객체를 변경하지 않았다면, hashCode는 여러 번 호출해도 그 결과가 항상 같아야 합니다.
- equals 메서드의 실행 결과로 두 객체가 같다고 나온다면, hashCode 메서드의 호출 결과도 같다고 나와야 합니다.
첫번째 요구 사항은 일관성 유지를 위해 hashCode가 필요하다는 것입니다.
두번째 요구 사항을 지키지 않는다면 컬렉션 내부에 요소가 들어 있는지 제대로 확인하지 못하는 문제가 발생할 수 있습니다. 아래의 예시에서는 hashCode를 오버라이드 하지 않았습니다.
class FullName(
var name: String,
var surname: String,
) {
override fun equals(other: Any?): Boolean {
return other is FullName
&& other.name == name
&& other.surname == surname
}
}
fun main() {
val set = mutableSetOf<FullName>()
set.add(FullName("Marcin","Moskala"))
val fullName = FullName("Marcin","Moskala")
println(fullName in set) // false
println(fullName == set.first()) // true
}
그래서 코틀린은 equals 구현을 오버라이드 할 때, hashCode도 함께 오버라이드 하는 것을 추천합니다.
그리고 이전에 hashCode 함수를 통해 어떤 버킷에 배치할지를 결정한다고 말했습니다. 그렇기 때문에 만약 동일한 hashCode를 가진 요소가 많아진다면, 동일한 버킷에 많은 요소를 배치할 것이기 때문에 해시 테이블을 사용하는 의미 자체가 없어질 수 있습니다. 그렇기 때문에 규약을 위반하는 것은 아니지만, 이를 지양하는 것이 좋습니다.
hashCode 구현하기
hashCode는 일반적으로 모든 해시 코드의 값을 더하는 과정마다 31을 곱하는 방식으로 구해집니다. 31을 꼭 사용해야 하는것은 아니지만 관례적으로 31을 많이 사용합니다.
data class FullName(val name: String, val surname: String) {
// hashCode 구현 시 31을 사용한 예시
override fun hashCode(): Int {
var result = 17 // 기본값을 17로 설정 (보통 임의의 홀수로 시작)
result = 31 * result + name.hashCode() // name을 기반으로 hashCode 계산
result = 31 * result + surname.hashCode() // surname을 기반으로 hashCode 계산
return result
}
override fun equals(other: Any?): Boolean {
return other is FullName && other.name == name && other.surname == surname
}
}
hashCode를 일반적으로 직접 구현할 일은 없습니다. 하지만 만약 구현해야 한다면 꼭 equals로 같이 구현해야 합니다.