티스토리 뷰

이번 아이템에서는 Comparable 인터페이스의 비교, 정렬, 제네릭이란 특성을 가진 메서드인 compareTo()에 대해 알아보겠다. compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻한다.

Comparable을 구현한 Collections와 Arrays 등의 객체의 배열은 다음처럼 쉽게 정렬할 수 있다.

Arrays.sort(/* 배열 객체*/);
Collections.sort(/* 컬렉션 하위 객체*/);

그 외에도 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게할 수 있다.아래는 중복 제거 후 알파벳으로 출력하는 코드다.

/* [code] Item14_Main.java */
public class Item14_Main {
    public static void main(String[] args) {
        String[] strArr = {"죠르디","죠르디","팬다주니어","콥","빠냐","빠냐","스카피"};
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, strArr);
        System.out.println(s);
    }
}

-

/* [console] */
[빠냐, 스카피, 죠르디, 콥, 팬다주니어]

알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.

compareTo 메서드의 일반 규약

이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.

다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수(signum fucntion)를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.

  • Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다(따라서 x.compareTo(y)는 y.compareTo(x)가 예외를 던질 떄에 한 해 예외를 던져야 한다.
  • Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareToz) > 0 이면 x.compareTo(z) 이다.
  • Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 다.
  • 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.

"주의: 이 클래스의 순서는 eqauls 메서드와 일관되지 않다."

일반 규약 추가 설명

Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수타입은 컴파일 타임에 정해진다. 입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다.

// T에 인수 타입이 들어가서 해당 인수 타입에 따라 정해진다.
public interface Comparable<T> {
    public int compareTo(T o);
}

인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다. 인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다. Null을 인수로 넣어 호출하면 NullPointerException을 던져야 한다.

compareTo 세부 내용

compareTo 메서드는 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다. 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.

// 객체 참조 필드: 클래스 내 변수로 선언된 객체
public class Bear {
    private String name;
    private int height;
    private int size;
    private Habitat habitat; // 객체 참조 필드
}

Comparable을 구현하지 않는 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용한다. 기본 타입들간에 비교를 할 때는 박싱된 기본 타입 클래스들에 새로추가된 정적 메서드인 compare을 이용한다. 관계연산자와 < 와 > 를 사용하는 이전 방식은 절대 사용해서는 안된다.

관계연산자: ==, !=, <=, >= 등 참 거짓을 판별하는데 사용되는 연산자.

compareTo로 비교할 때는 클래스에 핵심 필드를 우선으로하여 비교한다. 비교 과정에서 결과가 0이 아니라면, 즉 순서가 결정되면 거기서 끝이지만 아니면 계속해서 비교한다.

public int compareTo (PhoneNumber pn){
    int result = Short.compare(num1, pn.num1);
    if (result == 0){
        result = Short.compare(num2, pn.num2);
        if(result == 0)
            result = Short.compare(num3, pn.num3);
    }
    return result;
}

자바 8에서는 람다 함수를 통하여 비교자 생성 메서드를 생성할 수 있다. 그런데 이 방식에는 약간의 성능 저하가 뒤 따른다. 저자는 아래 예제를 정렬된 배열에 적용해보니 컴퓨터에서 10% 정도 느려졌다고 한다.

자바의 정적 임포트 기능을 이용하면 정적 비교자 생성 메서드들을 그 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해진다. 정적 임포트에 대해서는 차후에 다른 글을 통해 다뤄보도록 하겠다.

정적 임포트 기능에 대한 세부적인 내용은 아래에 참조 1에 정리하겠다.

private static final Comparator<PhoneNumber> COMPARATOR =
            Comparator.comparingInt((PhoneNumber pn) -> pn.num1)
                    .thenComparingInt(pn-> pn.num2)
                    .thenComparingInt(pn-> pn.num3);

public int compareTo (PhoneNumber pn){
        return COMPARATOR.compare(this, pn);
    }

이따금 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫번째 값이 크면 양수를 반환하면 양수를 반환하는 compareTo나 compare 메서드와 마추할 것인데, 이 방식은 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다.

부동소수점 계산 방식에 따른 오류는아래에 참조 2에 정리하겠다.

// 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
    @Override
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

// 비교자 생성 메서드를 이용한 비교자
static Comparator<Object> hashCodeOrder = 
        Comparator.comparingInt(o->o.hashCode());

참고

참고 1. 정적 임포트 기능

MathRef 클래스에 생성한 PI 변수를 Item14_Main 클래스에서 import static 하여 클래스 명 없이 PI 변수명과 메서드명을 그대로 호출해서 사용했다.

변수명과 메서스명이 같더라도 import static Item14.MathRef.PI; 한번으로 둘 모두 import 할 수 있다.

/* [CODE] MathRef.java */
package Item14;

public class MathRef {
    public static final float PI = 3.14f;
    public static float PI(){
        return PI;
    }
}

/* [CODE] Item14_Main.java */
package Item14;

import static Item14.MathRef.PI;

public class Item14_Main {
    public static void main(String[] args) {
        System.out.println(PI);
        System.out.println(PI());
    }
}

참고 2. 부동소수점 계산 방식에 따른 오류

자바를 포함한 모든 프로그래밍언어가 소수를 완벽히 포현해내지 못하는데, 그이유는 컴퓨터는 2진수로 모든 숫자를 이해하고 연산하기 때문이다. 예를 들면, 0.1과 0.2를 이진수로 표현하면 변수 타입에 따라 소숫점 뒷자리들을 잃어버리게 되고 이는 곧 오류로 이어질 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함