2011.04.17 20:45
  compareTo메소드는 Comparable 인터페이스에서 유일하게 존재하는 메소드이다. 비록 Object의 메소드는 아니지만, equals와 유사한 특성을 갖는다. 차이점이라면 두 객체가 동일한지를 비교하는 것과 더불어 순서까지 비교할 수 있으며, 제네릭 타입을 지원한다. 특정 클래스에서 Comparable 인터페이스를 구현한다는 것은 자신의 인스턴스들이 자연율(律)(Natural Order)을 따른 다는 것을 나타낸다. 

 Comparable 인터페이스를 구현하는 객체들이 저장된 배열의 정렬은 다음과 같이 쉽게 할 수 있다.

 Arrays.sort(a); 

  Comparable 인터페이스를 구현하는 객체들이 저장된 컬렉션을 검색하고, 최대 값을 찾고, 자동으로 정렬된 상태로 유지하는 것도 이와 유사하게 쉽다. 예를 들어, String 클래스에서 구현하는 Comparable에 의존하는 다음 프로그램에서는 명령행에서 준 인자들을 알파벳 순으로 출력하되 같은 값은 제외한다.

 
 우리 클래스에서 Comparable 인터페이스를 구현하면, 이  인터페이스에 의존하는 자바의 수많은 알고리즘 및 컬렉션 클래스들과 상호 연동이 가능하다. 실제로 자바 라이브러리의 모든 값 클래스들은 Comparable 인터페이스를 구현한다. 

 순서 판단을 위해 현재 객체(compareTo 메소드가 호출된)와 지정 객체(compareTo 메소드의 인자로 전달된)를 비교한다. 현재 객체의 값이 지정 객체보다 작으면 음수 정수 값을, 같으면 0을, 크면 양의 정수 값을 반환한다.  this(자신) - argument(비교대상)의 값을 반환한다고 생각하면 쉬울것이다. 만약 비교 할 수 없는 타입이라면 ClassCastException 예외가 발생된다.



 compareTo 메소드도 보편적 계약을 갖고 있는데 equals 메소드와 유사한 모습을 보여준다. 
 다음 설명에서 sgn(표현식)은 signum 수학함수를 나타내며, 표현식의 값이 음수면 -1을, 0이면 0, 양수면 1을 반환한다.

- 모든 x와 y에 대하여, sgn(x, compareTo(y)) == -sgn(y.compareTo(x))가 되도록 해야한다.(이것은 y.compareTo(x)가 예외를 발생시킬 때만 x.compareTo(y)가 예외를 발생시켜야 한다는 의미를 내포한다.)

- 이행적인 관계가 성립되어야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0) 이면 x.compareTo(z) > 0 이어야 한다.

- 끝으로, x.compareTo(y) == 0 이라면, 모든 z에 대해 sgn(x.compareTo(z)) == sgn(y.compareTo(z))이 되어야 한다. 

