2017년 5월 5일 금요일

effective java 규칙 8. equals를 재정의할 때는 일반 규약을 따르라

요약 : class 를 설계할때 Object class 의 비 final 메소드를 재정의 할때는 각 비 final 메소드들의 일반 규약에 따라 재정의 해야한다.
그래야 일반 규약으로 작동하는 class 와 같이 사용이 가능하며 규약을 따르지 않으면 정상적인 작동을 보장하지 않는다.
예를 들자면 Set 과 같은 경우 외부로부터 인자를 받아 내부 배열변수에 저장한다.
이때 중복된 인자는 저장 하지 않는다. 중복된 인자인지 여부를 equals 로 체크한다고 하면 (실제로는 hashCode() 로 비교하는듯하다.) 이 Set 타입의 객체는 add method 로 전달 받을 인자(외부에서 add 메소드 로 입력받은 인자)는 일반 규약을 따라 equals 가 재정의 되었다고 전제한 상태에서 add method 로 부터 들어온 인자를 Set 내부에 배열로 담겨진 기존 인자들과 일반 규약에 의거해 비교할 것이기 때문에 일반 규약을 따르지 않는 인자들의 정상적인 동작을 보장할 수 없다.

그래서 일반적으로 class 를 작성할때 Object 의 비 final 메소드를 재정의 한다.

그렇지만 object class 의 equals method 를 재정의 할 때 (재정의 자체가 쉬워보이는 것 만큼) 생각보다 실수할 여지가 많다.
그렇기 때문에 객체간의 동일성 비교가 필요하지 않을 경우엔 Object class 의 equals method 를 override 하지 않아도 된다.
재정의 하지 않을 경우 모든 객체는 자기 자신하고만 같게 된다.(object class 의 equals 의 기본규칙)

다음은 Object class 의 equals method 재정의가 불필요한 경우이다.

    1. 각각의 객체가 고유하다. 값을 나타내는 클래스가 아닌 활성 개체를 나타내는 클래스 일 경우
  • Thread class 같은 경우
  • 실행 단위를 표현하는 Thread 는 값이 동일성 비교가 필요없다. a thread 와 b thread 객체는 단지 실행단위이기 때문에 동일성 비교는 필요 없고 thread 는 고유한 단위이기 때문에 동일성비교는 object 의 equas 메소드의 일반 규약만으로도 충분하다.
  • 값의 비교가 필요없는 개체일 경우, 기능을 모아놓은 클래스 같은 경우
    2. 논리적 동일성 검사 방법이 있건 없건 상관없다. equals method 유무가 의미가 없다.
            java.util.Random class 같은 경우 equals 를 override 하여 생성된 난수값이 같은지를 비교할 순 있지만
            Random class 의 목적이 객체의 동일성 비교에 있지 않은 만큼 equals 비교는 무의미 하다.
            즉 class 설계자가 동일성을 비교할 필요가 없도록 의도하여 설계하였다면 equals 의 비교는 무의미 하다.
    3. class 가 private 으로 선언되어있거나 package-private 이면서 equals 를 호출할 일이 없다면 equals 의 override는 불필요하다.
            그래도 클라이언트가 의도와 다르게 equals 비교를 포함한 로직을 작성할 수도 있기에
            override equals 를 호출할때 exception 을 throw 하도록 구성하는 것을 권장

            @Override
            public Boolean eqauls(Object o) {
                throw new AssertionError(); // 호출되면 안 되는 메서드를 호출시 exception throw
            }  
        private class 는 내부 클래스에서만 지정 가능한 접근 한정자 이며 그렇지 않을 경우 compile 시점에 error 발생한다.
            내부 private class 에 구현로직을 만들어 외부 client 코드에서 사용할때는 내부 private class 에 
            접근할 방법이 없기 때문에 구현 로직의 캡슐화가 가능하며 
            외부 client 코드에서의 내부 클래스의 기능을 활용 하려 할 때는 
            내부 private class 를 싸고 있는 class 의 제공된 method 를 통해서만(api) 
            내부 private class 의 기능을 사용할 수 밖에 없기 때문에 
            내부클래스(로직을 구현하는 클래스)의 클래스 설계자의 의도에 맞지 않는 오사용을 막을 수 있다.

    4. 상위 클래스에서 재정의된 equals method 가 상속받은 하위 클래스에서 사용하기 적합하면 하위 클래스에서 다시 재정의 할 필요는 없다.
            대부분의 Set의 하위 클래스는 AbstractSet 의 equals를 그대로 사용
            (List, Map 도 동일 AbstractList, AbstractMap)

    5. 싱글턴 객체일때 - 싱글턴 클래스에서 생성할 수 있는 객체는 최대 한개 이기 때문에 Object 의 equals 로서 충분히 비교가 가능하다. 

