본문 바로가기

WEB_Programming/Pure Java

Object Ordering

Object Ordering
List l은 다음과 같이 소트된 것이다고 가정하자.
Collections.sort(l);
만약 List의 내용이 String 엘리먼트로 구성되어 있다면, 알파벳 순서로 정렬이 된다. 만약 Date엘리먼트라면 연대순으로 정렬될것이다. 어떻게 이렇게 된것일까? String과 Date는 둘다 Comparable인터페이스를 구현하고 있어서 그렇게 된다. Comparable 구현은 클래스를 위한 일반적인 오더링 방법을 제공할때 사용된다. 이러한 구현객체들은 자동적으로 소트가 이루어 진다. 다음 테이블은 Java 플랫폼에서 Comparable가 구현된 객체들에 대한 리스트들이다.
Classes Implementing Comparable
ClassNatural Ordering
Byte Signed numerical
Character Unsigned numerical
Long Signed numerical
Integer Signed numerical
Short Signed numerical
Double Signed numerical
Float Signed numerical
BigInteger Signed numerical
BigDecimal Signed numerical
Boolean Boolean.FALSE < Boolean.TRUE
File System-dependent lexicographic on path name
String Lexicographic
Date Chronological
CollationKey Locale-specific lexicographic

list의 소트를 하고자 하는데 Comparable가 구현되어 있지 않다면, Commections.sort(list)는 아마도 ClassCastException을 던질 것이다. 유사하게 Collections.sort(list, comparator)을 이용하고자 해서 다른 comparator을 이용하고자 할때에도 각 엘리먼트가 비교를 할 수 없는 상황이라면 CalssCastException을던질 것이다. Elements를 서로 비교할수 있는것을 mutually comparable라고 부른다.
비록 서로다른 엘리먼트 타입이라도 mutually comparable이면, 리스트에서는 상호간에 비교를 허용하게 된다.

Comparable 인터페이스에 대해서 각 엘리먼트를 비교하거나 컬렉션에서 그들을 비고하고자 한다면 다음 섹션에서는 Comparable 타입의 구현에 대해서 설명할 것이다.

Writing Your Own Comparable Types

Comparable 인터페이스는 다음과 같은 메소드로 구성되어 있다.
public interface Comparable<T> {
public int compareTo(T o);
}
cpmpareTo 메소드는 주어진 객체와 특정 객체 사이의 비교를 수행하며, 음수, 0, 양수 값을 반환한다. 주어진 객체가 현재 객체보다 작을 경우에는 음수를, 동일한경우 0, 지정된 대상 객체보다 클 경우에는 양수를 반환한다. 만약 지정된 객체가 주어진 객체와 비교를 수행할 수 없을경우 ClassCastException을 반환한다.

  following class representing a person's name 구현을한 예이다.

import java.util.*;

public class Name implements Comparable<Name> {
private final String firstName, lastName;

public Name(String firstName, String lastName) {
if (firstName == null || lastName == null)
throw new NullPointerException();
this.firstName = firstName;
this.lastName = lastName;
}

public String firstName() { return firstName; }
public String lastName() { return lastName; }

public boolean equals(Object o) {
if (!(o instanceof Name))
return false;
Name n = (Name)o;
return n.firstName.equals(firstName) &&
n.lastName.equals(lastName);
}

public int hashCode() {
return 31*firstName.hashCode() + lastName.hashCode();
}

public String toString() {
return firstName + " " + lastName;
}

public int compareTo(Name n) {
int lastCmp = lastName.compareTo(n.lastName);
return (lastCmp != 0 ? lastCmp :
firstName.compareTo(n.firstName));
}
}
이전에 짧은 예제를 보면, 이전 예제에는 제한이 있으며, 중간 이름을 지원하지 않는다는 것이고, 첫번째 이름과 마지막 이름에 의존된다는 것, 그리고 국제화를 지원하지 않는것을 확인할 수 있다. 그럼에도 불구하고, 이것은 중요한 포인트를 말해준다.
  • Name 객체는 변하지 않는다.equals에 적용될 것은 변하지 않는 타입이어야 한다는 것이고, 이러한 값을 위해서 Set의 엘리먼트나,  Map의 키값이 적당하게 이용될 수 있다. 이러한 컬렉션은 그것들의 엘리먼트를 변경하거나 키값을 변경하는 경우 깨어지기 때문이다.
  • 생성자 체크에서 널 체크를 수행한다. 이것은 모든 Name객체는 지정된 형태의 메소드를 처리하고 있으며 NullPointException을 통해서 체크를 수행하여 객체를 보장하고 있다는 것이다.
  • hashCode는 재정의한 메소드이다. 이것은 특정 클래스를 위한 필수 값으로 equals메소드를 재정의해야 한다. (Equals 객체는 반드시 hash codes가 동일 해야한다.)
  • equals 메소드는 객체가 널이거나 잘못된 타입인경우 널을 반환한다. compareTo 메소드는 특정 환경에서 런타임 익셉션을 던지게 된다. 이러한 특징은 각각의 메소드의 일반적은 계약에 의해서 지정되는 행동이다.
  • toString메소드는 Name 객체를 사람들이 읽기 쉬운 형태로 재정의하고 있다. 이러한 것은 좋은 아이디어이며, 특히 객체가 특히 collection에 put 혹은 get되는 경우 매우 유용하다. 다양한 컬렉션 타입에서 toString메소드는 그들의 엘리먼트, 키값들, 값들에 의해서 값이 결정된다.