- 반드시 요구되는 것은 아니지만, (x.compareTo(y) == 0) == (x.equals(y))가 되도록 하는 것이 좋다. 그리고 Comparable 인터페이스를 구현하면서 이 조항을 지키지 않는 클래스에서는 API문서에 나타나도록 그 사실을 분명하게 표시해야 할 것이다. 즉, 다음과 같이 하기를 권한다. "주의 : 이 클래스는 equals 메소드와 다르게 자연률 순서를 지원한다."




 compareTo 메소드의 작성 방법은 equals 메소드와 유사하지만 몇가지 중요한 차이점이 있다.
 compareTo 메소드가 정의되어있는 Comparable 인터페이스가 매개변수화 타입을 가지므로, compareTo 메소드의 인자로 전달되는 비교 객체의 타입은 컴파일 시점에서 미리 결정된다. 따라서 compareTo 메소드 내부에서 비교 객체의 타입을 확인하거나 타입 변환을 할 필요 없다. 만일 인자의 타입이 잘못되면 코드가 컴파일 되지 않기 때문이다. 만일 인자가 null이면 가능한 빨리 NullPointerException 예외를 발생시켜야 한다.


 Comparable 인터페이스를 구현하지 않고 있거나, 자연율 순서와 다른 순서 매김을 사용할 필요가 잇다면 Comparator 인터페이스를 대신 사용할 수 있다. 이때는 자신의 것을 작성하거나, 또는 String 클래스의 Comparator인 CASE_INSENSITIVE_ORDER.compare 메소드, Double.compare, Float.compare 와 같이 이미 만들어 진것을 사용하자.

 특히 부동 소수점 값을 비교할 때 관계 연산자를 사용하면 compareTo 메소드의 보편적 계약을 따르지 않기 때문에 위의 메소드를 사용하는 것이 필요하다.

 비교할 필드가 많을 경우 우선시 되는 필드의 순서대로 비교하여 동일할경우 다음 중요한 필드의 값을 비교하는 식의 방법을 쓴다. 

 비교할 때 조건문을 쓰고 임의의 정수를 return 하는 방법을 사용할 수 도 있지만, compareTo는 부호를 중요시하므로, 위에서 잠깐 언급한것 처럼 두 필드의 차를 return하는 방법을 쓴다면 더 성능이 좋은 메소드를 구현할 수 있다. 
 이 방법은 빠르지만, 두 수의 차가 Integer.MAX_VALUE(2^31 - 1) 보다 작거나 같은 범위인지를 고려해야 된다는 점을 항상 염두에 두자. 이런 실수는 대다수의 정수값에 대해서 잘 동작하기 때문에 찾기 힘들기 때문에 결함을 찾기가 쉽지 않다.

신고
Posted by JAVA_HOME

댓글을 달아 주세요

2011.04.17 16:39
 Cloneable 인터페이스는 복제를 허용하는 객체라는 것을 알리는 목적으로 사용하는 믹스인 인터페이스(mixin interface)이다.

 하지만 그런 목적에 부합하지 못하고 있다. 자신이 clone 메소드를 갖고 있지 않으며, Object 클래스의 protected인 clone 
메소드를 사용할지 여부를 결정한다.

 그리고 Cloneable 인터페이스를 구현한 효과를 보려면, 그 클래스와 그 클래스의 모든 수퍼 클래스들은 문서화된 규약을 준수해야 하는데, 그 규약이란 것이 꽤 복잡하고 강제성을 띄지 않으면서 분량도 많지 않다. 그리고 그 규약에 기인하여 만들어진 메커니즘은 자바 언어 영역을 벗어난다. 생성자를 호출하지 않고 객체가 생성되어 복제되기 때문이다.



 앞에서 살펴본 메소드들과 같이 clone에도 보편적 계약이 있는데 그 내용이 매우 빈약하다. 

- x.clone () != x 는 true를 반환한다.
- x.clone().getClass() == x.getClass() 는 true를 반환한다.
- x.clone().equals(x) 는 일반적으로 true가 될 것이지만 필수요건은 아니다.


 일반적으로 객체를 복제할 때는 그 객체의 클래스로부터 새로운 인스턴스를 생성하게 되지만, 그 때 내부 데이터 구조의 복사도 필요할 수 있다. 복제를 할 떄 어떤 생성자도 호출되지 않는다. 

 위의 계약은 많은 문제점을 내포하고 있는데 자세한 내용은 책을 참고하기 바란다.


 눈여겨 볼 점 몇가지를 소개하자면,
 1. 2번째 계약을 지키기 위해 final이 아닌 수퍼 클래스의 clone 메소드를 오버라이드 할 경우, 서브 클래스의 clone에서는 반드시 super.clone을 호출하여 얻은 객체를 반환해야 한다.

더보기


 2. Array와 같이 가변 객체를 참조하는 필드를 가지고 있을 때는 참조값만 복사하는 Shallow copy가 아니라 인스턴스 자체를 복제하는 Deep Copy를 고려하자.

