(Effective Java) 3장 모든 객체의 공통 메서드
아이템 10. equals는 일반 규약을 지켜 재정의하라
- 자바에서는
==
의 동작이 두 가지다.
- 원시 타입에 사용할 경우, 두 피연산자의 값이 같은지 비교 (동등성, equality)
- 참조 타입에 사용할 경우, 두 피연산자의 주소가 같은지 비교 (참조 비교, reference comparision)
- equals는 두 객체가 물리적으로 같은가가 아니라 논리적 동치성을 확인해야 할 때 재정의 한다.
- equals 메서드를 재정의 할 때는 반드시 일반 규약을 따라야 한다.
- equals 메서드는 동치관계(equivalence relation)를 구현하며, 다음을 만족한다.
1
2
3
4
5
6
7
// x, y, z는 null이 아닌 참조 값이어야 함. Objects.requireNonNull() 사용 권장.
반사성(reflexivity) :
x.equals(x) == true
대칭성(symmetry) : x,equals(y) == true 이면, y.equals(x) == true 여야 한다.
추이성(transitivity): x.equals(y) == true && y.equals(z) == true 이면, x.equals(z) == true 여야 한다.
일관성(consistency) : x.equals(y) 를 반복해서 호출해도 항상 결과가 true이거나 항상 false여야 한다.
Non-nullity : x.equals(null) == false
1
2
3
4
5
6
7
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other !is Client)
return false
return name == other.name && postalCode == other.postalCode
}
}
구체 클래스를 확장(상속)해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
- equals 규약을 어찌 만족시키더라도, 리스코프 치환 원칙(상위 타입과 하위 타입의 호환)을 위배할 수 밖에 없다.
- https://github.com/umbum/effective-java-3e-source-code/blob/master/src/effectivejava/chapter3/item10/Point.java#L20-L26
- 구체 클래스가 아니라 추상 클래스를 확장하는 경우 equals 규약을 만족하면서 확장할 수 있다. 추상 클래스는 인스턴스화가 불가능하기 때문.
- 상속 대신 컴포지션 을 사용하면 새로운 값을 추가하면서 equals 규약을 만족할 수 있다. (상위 타입 변수를 private 변수로 가지고 있는 식.)
- 잘못 구현된 equals의 예
- java.sql.Timestamp는 java.util.Date라는 구체 클래스를 확장한 후 nanoseconds 필드를 추가하면서 대칭성을 위배.
- java.net.URL의 equals는 URL과 호스트의 IP 주소를 이용해 비교하는데, 네트워크를 통하게 되므로 일관성이 없음.
- 처음에
o == null
로 체크하는건 불필요하다.java instanceof
는 첫 번째 인자가 null이면 알아서 false를 반환하고, 인스턴스 체크는 꼭 해야 하기 때문에 이걸로 된다.
- 실무에서는 lombok @Data(에 포함된
@EqualsAndHashCode
)를 써서 정의하는 경우도 많다.- 필드를 하나씩 비교하는 과정에서 equals를 쓰고, 또 그 안에서 다시 equals를 쓰고 하는 경우가 있을 수 있어 이런 자동 생성된 코드가 완벽하게 문제가 없으리라고 단언할 수는 없지만, 이 것 때문에 문제가 발생하는 경우가 많지는 않아서 실무에서도 많이 사용한다.
- 컴파일 이후 target/classes 폴더 아래의 .class 파일을 확인하면 lombok이 equals()와 hashCode()를 어떻게 구현하고 있는지 확인할 수 있다.
아이템 11. equals를 재정의하려거든 hashCode도 재정의하라
- equals만 재정의하는 경우,
hashSet()
등에서는 같은 객체더라도kt false
가 발생한다. - 실제로
HashSet
의 원소 비교는- 객체의 해시 코드를 비교하고,
- 객체의 내용을 비교.
- 방식으로 이루어지기 때문에 객체의 내용이 같더라도 해시 코드가 다르면
false
를 반환하게 된다.
- 방식으로 이루어지기 때문에 객체의 내용이 같더라도 해시 코드가 다르면
- 그래서 JVM 기반 언어에서는 “equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다.” 는 제약이 있다.
hashCode()
는 아래과 같이 간단하게 구현할 수 있다.- 31은 소수라서 고른 것이며,
31 * i
는(i << 5) - i
와 같으므로 연산 시 이득이 있다.
- 31은 소수라서 고른 것이며,
1
2
3
class Client(val name: String, val postalCode: Int) {
override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}
아이템 12. toString을 항상 재정의하라
- 주로 lombok을 사용한다.
아이템 13. clone 재정의는 주의해서 진행하라 : 그 보다는 복사 팩터리를 사용하라
- clone 메서드는 내부에서 super.clone()을 호출하고, 자기 자신으로 캐스팅해서 리턴하는 식으로 되어 있다.(deep copy가 필요하다면 그 작업도 해주고.)
- super.clone()의 반환 객체는 상위 클래스…clone()을 따라가다 최종적으로 Object 타입이 될 텐데, Object를 하위 클래스로 캐스팅하는게 왜 오류가 안나는 것인가?
일반적으로 clone/Cloneable 재정의 보다는, **복사 생성자 복사 팩터리** 를 사용하는 것이 좋다. - 복사 생성자/복사 팩터리는 객체를 인자로 받아 같은 타입의 객체를 새로 만들어주는 생성자나 정적 팩터리 메서드를 말한다.
- clone 메서드 방식을 쓸만한 유일한 케이스는 배열 이다.
아이템 14. Comparable을 구현할지 고려하라
- compareTo 규약을 지켜서 구현하도록 한다. (equals 규약과 비슷하고, 책에서 명시하고 있다.)
- 구현하면 TreeSet, TreeMap, Collections, Arrays 등 비교를 활용하는 클래스와 사용할 수 있다.
- equals 규약과 비슷하기 때문에, 구체 클래스를 확장하면서 새로운 필드를 추가하는 경우 compareTo 규약을 지킬 방법이 없다.
- 상속 대신 컴포지션을 쓰면(상속 말고 내부에 멤버 변수로 두면) 규약을 지키면서 구현 가능하다.
This post is licensed under CC BY 4.0 by the author.