#003 모든 객체에 공통적인 메소드

Object는 실체 클래스(concrete class)지만 원래 상속을 목적으로 설계되었다. 이 장에서는 final이 아닌 Object의 메소드(equals, hashcode, toString, clone, finalize)들을 언제 어떻게 오버라이드하는지 알려준다.

실체 클래스란? 추상 클래스와 상반되는 것으로서, 자신의 인스턴스를 생성할 수 있는 일반적인 클래스를 말한다.

8. equals 메소드를 오버라이딩 할 때는 보편적 계약을 따르자
인스턴스의 동일여부를 판정하는 euqals 메소드의 오버라이딩은 간단한 것 같지만, 잘못 구현하는 경우가 많아서 참담한 결과를 초래할 수 있다.
그런 문제를 피하는 제일 쉬운 방법은 equals 메소드를 오버라이드하지 않고 상속받은 그대로 사용하는 것이다.
(여기서 equals 메소드는 Object.equals이거나, 다른 수퍼 클래스에서 Object의 equals를 이미 오버라이딩 한 것을 말한다.)

equals 메소드를 오버라이드하지 않고 상속받은 그대로 사용하는 경우

1. 클래스의 각 인스턴스가 본래부터 유일한 경우
인스턴스가 갖는 값보다는 활동하는 개체임을 나타내는 것이 더 중요한 Thread와 같은 클래스가 여기에 해당된다.
그런 클래스들은 인스턴스가 갖는 값의 논리적인 비교는 의미가 없으며, 객체 참조가 같으면 동일한 것임을 알 수 있으므로
Object의 equals(== 연산자 사용)를 그냥 사용하면 된다.
2. 두 인스턴스가 논리적으로 같은지 검사하지 않아도 되는 클래스의 경우 클래스를 설계할 때, 인스턴스가 갖는 값을 논리적으로 비교하는 기능이 필요하지 않을 경우, Object로부터 상속받은 equals를 그냥 사용한다.
3. 수퍼 클래스에서 equals 메소드를 이미 오버라이딩 했고, 그 메소드를 그대로 사용해도 좋은 경우
예를 들어, Set 인터페이스를 구현하는 대부분의 클래스는 AbstractSet에 구현된 equals를 상속받아 사용한다.
List의 경우는 AbstractList, Map의 경우는 AbstractMap에서 equals를 상속받아 사용한다.
// AbstractSet.java
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Set))
        return false;
    Collection c = (Collection) o;
    if (c.size() != size())
        return false;
    try {
        return containsAll(c);
    } catch (ClassCastException unused)   {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
}
4. private이나 패키지 전용(package-private) 클래스라서 이 클래스의 equals 메소드가 절대 호출되지 않아야 할 경우
우연히 호출될 수 있는 상황에서는 다음과 같이 오버라이딩해서 호출되지 않도록 한다.
(패키지 전용은 클래스의 접근 지시자를 지정하지 않은 default 접근을 말한다.)
@Override
public boolean equals(Object o) {
     throw new AssertionError(); // 메소드가 절대 호출되지 않는다
}

equals 메소드를 오버라이드 해야하는 경우

객체 참조만으로 인스턴스 동일 여부를 판단하는 것이 아니라, 인스턴스가 갖는 값을 비교하여 논리적으로 같은지 판단할 필요가 있는 클래스로서, 자신의 슈퍼 클래스에서 equals 메소드를 오버라이드 하지 않았을 때이다. 일반적으로 값(value) 클래스가 여기에 해당된다.

equals 메소드를 오버라이드 할 때 따라야하는 보편적 계약

1. 재귀적이다(Reflexive)
null이 아닌 모든 참조값 x에 대해, x.equals(x) 반드시 true를 반환해야 한다.
2. 대칭적이다(Symmetric)
null이 아닌 모든 참조값 x, y에 대해, y.equals(x)가 true를 반환한다면 x.equals(y)도 반드시 true를 반환해야 한다.
3. 이행적이다(Transitive)
null이 아닌 모든 참조값 x, y, z에 대해, x.equals(y)가 true를 반환하고 y.equals(z)가 true를 반환한다면
x.equals(z)도 반드시 true를 반환해야 한다.
4. 일관적이다(Consistent)
null이 아닌 모든 참조값 x, y에 대해, equals 메소드에서 객체 비교 시 사용하는 정보가 변경되지 않는다면,
x.equals(y)를 여러 번 호출하더라도 일관성 있게 true 또는 false를 반환해야 한다.
5. null이 아닌 모든 참조값 x에 대해, x.equals(null)은 반드시 false를 반환해야 한다.

양질의 equals 메소드를 만드는 방법