더보기


 3. 복잡한 객체를 복제하는 마지막 방법은, super.clone을 호출하여 반환된 복제 객체의 모든 필드를 초기화 한 후 고주순의 메소드를 호출하여 원본 객체와 같은 상태로 재생하는 것이다.

더보기


4. 생성자 처럼, clone 메소드에서는 복제 중인 객체의 final이 아닌(달리 말해, 오버라이드 가능한) 어떤 메소드도 호출 하면 안된다.

더보기


5. Object의 서브클래스에서 clone 메소드를 오버라이딩 할 때는(특히 public으로) CloneNotSupportedException과 같은 checked외를 발생시키지 않는게 메소드를 사용하기 쉽다. 하지만 상속을 목적으로 설계된 클래스가 clone을 오버라이드 하고 있다면 clone을 protected로 지정하고 CloneNotSupportedException 예외를 발생시킨다고 메소드를 선언해야 하며, 그 클래스에서는 implements Cloneable을 선언하지 말아야한다. 그렇게 함으로써 그 클래스의 서브 클래스에서는 마치 Object로부터 직접 상속 받는것처럼 Cloneable 인터페이스의 implements 여부를 선택할 수 잇기 때문이다.

6. implements Cloneable 을 선언한 클래스가 스레드에서 안전하게 사용될 수 있게 하고자 한다면, 그 클래스의 clone 메소드는 다른 메소드처럼 반드시 올바르게 동기화 되어야 한다. Object의 clone 메소드는 동기화를 지원하지 않기 때문이다.



하지만 위와 같이 복잡하게 clone 메소드를 오버라이드 해야 하는 것일까? Cloneable을 implements하는 클래스의 서브 클래스에서는 선택의 여지가 없이 잘 동작하는 clone메소드를 구현해야하지만, 그렇지 않다면 복제 생성자나 복제 팩토리 메소드를 제공한다면(자기 자신을 타입으로 하는) 자바 언어 영역을 벗어난 형태의 위험스러운 객체 생성 메커니즘에 의존하는 clone 메소드에 비해 훨씬 강력하게 구현을 할 수 있을 것이다.

 Cloneable의 단점이 많기 때문에, 배열 복제 정도로 사용한다면 모를까, 일부 숙련 프로그래머들은 clone 메소드를 전혀 오버라이드 하지 않고 호출 하지도 않는다.

신고
Posted by JAVA_HOME

댓글을 달아 주세요

2011.04.15 21:02

 자바 API문서를 보면 toString 메소드의 보편적 계약에 다음과 같이 나와있다.

 반환되는 문자열은 '간결해야 하지만 사람이 읽기 쉬운 형태의 정보 표현' 이어야 한다.
모든 서브 클래스 들은 이 메소드를 오버라이드 할 것을 권한다.


 비록 앞에서 다뤄본 equals와 hashCode 계약을 준수하는 것 만큼 중요하지는 않지만 toString 메소드를 잘 구현하면 우리 클래스를 더욱 편하게 사용할 수 있다. 
 따라서 객체가 너무 크거나, 문자열로 변환하기 어려운 상태가 아니라면 객체의 모든 중요한 정보를 반환하면 좋다. 
 static 팩토리 메소드나 생성자에서 toString에 들어갈 정보를 인자로 받아 필드를 초기화하는 것도 좋은 방법이 되겠다.

 toString 메소드를 구현할 때 고려해야될 중요한 점은 반환 값을 다른 곳에서 사용될 수 있다는 점이다.
 따라서 반환값을 형식화 시킨다면 다른 곳에서 유용하게 사용할 수 있을 것이다. 하지만 그만큼 신경을 써야되고 차기버전에서 수정이 필요하다고 생각이 되어도 유연성을 잃게 되어 함부로 고칠 수 없게 된다.

 따라서 두가지의 선택이 가능하게 된다. 반환 값의 형식을 API에 상세하게 규정할 것인가, 규정하지 않을 것인가.

 하지만 표현 형식의 규정 여부와는 무관하게 그 의도를 명쾌하게 문서화해야한다. 그리고 toString 값에 포함되는 모든정보를 프로그램적으로 접근하는 방법을 제공하자.

 이렇게 된다면 프로그래머들은 형식에 의존하는 코드나 보존 데이터를 만들지 않을 것이고, 차후에 형식이 바뀌더라도 문제가 없을것이다. 

 추가로 javadoc 주석을 사용하여서 표현 형식을 규정하는 것과 하지 않는 예를 보자.


