-
[Java] Comparator와 Comparable란?Java 2022. 2. 3. 00:01
Arrays.sort()를 호출하면 컴퓨터가 알아서 배열을 정렬하는 것처럼 보이지만, 사실은 Character클래스의 Comparable의 구현에 의해 정렬됩니다.
Comparator와 Comprable이란?
모두 인터페이스로 컬렉션을 정렬하는데 필요한 메서드를 정의하고 있습니다.
정렬을 하는 데는 두 가지가 필요합니다.
1. 정렬 대상
2. 정렬 기준
Comparator와 Comparale은 객체 정렬에 필요한 정렬 기준을 제공하는 인터페이스입니다.
인터페이스이기 때문에 Comparable 또는 Comparator를 사용하고자 한다면 인터페이스 내에 선언된 메서드를 반드시 구현해야 합니다.
가장 중요한 것은 Comparable과 Comparator는 객체를 비교할 수 있도록 만듭니다!
Comparator와 Comparable의 등장 배경
우리는 보통 기본형 변수(byte, int, double 등)는 부등호를 가지고 변수를 비교할 수 있었습니다.
하지만 객체를 비교하기 위해서는 객체가 가지고 있는 정보를 토대로 정렬 기준이 필요합니다.
예를 들어 사람을 객체라고 가정한다면 사람은 정렬 대상이 되는 것이며 정렬 기준으로는 키, 몸무게, 이름 오름차순, 내림차순 등등 정렬 기준이 많습니다.
따라서 객체는 기준을 정해주지 않으면 컴퓨터는 어떤 객체가 더 높은 우선순위를 갖는지 판단할 수 없기 때문에 이러한 문제점을 해결하기 위해 Comparable 또는 Comparator가 사용됩니다.
자바 API에서 Comparable를 찾아보겠습니다
public interface Comparable<T>{ int compareTo(T o); }
앞서 소개한 대로 Comparable은 인터페이스이며, Java.lang 패키지에 존재합니다.
Comparable을 구현하는 각 클래스의 객체에 전체 순서를 부여합니다.
Comparable 인터페이스에서는 compareTo(T O) 추상 메서드 하나만이 선언되어 있습니다.
이 메서드는 인자로 들어온 객체와 비교하여 순서를 지정합니다.
만약 Comparable을 사용하려면 compareTo 메서드를 재정의해야 합니다.
compareTo 메서드는 객체가 인자로 들어온 객체보다 작으면 -1, 같으면 0, 크면 1을 반환합니다.
자바 API에서 Comparator를 찾아보겠습니다.
@FunctionalInterface public interface Comparator<T>{ int compare(Object o1, Object o2); boolean equals(Object obj); }
Comparator는 인터페이스이며, java.util 패키지에 존재합니다.
아까와는 다르게 @FunctionalInterface 어노테이션이 붙어있습니다. 이는 인터페이스가 하나의 추상 메서드를 가지고 있음을 의미합니다!
여기서 의문점이 두 가지 생길 수 있습니다.
1. Comparable도 추상 메서드를 하나를 가졌는데 왜 어노테이션이 붙지 않을까요?
사실 Comparable도 추상 메서드가 하나이기 때문에 함수형 인터페이스입니다.
하지만 Comparable은 "이것을 비교한다"가 아닌 "이것을 비교할 수 있다"를 의미하기 때문에 어노테이션을 사용하지 않습니다.
오라클 Interface 명세서에는 다음과 같이 기록되어있습니다.
9.6.4.9. @FunctionalInterface일부 인터페이스는 부수적으로 기능하기 때문에 기능 인터페이스의 모든 선언에 @FunctionalInterface 주석을 추가하는 것이 필요하거나 바람직하지 않습니다.
즉, 여기서 Comparable 인터페이스는 부수적으로 기능하기 때문입니다.
Comparable은 일반적으로 객체 A.compareTo(객체 B)로 사용합니다.
Comparator는 람다식을 사용할 수 있습니다.
예시 메서드) comparing(Function <? super T,? extends U> KeyExtractor)
2. Comparator는 추상 메서드가 두 개인데 왜 함수형 인터페이스인가요?
boolean equals(Object obj)는 java.lang.Object의 공영 메서드 중 하나인 equals를 재정의 하는 추상 메서드로써 이것은 추상 메서드로 간주되지 않습니다.
단지 Comparator를 구현하는 클래스는 오버 라이딩이 필요할 수도 있다는 것을 알리기 위해서 정의한 것뿐입니다.
따라서 단 하나의 추상 메서드인 int compare(T o1, T o2)가 존재하기 때문에 함수형 인터페이스입니다.
그리고 추상 메서드 이외에도 Comparator에는 다양한 메서드들이 존재합니다.
거의 20개의 메서드들이 존재하지만 default 메서드와, static 메서드들로 이미 정의되어 있기 때문에 이들은 추상 메서드가 아닙니다.
Comparable의 구현
public final class Integer extends Number implements Comparable { ... public int compareTo(Integer anotherInteger{ int v1 = this.value; int v2 = anotherInteger.value; //같으면 0, 오른쪽 값이 크면 -1, 작으면 1을 반환 return ( v1 < v2 ? 1 : (v1==v2? 0 : 1); } ... }
3항 연산자를 사용하여 같으면 0, 오른쪽 값이 크면 -1, 작으면 1을 반환합니다.
7,5이라는 숫자가 들어왔을 때 1을 반환합니다.
5,7이라는 숫자가 들어왔을 때 -1을 반환합니다.
사실 왼쪽 값이 크면 음수를 , 두 값이 같으면 0, 왼쪽 값이 크면 양수를 반환하면 되는데 왜 0 ,-1 , 1을 정확하게 반환할까요?
return v1 - v2; 를 사용한다면 삼항 연산자를 2번 사용할 필요가 없을 텐데 말이죠
이유는 삼항 연산자를 사용하는 것이 성능이 2~3% 정도 미세하게 더 빠르다고 합니다.
Integer 값이 32비트여서 뺄셈을 할 때 비트 연산이 모두 일어나지만 비교의 경우에는 비트 모두를 계산할 필요 없이 앞부분이 다르면 어느 것이 큰지 결정이 됩니다.
따라서 정렬을 하기 위해 compareTo 메서드가 계속 호출되기 때문에 코드가 조금 길어져도 빠른 것을 택한 겁니다.
단지 compareTo 메서드로 값을 비교하는 것 가지고는 정렬이 안 되겠죠?
정렬이라는 것은 두 대상을 비교하는 것이며 따라서 이 반환한 값을 가지고 정렬에 사용합니다.
만약 오름차순이라면 반환된 값이 1이라면 7,5의 위치를 바꿉니다.
정렬은 Comparable 내부적으로 구현돼있는 것은 아니며 앞서 말했듯이 객체를 비교할 수 있는 정렬 기준을 구현한 것입니다.
실제로 정렬은 sort() 메서드를 사용하여 정렬 대상과 정렬 기준을 매개변수로 입력받아 실행됩니다.
Comparable와 Comparator의 사용
class Ex11_7 { public static void main(String[] args){ String[] strArr = {"cat", "Dog", "lion", "tiger"}; Arrays.sort(strArr); //String의 Comparable구현에 의한 정렬 System.out.println("strArr=" + Arrays.toString(strArr); Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER); System.out.println("strArr=" + Arrays.toString(strArr); Arrays.sort(strArr, new Descending()); System.out.println("strArr=" + Arrays.toString(strArr); } } class Descending implements Comparator { public int compare(Object o1, Object o2){ if( o1 instanceof Comparable && o2 instanceof Comparable){ Comparable c1 = (Comparable) o1; Comparable c2 = (Comparable) o2; return c1.compareTo(c2) * -1 ; //1을 곱하여 기본 정렬방식의 역으로 변경 //c2.compareTo(c1)을 사용해도 됩니다. } return -1; } } //결과 strArr = [Dog, cat, lion, tiger] //결과 strArr = [cat, Dog, lion, tiger] //결과 strArr = [tiger, lion, cat, Dog]
Arrays.sort(strArr)에는 정렬 대상은 있는데 정렬 기준이 없습니다.
이런 경우에는 String 클래스의 내부의 Comparable 구현에 의한 정렬 기준을 갖습니다. ( 대문자가 소문자보다 우선순위)
Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER);
정렬 대상 와 정렬 기준이 있습니다. CASE_INSENSITIVE_IRDER는 대소문자를 구분하지 않는 정렬 기준으로 자주 쓰이기 때문에 String 클래스에 이미 구현되어 있는 것입니다.
Arrays.sort(strArr, new Descending());
정렬 대상과 정렬 기준이 있는데 new Descending()은 Comparator를 구현한 Descending 클래스를 사용합니다.
Descending 클래스를 살펴보면 Comparator interface를 사용하여 compare 메서드를 재정의하고 있습니다.
compare 메서드는 Comparable 타입으로 형 변환한 뒤 compareTo 메서드를 사용하여 기본 정렬 방식의 역으로 변경합니다.
Comparator의 람다식 적용
List<String> list = Arrays.asList("DEF", "ABC", "GHI"); //Comparator 익명클래스로 구현 Collections.sort(list, new Comparator<String>(){ public int compare(String s1, String s2){ return s1.compareTo(s2); } }); //Comparator 람다식으로 구현 Collections.sort(list, (s1,s2) -> s1.compareTo(s2)); for(String str: lst){ System.out.print(i + " "); //"ABC" "DEF" "GHI" 출력 }
Comparator는 functional Interface(함수형 인터페이스) 이기 때문에 람다식을 적용할 수 있습니다.
출처
자바의 정석
https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Comparable.html
https://st-lab.tistory.com/243
https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/Comparator.html
https://stackoverflow.com/questions/56827701/is-comparable-interface-a-functionalinterface
https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.6.4.9
https://mkyong.com/java8/is-comparator-a-function-interface-but-it-has-two-abstract-methods/
https://sgcomputer.tistory.com/300
'Java' 카테고리의 다른 글
[Java] 스트림의 중간연산 (0) 2022.02.09 [Java] Math.max() vs 삼항연산자 (0) 2022.02.07 [Java] 스트림의 생성 (0) 2022.01.31 [Java] 스트림(Stream)이란? + 특징, 등장배경 (0) 2022.01.29 [Java] 싱글톤(Singleton)이란? (0) 2022.01.27