이와같이 5가지 경우에는 재정의가 불필요하다.
5가지의 경우는 다음과 같이 3가지 경우로 요약이 가능하다.
  1. 동일성 비교가 필요 없거나 동일성 비교가 무의미한 클래스
  2. 상위 클래스의 equals 가 하위 클래스의 동일성 비교에 충분할 경우
  3. 싱글턴 객체를 생성하는 클래스
는 equals 의 override(재정의)가 불필요하다.

불필요한 경우를 뒤집어 생각한다면 필요한 경우를 유추할 수 있다.
즉 상반된 경우엔 Object class 의 equals 를 재정의를 해야만 한다.
보통 다음과 같은 경우에 equals method 를 override 한다.

    1. 논리적 동일성 검사가 필요한 경우
        객체의 동일성이 아닌 논리적인 동일성이 필요한 객체일때
    2. 상위클래스에서 재정의한 equals method 가 하위클래스에서 사용하기 어려울때

이런 조건에 부합하는 객체는 보통 값 객체이다.
Integer 나 Double 객체의 동일성은 객체 자체가 동일한지 여부가 중요한게 아니라 
값이 동일한지가 중요하기 때문이다. 즉 논리적 동일성 검사가 필요한 경우이다.
그렇지만 값 객체라 하더라도 singleton 객체이면 역시 equals override 는 필요없다.
무조건 단일 객체만 생성되는 Enum class(객체동일성이 곧 논리적 동일성) 가 그런 경우이다.

이렇게 논리적 동일성 검사가 재대로 정의된 값 객체는 Set 자료형에 인자로 전달 될 수 있는등 일반 규약에 따르는 클래스에서 사용할 수 있다. (만약 Set 객체에 전달된 인자 객체가 일반 규약을 따르지 않는다면 결과를 예측하기 어렵다.)

자 그럼 일반 규약에는 어떤것이 있는지 알아보자.

equals method 일반규약은 다음과 같이 5개의 항목으로 정의된다.
  1. 반사성 (자신과의 동치성 비교시 equal true)
  2. 대칭성 (a.equals(b) == true 이면 b.equals(a) == true)
  3. 추이성 (a.equals(b) == true, b.equals(c) == true 이면 a.equals(c) = true)
  4. 일관성 (equals 비교 횟수와 시점에 상관없이 항상 같은 결과)
  5. null 에 대한 비 동치성 (null 과의 equals 비교시 항상 false 반환 ex : a.equals(null) == false)

각 항목을 자세히 살펴보자.
equals method 의 일반규약 상세
1. 반사성 
    고의적으로도 깨트리기 어려운 규칙
    override 한 equals 메소드의 로직이 항상 false 를 반환하도록 하면 가능
2. 대칭성 
    a==b 이면 b==a
    비교하는 두 객체의 type 이 다를 경우 (equals method 의 parameter type 은 Object 이기 때문게 충분히 가능한 상황)
    a 객체에서는 b의 값을 casting 해서 값을 비교하는 로직이 있지만
    b 의 equals 로직에는 없을 때 false 가 발생 할 수 있다.
3. 추이성 
    상속구조의 클래스들 equals 비교에서 발생할 가능성이 있다.
    a 를 상속한 b타입의 b1객체 가 a와의 동치성을 비교하기위해  
    a의 속성만(b의 upcasting) 으로 비교해서 equals 가 true 가 되었고
    b타입의 b2 객체가 b1와 의 동치성을 비교할 때는 같은 b타입이기 때문에  b타입에만 있는 멤버변수까지 동치성 비교에
    사용하였다면 
    결국 a = b1 과 같고 a = b2 와 같을순 있지만 b1 != b2 가 될 수도 있다.
    일부 견해에서는 이러한 예외상황을 막기위해 비교하는 파라미터 타입을 좀더 엄격히 관리해 
    (interface of 대신 getClass()) 같은 타입(같은 클래스)이 아니면 false 를 리턴하도록 해야 한다는 주장도 있다.
    하지만 그와 같은 주장은 리스코프 대체 원칙에 위배되는 결과를 초래하기 때문에 받아들이기 힘든 견해이다.
    상위 자료형의 어떠한 메서드에서 상위 자료형을 계승한 자료형의 객체를 담는 컬렉션 변수가 있을때를 가정할때
    상위 자료형의 equals 가 getClass 로 파라미터 타입을 제한한다면
    컬렉션 변수의 contains 메소드는 잘 작동하지 못할 것이다.

    그렇다면 계승구조의 클래스들의 추이성을 보장하는 방법은 무엇일까
    불행하게도 객체 생성가능 클래스를 계승하여 새로운 값 컴포넌트를 추가하면서 추이성 규약을 어기지 않을 방법은 없다.
    effective java 에서 제시하는 대안은 클래스의 계승구조를 버리고 구성(composition) 하는 방식을 제안하고 있다.

    참고로 계승 구조에서 발생하는 추이성 문제에 해당하지 않는 상황은 부모클래스(계승모체)를 abstract 로 선언하는 경우이다.
    abstract 로 선언된 class 는 객체를 직접 만들수 없으므로 추이성 문제가 발생하지 않는다.
    

    ※ 리스코프 대체 원칙 (Liskov substitution principle)
    어떤 자료형의 중요한 속성은 하위 자료형에도 유지되어서 그 자료형을 위한 메서드는 하위 자료형에도 잘 동작해야 한다는 원칙