신고
Posted by JAVA_HOME

댓글을 달아 주세요

2011.04.15 15:56
hashCode 메소드를 제대로 오버라이드 하지 않아 코드 결함이 생기는 경우가 흔하다. 
항목 8에서 언급한 바와 같이 equals 메소드를 오버라이드 할때는 항상 hashCode 메소드를 오버라이드 해야 한다.

자바 API 문서의 Object.hashCode 메소드 명세에서 가져온 계약 사항은 다음과 같다.

- 애플리케이션 실행 중에 같은 객체에 대해 한 번 이상 호출되더라도 hashCode 메소드는 같은 정수를 일관성 있게 반환해야한다.(equals 메소드에서 비교하는 객체의 값이 변경되지 않는다면)
단, 애플리케이션이 매번 다시 실행될 때마다 일관되게 같을 필요는 없다.

- equals(Object) 메소드 호출 결과 두 객체가 동일하다면, 두 객체 각각에 대해 hashCode 메소드를 호출했을 때 같은 정수 값이 나와야 한다.

-equals(Object) 메소드 호출 결과 두 객체가 다르다고 해서 두 객체 각각에 대해 hashCode 메소드를 호출했을 때 반드시 다른 정수값이 나올 필요는 없다. 그러나 같지 않은 객체들에 대해 hashCode메소드에서 서로 다른 정수값을 반환한다면, 이 메소드를 사용하는 hash 컬렉션들의 성능을 향상시킬 수 있음을 알아야된다.


 이 중 중요한 조항은 두 번째의 동일한 객체들은 같은 해시 코드 값을 가져야 한다는 것이다.

 특히 HashMap과 HashSet 및 Hashtable을 포함하는 모든 hash 기반의 컬렉션들과 우리 클래스를 같이 사용할 때 잘못된 hashCode 메소드는 우리 클래스가 올바르게 동작하지 않게 만들 것이다.


 또한 Hash algorithm을 책이나 검색을 통해 알아보는 것이 좋다. 이는 hash 컬렉션 들의 속도를 향상 시킬수 있을 것이다. 절대로 임의의 Magic Number를 사용해서는 안된다
신고
Posted by JAVA_HOME

댓글을 달아 주세요

2011.04.15 15:34
 Object의 메소드 중 첫번째로 다뤄 볼 Equals 메소드는 절대 임의로 오버라이딩을 하면 안된다.

 임의로 오버라이딩을 하여서 생기는 문제를 피하는 가장 좋은 방법은 Object 클래스의 것, 또는 Super 클래스에서 오버라이드 한 것을 상속 받아서 그대로 사용하는 것이다.

 다음 조건 중 어느 하나라도 만족한다면 그대로 사용하는 것이 좋다.

 1. 클래스의 각 인스턴스가 본래부터 유일한 경우

더보기

2. 두 인스턴스가 논리적으로 같은지 검사하지 않아도 되는 클래스

3. 수퍼클래스에서 equals 메소드를 이미 오버라이딩 했고, 그 메소드를 그대로 사용해도 좋은 경우

4. private이나 패키지 전용 클래스여서 이 클래스의 equals 메소드가 절대 호출되지 않아야 할 경우 

더보기

 