이번 섹션에서 엘리먼트의 정렬에 대한 것이다. Name's의 compareTo 메소드에 대해서 조금 이야기 해보자. 이것은 특별한 이름 정렬 알고리즘을  구현한다. last names는 첫번째 이름을 이전에 검사를 수행한다. 이것은 자연스러운 정력 방식이라고 생각할 것이다. 자연스럽다, 혹은 비자연스럽다는 매우 결정하기 혼란한 문제다.

어떻게 compareTo 메소드를 구현했는지 확인해보라. 이것이 매우 일반적인 방식이기 때문에 유용할 것이다. 첫번째로 객체의 표기적인 부분에 대해서 비교를 많이 하게 된다. 종종 자연적인 오더링 방식을 파트 특정 부분으로 변경하여 검사를 수행할 수 있다. 이번 예에서는 part는 스트링으로 문자열 정렬 방식으로 정렬하고 있는 것이다. 만약 결과가 0이 아닌 값을 반환하는 경우에는 다음에 제시하는 다양한 파트를 검사해 볼 수 있다. 이번 예에서는 오직 2개의 파트에 대해서 다루고 있는 것이다. first name과 last name이 그것이다. 

a program that builds a list of names and sorts them.은 이러한 처리를 테스트 해 보는 예제이다.

import java.util.*;

public class NameSort {
public static void main(String[] args) {
Name nameArray[] = {
new Name("John", "Lennon"),
new Name("Karl", "Marx"),
new Name("Groucho", "Marx"),
new Name("Oscar", "Grouch")
};
List<Name> names = Arrays.asList(nameArray);
Collections.sort(names);
System.out.println(names);
}
}
만약 이 프로그램을 수행한다면 다음과 같은 결과를 출력할 것이다.
[Oscar Grouch, John Lennon, Groucho Marx, Karl Marx]
여기에는 compareTo 메소드의 행동에 대해서 4가지 제약을 가지고 있다. which we won't go into now because they're fairly technical and boring and are better left in the API documentation.
Compable를 구현하여 제약을 따르게 하는 것은 실제로 모든 클래스에서 중요한 작업이다. 만약 이러한 비교를 수행하기를 원한다면 Comparable를 읽어보길 바란다. 기술적으로 말해서 이러한 제약은 클래스의 객체들의 최종 정렬을 위해서 자연적인 정렬을 보장해주는 구현방법이다. 이것은 매우 올바른 방식으로 소팅을 수행하기 위해 필요한 작업이다.

Comparators

만약 natural ordering대신 다른 정렬 방식을 이용하길 원한다면 어떻게 해야할까? 혹은 몇몇 객체를 Comparable를 구현하지 않고 소팅을 구현하고 싶을경우 어떻게 해야할까? 이러한 상황에서 Comparator라는 것이 지원되고 있다. 이것은 객체에 정렬 방식을 캡슐화 하는 방법으로 Comparable 인터페이스와 같으며, 다음과 같은 한가지 메소드를 가지고 있다.
public interface Comparator<T> {
int compare(T o1, T o2);
}
compare 메소드는 2개의 인자로 구성되며, 음수, 0, 양의 정수값을 반환하며, 첫번째 아규먼트가 더 작은경우 음수를, 같은경우 0을, 두번째 값보다 큰경우 양수의 값을 반환한다. 만약 야규먼트가 Comparator을 통해서 서로 비교하기에 적합하지 않은경우 ClassCastException을 반환한다.

Comparable를 Comparator에 적합하게 적용하기 위해서는 많은 설명이 필요하다. compara메소드를 쓰는것은  cmopareTo메소드를 구현하는 것과 가까운 작업이다. 다른것은 객체를 전달하는 아규먼트가 틀리다는 것이다. compare 메소드는 Comparable's의 compareTo 메소드와 동일한 4가지 제약조건을 가지고 잇다. Comparator은 객체의 비교를 위해서 전체적인 정렬방식을 필요로 한다.

다음 Employee라고 불리는 클래스가 있고 다음과 같다면

public class Employee implements Comparable<Employee> {
public Name name() { ... }
public int number() { ... }
public Date hireDate() { ... }
...
}
Employee 인스턴스는 Name employee 이름에 대해서 정렬을 수행하고 있다고 가정하자. 불행하게도 선임자 순서대로 정렬을 수행하고자 하는 경우가 있다고 하는경우 어떻게 해결해야할까? 이 의미는 무언가를 작업을 해야한다는 말이며, 그렇게 많지는 않을 것이다. 다음 프로그램은 요청되는 리스트를 처리하는 방법이다.
import java.util.*;
public class EmpSort {
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
return e2.hireDate().compareTo(e1.hireDate());
}
};

// Employee database
static final Collection<Employee> employees = ... ;

public static void main(String[] args) {
List<Employee>e = new ArrayList<Employee>(employees);
Collections.sort(e, SENIORITY_ORDER);
System.out.println(e);
}
}
Comparator는 매우 당연하면서 직관적이다. 이것은 Date 의 자연적인 정렬 순서대로 지정된다. 또한 hireDate메소드를 통해서 접근하고 있음을 확인할 수 있다. 이것은 Comparator는 hire date의 두번째 아규먼트가 첫번째에 비해 어떠한지를 검증하고 있음을 확인할 수 있다. 결과로 누가 가장 최근에 들어왔는지, 누가 선배인지 확인할 수 있게 되었다. 가끔 이 값을 거꾸로 수정해야 한다면 다음과 같은 코드로 가능하다.
// Don't do this!!
return -r1.hireDate().compareTo(r2.hireDate());
You  should always use the former technique in favor of the latter because the latter is not guaranteed to work. The reason for this is that the compareTo method can return any negative int if its argument is less than the object on which it is invoked. There is one negative int that remains negative when negated, strange as it may seem.
-Integer.MIN_VALUE == Integer.MIN_VALUE
이전 프로그램에서 Comparator는 List의 소팅을 위해서 매우 적합하다. 그러나 무언가 부족하다. 이것이 TreeSet과 같은 정렬된 값의 재 정렬 할 수 없다. 외냐하면 ordering이 equals과 조화되지 않기 때문이다. 이 의미는 Comparator 에서 equals메소드는 서로 동등하지 않다는 의미이다. 특히, 두개의 employee가 동일한 날에 고용되어 두 값이 equal인경우가 있을 것이다. 만약 List에서 소팅을 하고자 한다면 이것은 문제가 되지 않는다. 그러나. Comparator를 이용하여 Collection을 소팅하고자 한다면 문제가 있다. 만약 동일한 날짜에 여러명의 직원을 고용하여 TreeSet에 넣고자 하는 경우 첫번째 값만 Set에 저장될 것이기 때문이다. 두번째는 중복된 엘리먼트로 값이 무시된다.

이러한 문제를 수정하기 위해서 Comparator을 조정해야하며, 이러한 작업을 위해서 equals 와 함께 매핑을 해야한다. 다른말로 이러한 조정작업은 compare를 이용할때 오직 엘리먼트의 동등한지 여부를 확인하고자 할때 만 사용된다. 이것은 또한 equals를 이용하여 비교를 수행핼때에도 동일하게 적용된다. 두 부분을 비교할때 Name과 같은 객체의 예에서 첫번째 파트는 우리가 관심을 가지는 부분이다. 이번 케이스에서 보면 hire date가 그것이된다. 두번째 파트는 객체를 유니크하게 구분 할 수 있는 부분이 될것이다. 여기에 employee number가 가장 좋은 속성값이 될 것이다. 이러한 Comparator의 결과는 다음과 같다.

static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
int dateCmp = e2.hireDate().compareTo(e1.hireDate());
if (dateCmp != 0)
return dateCmp;
return (e1.number() < e2.number() ? -1 :
(e1.number() == e2.number() ? 0 : 1));
}
};
마지막 주의점중의 하나는 최종적인 return 구분은 단순하게 만들려는 충동을 느낄 것이다. 다음과 같이.
return e1.number() - e2.number();
절대로 이렇게 하지는 말길 바란다. employee number이 음수 값을 가지게 된다고 명확하게 알 수 있다고 할지라도 말이다. 이것은 일반적인 방법이 아니다. 왜냐하면 부호있는 정수형 타입은 2개의 변경이 가능한 정수형 타입과의 차이를 명확하게 구분하는데 이용되기에 좋은 값이 아니기 때문이다. 만약 i라는 값이 매우 큰 양의 정수이고, j가 아주작은 음의 정수형이라고 한다면 i - j의 결과는 정수형 표현 범위를 넘어서, 음의 정수를 반환하게 될 것이다. comparator 위반 결과는 잘못된 결과나, 미묘하지만 찾기 어려운 버그를 만들어 낼 것이다. 이것은 반드시 이렇게 해야하는 개념적 이론은 아니다. 그러나 사람들은 이것때문에 고생을 한다. ^^

'WEB_Programming > Pure Java' 카테고리의 다른 글

The SortedMap Interface  (1) 2008.11.03
The SortedSet Interface  (0) 2008.11.03
The Map Interface  (0) 2008.10.28
The Queue Interface  (0) 2008.10.28
The List Interface  (0) 2008.10.24