반응형

최상위 객체인 Object에 선언되어 있는 메서드는 아니지만 자주 사용되는 compareTo 메서드에 대해 설명한다.

 

int java.lang.String.compareTo(String anotherString)

자바에서 기본적으로 제공하는 값 클래스(value class)는 compareTo 메서드가 구현되어 있으며 또한 String 클래스에도 compareTo가 구현되어 있다. String.compareTo 메서드는 문자열을 비교하여 같으면 0을, 사전순으로 작으면 음수를, 사전순으로 크면 양수를 리턴한다.

	public static void main(String[] args) {
		String str1 = "apple";

		System.out.println(str1.compareTo("banana")); // -1
		System.out.println(str1.compareTo("apple")); // 0
		System.out.println(str1.compareTo("abc")); // 14
	}

 

String.compareTo 메서드의 소스코드는 아래와 같다.

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

1. 문자를 하나씩 비교하여 다를 경우 차를 구해 바로 리턴한다.

2. 길이가 작은 문자열의 길이 만큼 비교하여도 같다면 문자열의 길이가 작은게 사전순으로 더 앞에 있다고 판단하고 차를 구해 리턴한다.

 

이렇게 compareTo 메서드는 해당 클래스 객체들의 자연적 순서를 비교할 수 있도록 기능을 제공한다. 그리고 다들 알겠지만 compareTo는 일반적인 메서드가 아닌 Comparable 인터페이스에 정의되어 있는 유일한 메서드이다.

 

Comparable 인터페이스

Comparable 인터페이스는 클래스 객체에 순서를 정의하도록 한다. 이 순서를 클래스의 자연 순서라고 하며 클래스의 compareTo 메소드는 자연 순서를 계산할 수 있도록 기능을 구현해야 한다. 그래서 위와 같이 String 클래스는 Compareable 인터페이스를 구현하였고, 자연 순서를 계산할 수 있도록 compareTo 메서드를 구현한 것이다.

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
	...
}

 

규칙9에서 설명했던 hashCode는 규약을 따르지 않으면 hashCode를 사용하는 자바 API(ex : HashMap) 동작에 문제가 발생한다고 설명했다. 마찬가지로 Comparable의 compareTo 메서드를 사용하는 자바 API도 여러 개 존재하는데 compareTo 규약을 준수하지 않을 경우 API를 오동작시킬 수 있다. 아래는 compareTo를 사용하는 API의 예시이다.

 

- void java.util.Arrays.sort(Object[] a)

	public static void arraySort() {
		String arr[] = { "banana", "apple", "google", "aws" };

		Arrays.sort(arr);
		System.out.printf("%s\n", Arrays.toString(arr)); // [apple, aws, banana, google]

		Arrays.sort(arr, Collections.reverseOrder());

		System.out.printf("%s\n", Arrays.toString(arr)); // [google, banana, aws, apple]
	}

 

- java.util.TreeSet<E>

	public static void treeSet() {
		String arr[] = { "banana", "apple", "google", "aws" };

		Set<String> s = new TreeSet<String>();
		s.addAll(Arrays.asList(arr));

		System.out.println(s); // [apple, aws, banana, google]
	}

 

그렇다면 이제 compareTo를 잘 구현해야한다. 규약을 요약하자면 아래와 같다.

 

- Symmetry(대칭성)

X가 Y보다 크다면 Y는 X보다 작아야 한다. 반대로 X가 Y보다 작다면 Y는 X보다 커야 한다. 또한 X가 Y랑 같다면 Y도 X랑 같아야 한다.

 

- Transitivity(추이성)

X가 Y보다 크고, Y가 Z보다 크다면 X는 Z보다 커야한다.

 

- Consistent(일관성)

X와 Y가 같다면 X와 Z와의 비교 결과와 Y와 Z의 비교 결과는 항상 같다.

 

(이펙티브 자바 책에서는 위 순서를 반사성, 대칭성, 추이성이라고 표현하지만, 필자는 잘못되었다고 생각한다.)

 

- (강력히 추천하지만 절대적으로 요구되는 것은 아닌) X와 Y의 compareTo의 결과가 0이라면 equlas의 결과는 true여야 한다.

네번째 규약은 특별한 경우가 아니라면 지키는게 좋다. 자바에서 제공하는 대부분의 컬렉션 구현체들은 마지막 규약이 지켜졌다고 생각하고 동작한다. 만약 규약을 지키지 않으면 엄청난 재앙을 가져오진 않지만, 그래도 약간의 문제가 발생할 수 있다.

 

네번째 규약을 지키지 않은 대표적인 클래스가 있는데 예시는 아래와 같다.

	public static void main(String[] args) {
		BigDecimal a = new BigDecimal("1.0");
		BigDecimal b = new BigDecimal("1.00");

		System.out.println(a.equals(b)); // false
		System.out.println(a.compareTo(b)); // 0
		System.out.println((a.compareTo(b) == 0) == a.equals(b)); // false

		Set<BigDecimal> hashSet = new HashSet<BigDecimal>();
		hashSet.add(a);
		hashSet.add(b);

		System.out.println(hashSet.size()); // 2

		Set<BigDecimal> treeSet = new TreeSet<BigDecimal>();
		treeSet.add(a);
		treeSet.add(b);

		System.out.println(treeSet.size()); // 1
	}

BigDecimal은 소수점까지 모두 동일해야지 동일한 것으로 판단한다. 하지만 compare는 크기를 비교하므로 0을 리턴한다. 따라서 네번 째 규약을 지키지 않는 클래스이다. 이렇기 때문에 equals(hashCode)로 동일성을 판단하는 HashSet에서는 두 원소를 넣어도 다르다고 판단하여 두 개의 객체가 존재한다. 하지만 TreeSet은 compareTo로 비교하기 때문에 두 객체는 동일하다고 판단하여 하나의 객체만 존재하게 된다.

 

근데 만약 이미 구현되어 있는 클래스의 compareTo 순서 비교 방식이 아닌 새로운 순서 비교 방식을 사용하고 싶다면 어떻게 해야할까? 이럴 경우 Comparator 인터페이스를 구현하여 사용하면 된다.

 

Comparator 인터페이스

일반적으로 Comparable.compareTo를 사용하는 메서드는 Comparator의 compare 메서드를 사용할 수 있도록 구현되어있다. (ex : Collections.sort 또는 Arrays.sort) 따라서 미리 구현되어 있는 compareTo 방식이 아닌 새로운 방식으로 순서를 지정하고 싶다면 Comparator를 구현하면 된다.

 

아래는 예시이다.

	public static void main(String[] args) {
		String arr[] = { "banana", "apple", "google", "aws" };

		Arrays.sort(arr, new Comparator<String>() {
			public int compare(String o1, String o2) {
				return o2.compareTo(o1); // o1을 o2와 비교한 것이 아닌 o2를 o1과 비교
			}
		});

		System.out.printf("%s\n", Arrays.toString(arr)); // [google, banana, aws, apple]

	}

 

Arrays.sort에 새롭게 구현한 Comparator 구현체를 사용하였다. 자세히 보면 기존 o1.compareTo(o2)가 아닌 o2.compareTo(o1)을 사용하여 사전순이 아닌 역사전순으로 비교하도록 하였다.

 

이렇게 자신이 원하는 순서를 다시 재정의할 수 있는데, 자바에서는 자주 사용하는 비교 방식의 Comparator를 미리 제공한다. 위에 예시였던 arraySort() 메서드에서 보면 Collections.reverseOrder()를 사용하였는데, 이는 우리가 지금 구현한 Comparator와 동일하게 구현되어 있는 구현체이다. 이 외에도 String 클래스에서는 대소문자를 무시하여 비교하는 구현체를 제공한다. 아래는 해당 소스이다.

    public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();
    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

 

반응형

+ Recent posts