그렇다면 오버라이드 해야 하는 경우는 언제일까?

 객체 참조만으로 인스턴스의 동일 여부를 판단하는 것이 아니라, 인스턴스가 갖는 값을 비교하여 논리적으로 같은지 판단할 필요가 있는 클래스로써, 자신의 수퍼클래스에서 equals 메소드를 오버라이드 하지 않았을 때이다. 일반적으로 Value 클래스가 여기에 해당된다. 또한 Map의 키나 Set의 요소로 객체를 저장하고 사용할 수 있게 하려면 equals 메소드의 오버라이딩이 꼭 필요하다.

 오버라이드 할 필요가 없는 값 클래스가 있는데, 그 중 하나는 각 값당 최대 하나의 객체만 존재하도록 인스턴스 제어를 사용하는 열거형(enum)과 같은 클래스이다.
 이런 클래스들은 값을 비교하는 논리적인 일치와  객체 참조 일치가 동일한 의미가 되기 떄문이다.


 equals 메소드를 오버라이드 할때는 이 메소드의 보편적 계약을 따라야한다. 아래의 내용은 자바 API 문서의 Object 클래스 명세를 가져 온 것이다.  

equals 메소드는 동등 관계(equivalence relation)을 구현하며, 다음 사항을 만족해야 한다. 

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


 모든 컬렉션 클래스를 포함하여 많은 클래스들이 전달 되는 객체는 equals 계약을 따른다고 믿고 행동하기 때문에 계약을 어기면 예기치 않은 결과가 나올 것이다.


양질의 equals 메소드를 만드는 방법을 열거해보자면 다음과 같다.

1. 객체의 값을 비교할 필요 없고 참조만으로 같은 객체인지 비교 가능하다면 == 연산자를 사용하자.
2. instanceof 연산자를 사용해서 전달된 인자가 올바른 타입인지 확인하자.
3. 인자 타입을 올바른 타입으로 변환한다.
4. 클래스의 "중요한(꼭 비교해야 하는)" 필드 각각에 대해서는 인자로 전달된 객체의 필드와 현재 객체(equals 메소드가 호출된)의 필드가 모두 같은지 빠뜨리지 말고 비교한다. 
5. 보편적 계약을 지키는지 꼭 확인한다. 필요하다면 단위 테스트 코드를 작성한다. 



추가로 유의할 사항 몇가지를 전달한다.

- equals 메소드를 오버라이드 할때는 hashCode 메소드도 항상 오버라이드 한다.
- 너무 똑똑한 척 하지마라. (너무 지나친 동일 비교를 고려하지 말자.)
- equals 메소드의 인자 타입을 Object 대신 다른 타입으로 바꾸지 말자. (오버로딩이 된다. @Override 주석을 다는것은 도움이 된다.)



 equals의 계약을 지키는 일은 매우 중요하고, 무심코 틀릴 가능성이 매우 크므로 꼭 책에서 각 계약을 어기는 잘못된 예를 찾아 보기 바란다.

 
신고
Posted by JAVA_HOME

댓글을 달아 주세요

2011.04.07 23:52
 모든 객체의 Super 클래스인 Object에는 equals, hashCode, toString, clone, finalize 메소드를 가지고 있다.
 이는 모든 자바 클래스에서 준수해야 하는 보편적 계약(general contracts)를 내포하고 있고, 서브 클래스에서는 이 메소드들을 오버라이드 하도록 설계 되어있다.
 계약을 따르는 것은 오버라이딩하는 클래스의 책임이고,  이를 잘못 사용한다면 이 계약에 따라서 행동하는 클래스(HashMap과 hashSet과 같은)이 올바로 동작하지 않을 것이다.

 이 장에서는 Object의 메소드들을 언제 어떻게 오버라이드 해야 올바른 지에 대해서 다룬다. 
 추가로 Object 클래스의 메소드는 아니지만 유사한 특성을 가지는 Comparable.compareTo 메소드를 다루고, 항목7 에서 다룬 finalize 메소드는 제외한다.
  
신고
Posted by JAVA_HOME

댓글을 달아 주세요


티스토리 툴바