1. 객체의 값을 비교할 필요가 없고 참조만으로 같은 객체인지 비교가 가능하다면 == 연산자를 사용하자.
2. instanceof 연산자를 사용해서 전달된 인자가 올바른 타입인지 확인하자.
대개의 경우 올바른 타입이란 호출된 equals 메소드가 정의된 클래스를 말하지만, 그 클래스가 구현하는 인터페이스도 올바른 타입이 될 수 있다.
3. 인자 타입을 올바른 타입으로 변환한다.

@Override 
public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    Point p = (Point)o; // o를 Point로 타입 변환한다.
    return p.x == x && p.y == y;
}
4. 클래스 중요한(꼭 비교해야하는) 필드에 대해서는 모두 같은지 빠뜨리지 말고 비교한다.
float 필드는 Float.compare, double 필드는 Double.compare 메소드를 사용한다.
(float의 경우 Float.NAN, -0.0f, double의 경우 Double.NAN, -0.0 값이 나올 수 있어서, 이런 값들의 올바른 비교를 위해 특별한 처리가 필요하다)
equals 메소드이 성능은 비교하는 필드의 순서에 영향을 받을 수 있다. 다를 가능성이 많거나 비교 비용이 적게드는 필드부터 먼저 비교해야 한다.
5. equals 메소드를 작성한 후에는 대칭적이며 이행적이고 일관성이 있는지 확인한다.

equals 메소드를 만들 때 유의할 사항

1. equals 메소드를 오버라이드 할 때는 hashcde 메소드도 항상 오버라이드 한다.(항목 9)
2. 너무 똑똑한 척 하지 마라. 너무 지나치게 동일 여부를 비교하려 하면 문제가 생기기 쉽다.
3. equals 메소드의 인자 타입을 Object 대신 다른 타입으로 바꾸지 말자.

public boolean equals(MyClass o) {
    ...
}

위의 코드는 메소드 인자가 Object 타입인 Object.equals를 오버라이드 한 것이 아니라, 오버로드 하고 있다.
오버로드 한다고 해서 문제가 발생하는 것은 아니지만 공연히 코드만 복잡하게 만든다.
@Override 주석을 사용하면 컴파일 시 에러가 발생하므로, 이런 실수를 방지할 수 있다.

9 equals 메소드를 오버라이드 할 때는 hashCode 메소드도 항상 같이 오버라이드 하자
equals 메소드를 오버라이드 하는 모든 클래스는 반드시 hashCode 메소드도 오버라이드 해야한다. 그렇지 않으면 hash 기반의 컬렉션들(HashMap, HashSet, HashTable)와 우리가 구현한 클래스를 같이 사용할 때 우리 클래스가 올바르게 동작하지 않을 것이다.

hashCode 메소드를 오버라이드 할 때 따라야하는 보편적 계약

1. 애플리케이션 실행 중에 같은 객체에 대해 한번 이상 호출되더라도 hashCode 메소드는 같은 정수를 일관성 있게 반환해야 한다.
2. equals 메소드 호출 결과 두 객체가 동일하다면, 두 객체 각각의 hashCode 메소드를 호출했을 때 반환되는 정수 값도 같아야 한다.
3. equals 메소드 호출 결과 두 객체가 다르다고 해서 두 객체 각각의 hashCode 메소드에서 다른 정수값이 나올 필요는 없다.

그러나 서로 다른 객체에 대해 hashCode 메소드에서 서로 다른 정수 값을 반환하면, 이 메소드를 사용하는 해시 컬렉션들의 성능을 향상시킬 수 있다.

좋은 hashCode 메소드 구현 방법

좋은 해시 메소드는 동일하지 않은 객체들에 대해 서로 다른 해시코드를 만든다. 서로 다른 인스턴스에 대해, 해시 메소드가 모든 가능한 해시 값을 고르게 분산시켜주어야 한다.
그 간단한 방법은 아래와 같다.

1. 예를 들어, 17과 같이 0이 아닌 상수 값을 result라는 int 변수에 저장한다.
2. 객체의 각 주요 필드(equals 메소드에서 비교할 때 사용된 필드) f에 대해 다음을 수행한다.
a. 각 필드에 대한 int 타입의 해시코드 c를 다음과 같이 산출한다.
1. f가 boolean 타입 → f ? 1 : 0
2. byte, char, short, int → (int) f
3. long → (int) (f^(f>>>32))
4. float → Float.floatToIntBits(f)
5. double → Double.doubleToLongBits(f)를 실행한 후, 반환된 long 타입의 값으로 a.3을 실행한다.
6. 객체 참조
   현재(equals 메소드가 호출된) 객체의 equals 메소드에서 그 필드를 비교하기 위해 f가 참조하는 객체의 equals 메소드를 재귀적으로 호출한다.
   그러면 그 객체의 필드에 대해 hashCode 메소드도 재귀적으로 자동 호출된다.
   더 복잡한 비교 가 필요하다면 필드의 “표준형식”을 만들어 처리하고 그 표준 형식에 대해 hashCode 메소드를 호출한다.
   만일 f의 값이 null이면 0을 반환한다.