4. 일관성 
    일단 같다고 판정된 객체는 추후 변경되지 않는 한 계속 같아야 한다.
    즉 변경불가능 객체의 동치관계는 변하지 않는다.

    또한 변경 가능 여부와 상관없이 신뢰성이 보장되지 않는 자원들을 비교하는 equals를 구현하는 것은 삼가 해야 한다.
    즉 equals 메서드는 메모리에 존재하는 객체들만 사용해서 결정적 계산을 수행하도록 구현해야 한다.
    (java.net.URL 클래스와 같이 네트워크 상태에 따라 같은 결과가 나온다는 보장이 없는 클래스에는 equals 구현을 삼가)

5. null 에 대한 비 동치성
    equals 의 파라미터가 null 일 경우 false 를 반환해야 한다는 규약이다.
    이 규약의 예외상황은 NullPointerException 이다. 즉 false 을 반환하지 못하고
    예외를 throw 하기 때문에 일반 규약을 충족하지 못한다.
    단 equals 메소드에서 interface of 로 형 검사를 한다면 instance of 특성상 첫번째 피연사자가 null 이면 
    무조건 false 를 반환하기때문에 형검사 로직을 포함하면 null 에 대한 비 동치성 규약을 지킬수 있다.


이상으로 일반규약의 설명은 마무리하고
다음은 이런 일반 규약을 토대로 equals 를 재정의 할때 권장하는 지침에 대해 살펴보겠다.

equals 재정의 지침 

1. == 연사자를 사용하여 equals 의 인자가 자기 자신인지 검사 (동치성에 의거함)
    - 성능 최적화, 객체에 포함된 값 비교 없이 동치성 결과 반환
2. instance of 자료형 검사 (null 에 대한 비 동치성)
    - null 비 동치성에도 훌륭한 지침이지만 
    계승된 클래스 또는 같은 class가 아니라면 굳이 비교할 필요가 없기 때문에 성능상에 이점이 있다.
3. equals 의 인자를 정확한 자료형으로 변환
    - instance of 자료형 검사로 인해 형 변환은 반드시 성공
4. 중요 필드 각각이 인자로 주어진 객체의 해당 필드와 일치하는지 검사
    - 기보자료형은 == 로 비교
    - float 와 double 은 compare 메소드를 사용해 비교 (Nan, -0.0f 같은 상수로 인해)
    - 배열은 모든 원소를 비교 - Arryas.equals 활용 및 참고
    - null 허용 필드 비교시 NullPointerException 발생 회피 
       (field == null ? o.field == null : field.equals(o.field))
    - 비교필드와 같을 때가 많을때
        (field == o.field || (field!=null && field.equals(o.field))) - 성능상의 이점
        equals 메서드 성능은 필드 비교 순서에도 영향을 받을 수 있다.
    - 객체의 요약정보를 담는 필드가 객체의 동일성을 보장할때 요약정보를 비교하는 로직으로 비교 비용을 줄일 수 있다.
5. 대칭성, 추이성, 일관성 검토 및 테스트
6. hashCode 재정의


sample code
@Overridepublic boolean equals(Object o) {
    // 동치성    if(o == this)
        return true;
    // 형검사    if(!(o instanceof Equals))
        return false;
    // 형변환    Equals l = (Equals) o;
    // 중요필드 비교    return l.d == d;}



    

댓글 없음:

댓글 쓰기

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor &...