Post

(Effective Java) 3장 모든 객체의 공통 메서드

아이템 10. equals는 일반 규약을 지켜 재정의하라

  • 자바에서는 ==의 동작이 두 가지다.
  1. 원시 타입에 사용할 경우, 두 피연산자의 값이 같은지 비교 (동등성, equality)
  2. 참조 타입에 사용할 경우, 두 피연산자의 주소가 같은지 비교 (참조 비교, 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 규약을 만족할 수 있다. (상위 타입 변수를 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의 원소 비교는
    1. 객체의 해시 코드를 비교하고,
    2. 객체의 내용을 비교.
      • 방식으로 이루어지기 때문에 객체의 내용이 같더라도 해시 코드가 다르면 false를 반환하게 된다.
  • 그래서 JVM 기반 언어에서는 “equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다.” 는 제약이 있다.
  • hashCode()는 아래과 같이 간단하게 구현할 수 있다.
    • 31은 소수라서 고른 것이며, 31 * i(i << 5) - i 와 같으므로 연산 시 이득이 있다.
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.