7. 배열
   배열의 각 요소를 별개의 필드처럼 처리한다. 즉, 위의 규칙들을 적용하여 처리해야 할 요소 각각의 해시 코드 값을 산출한다.
   배열 필드의 모든 요소를 처리해야 한다면 자바 1.5 버전에 추가되어 오버로딩 된 Arrays.hashCode 메소드들 중 하나를 사용한다.
b. 앞의 a 단계에서 구한 해시 코드 c를 result에 합계한다.
result = 31 * result + c; // 31은 소수이기 때문에 선택한 것으로, 비트 이동과 뺄셈으로 곱셈을 대체할 수 있어서 성능을 향상 시킬 수 있다.
3. result를 반환한다.
4. 동일한 인스턴스에 대해 같은 해시코드 값을 갖는지 테스트한다.

예제코드.

 
import java.util.*;
 
public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;
 
    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode  = (short) areaCode;
        this.prefix  = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }
 
    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max)
           throw new IllegalArgumentException(name +": " + arg);
    }
 
    @Override 
    public boolean equals(Object o) {
        if (o == this)
           return true;
        if (!(o instanceof PhoneNumber))
           return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber
            && pn.prefix  == prefix
            && pn.areaCode  == areaCode;
    }
 
    @Override 
    public int hashCode() {
        int result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        return result;
    }
 
    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
    }
}

10 toString 메소드는 항상 오버라이드 하자
java.lang.Object 클래스는 toString 메소드를 구현하고 있으나, 반환되는 값은 사용자가 원하는 형태가 아니다

예. PhoneNumber@163b7: 클래스명@16진수 해쉬코드

equals와 hashCode 메소드 계약을 준수하는 만큼 중요하지는 않더라도, toString 메소드를 잘 구현하면 클래스를 더욱 편하게 사용할 수 있다.

toString 메소드를 오버라이드 할 때 따라야하는 보편적 계약

1. 객체의 모든 중요한 정보를 반환해야 한다.
2. 표현 형식의 규정 여부와는 무관하게, 그 의도를 명쾌하게 문서화 해야한다.
    주석에 자세한 내용을 설명한다.
**
 * Returns the string representation of this phone number.
 * The string consists of fourteen characters whose format
 * is "(XXX) YYY-ZZZZ", where XXX is the area code, YYY is
 * the prefix, and ZZZZ is the line number.  (Each of the
 * capital letters represents a single decimal digit.)
 *
 * If any of the three parts of this phone number is too small
 * to fill up its field, the field is padded with leading zeros.
 * For example, if the value of the line number is 123, the last
 * four characters of the string representation will be "0123".
 *
 * Note that there is a single space separating the closing
 * parenthesis after the area code from the first digit of the
 * prefix.
 */
@Override public String toString() {
    return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
}
3. 표현 형식의 규정 여부와는 무관하게, toString 메소드의 반환 값에 포함되는 모든 정보를 프로그램적으로 접근하는 방법을 제공하자.
    예를 들어, PhoneNumber는 반환 문자열로부터 지역번호, 국번호, 선번호를 알려주는 메소드를 갖고 있어야 한다.

11 clone 메소드는 신중하게 오버라이드 하자

Cloneable은 단점이 많으므로 배열 복제 정도로 사용하면 모를까, 일부 숙련된 프로그래머들은 clone 메서드를 오버라이드 하지않고 호출하지도 않는다.
Cloneable과 Clone을 사용하는 것보다 객체를 복제하는 좋은 방법은 복제 생성자 또는 복제 팩토리 메소드를 제공하는 것이다.

예제코드.

public final class Galaxy {
  // PRIVATE /////
  private double fMass;
  private final String fName;
 
  public Galaxy (double aMass, String aName) {
     fMass = aMass;
     fName = aName;
  }
 
  /**
   * Copy constructor.
   */
  public Galaxy(Galaxy aGalaxy) {
    this(aGalaxy.getMass(), aGalaxy.getName());
    //no defensive copies are created here, since 
    //there are no mutable object fields (String is immutable)
  }
 
  /**
   * Alternative style for a copy constructor, using a static newInstance
   * method. 
   */
  public static Galaxy newInstance(Galaxy aGalaxy) {
    return new Galaxy(aGalaxy.getMass(), aGalaxy.getName());
  }
 
  public double getMass() {
    return fMass;
  }
 
  /**
   * This is the only method which changes the state of a Galaxy
   * object. If this method were removed, then a copy constructor
   * would not be provided either, since immutable objects do not
   * need a copy constructor.
   */
  public void setMass( double aMass ){
    fMass = aMass;
  }
 
  public String getName() {
    return fName;
  }
 
  /**
   * Test harness.
   */
  public static void main (String... aArguments){
    Galaxy m101 = new Galaxy(15.0, "M101");
 
    // 복제 생성자
    Galaxy m101CopyOne = new Galaxy(m101);
    m101CopyOne.setMass(25.0);
    System.out.println("M101 mass: " + m101.getMass());
    System.out.println("M101Copy mass: " + m101CopyOne.getMass());
 
    // 복제 팩토리 메소드
    Galaxy m101CopyTwo = Galaxy.newInstance(m101);
    m101CopyTwo.setMass(35.0);
    System.out.println("M101 mass: " + m101.getMass());
    System.out.println("M101CopyTwo mass: " + m101CopyTwo.getMass());
  }
}
 
---------- execute ----------
M101 mass    : 15.0
M101Copy mass: 25.0
M101 mass       : 15.0
M101CopyTwo mass: 35.0

코드출처: http://www.javapractices.com/topic/TopicAction.do?Id=12

복제 생성자나 복제 팩토리 메소드의 장점

– 자바 언어 영역을 벗어난 형태의 위험한 객체 생성 메커니즘에 의존하지 않는다.
– 자신이 속한 클래스에서 구현하는 인터페이스를 인자의 타입으로 가질 수 있다.

번외 Marker Interface (책에서는 Mix-in Interface)
오늘 시간이 촉박해서 마지막 Cloneable 인터페이스를 대충 넘겨서 안타까운 느낌이 있어서 좀 검색을 해보고 정리해봤습니다.

먼저 Cloneable 은 아무런 내용이 없는 Marker Interface 라고 합니다. 그런데 이게 뭘까요…
Marker Interface??

단순히 빈 인터페이스이지만 이 인터페이스를 구현한 클래스의 인스턴스를 사용하는 측은 인터페이스의 구현 여부를 보고 특별한 처리를 할 수 있도록 합니다. 이렇게 말하면 좀 어려운데, 쉽게 제가 이해한 내용을 말씀드리면 Grouping 한다고 봐도 괜찮을 것 같습니다.

그래서 Marker Interface를 Tag Interface 라고 부르기도 한다네요.

자바 어드밴스팀 팀장은 스터디원을 구별할 때 스터디원이 JavaAdvanceTeam 을 구현하고 있다면 그것은 자신의 팀원이라고 생각하고 처리할 수 있도록 구현할 수 있겠지요. 만일 저 인터페이스를 구현하지 않은 스터디원이 들어온다면 외부인이거나, 다른 스터디를 하는 팀원으로 판단하고 Kick 할 수 있겠습니다.

의사 코드로 표현해보면

void doManage(StudiablePeople people) throws NotStudyMatchException {
    if(people instanceof JavaAdvanceTeam) {
        myTeam.add(people);
    }
    throw new NotStudyMatchException();
}

정도가 되겠네요. 코드에서는 그냥 적절하게 아닐 경우 Kick 보다 예외를 던졌습니다.

실제 JavaAdvanceTeam 인터페이스의 구조는 다음과 같이

interface JavaAdvance {
    // 비어있다.
}

아무런 기능이 없어도 충분합니다. 그냥 구별(Marking) 만 하면 되니까요.

class 이항희 implements StudiablePeople, JavaAdvanceTeam { ... } // 우리 팀 스터디원
class 유미령 implements StudiablePeople, JavaAdvanceTeam { ... } // 우리 팀 스터디원
class 최치환 implements StudiablePeople, AndroidTeam { ... }     // 남의 팀 스터디원

뭐 이런식의 구별이 가능해지지는 거지요.

실제로 이러한 Marker Interface 는 Java API 에서도 볼 수 있는데,

Swing 프로그램시 사용되는 EventListener
객체 직렬화 시 사용되는 Serializerble
객체 복제 시 사용하는 Cloneable
입니다.

아무런 기능이 없지만 이벤트 발생 대상이 된다거나 직렬화 대상이 된다거나…복제 함수를 사용할 수 있게 해준다던가… 사용 측(Client성 객체) 에게 일종의 메타 정보를 주는 역할이 될 수 있죠. 위에도 말했듯이 일종의 그룹이 되었다라고도 표현할 수도 있죠…

이런 개념에서 clone을 접근해보면… clone은 기본적으로 Object 에 구현이 되어 있지만, 기본적으로는 protected 라 Object의 clone을 구현하여 접근제어를 변경하지 않으면 호출이 안됩니다.

그리고 구현을 했더라도 그 클래스가 Cloneable Marker Interface 를 구현하고 있지 않다면 CloneNotSupportedException 이 발생한다고 합니다. 런타임이 Marker Interface 를 보고 판단하는 거죠. 복제가 가능한지 가능하지 않은지…

creative by javacafe

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

%d 블로거가 이것을 좋아합니다: