"Comparator & Comparable"
객체 정렬에 필요한 메서드(정렬기준 제공)를 정의한 인터페이스
Comparable : 기본 정렬기준을 구현하는데 사용
Comparator : 기본 정렬기준 외에 다른 기준으로 정렬하고자할 때 사용
Comparable >> compareTo();
Comparator >> compare();
public interface Comparator {
int compare(Object o1, Object o2);
- compare >> Comparator가 가지고 있는 메서드
int compare(Object o1, Object o2); >> o1과 o2를 비교해서 정수값 반환
*****
결과 값 : 0 >> o1과 o2가 같다
결과 값 : 양수 >> 왼쪽값이 더 크다.
결과 값 : 음수 >> 오른쪽값이 더 크다.
public interface Comparable {
int compareTo(Object o);
int comparTo(Object o);
>> 주어진 객체 (o)를 자기자신(this)과 비교
"정렬(sort)은 무엇일까?"
두 대상을 비교한 다음 자리바꿈을 반복하는 것이 정렬이다.
위에서 본 두 메서드를 확인해보면,
compare >> o1, o2 두 대상을 비교하고,
compareTo >> 주어진 객체(o)와 자기자신(this) 두 대상을 비교한다.
Comparator와 Comparable은 둘 다 정렬에 사용되고, "정렬기준"을 제공한다.
정렬 기준이란?
정렬기준에는 오름차순, 내림차순 등등 여러가지 정렬기준이 있다.
사람으로 생각해봐도, 이름, 나이, 주소, 성적, 몸무게 등등 다양하게 정렬기준을 생각해 볼 수 있는데,
이것은 컴퓨터가 알아서 할 수 있는 부분이 아니다.
우리가 컴퓨터에게 정렬시킬 때는 반드시 정렬 기준을 제공해줘야한다.
이 때 사용하는 정렬 기준이 " Comparator"와 "Comparable"이다.
compare()와 compareTo()의 작성 방법
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));
}
...
Integer 클래스의 내용인데, Comparable은 compareTo 추상메서드를 구현하게 되어있다.
위에서 얘기했듯이 compareTo는 주어진 객체와 자기 자신(this)을 비교하는 것이기 때문에,
주어진 매개변수 anotherInteger 와 this.value 자기 자신을 비교한다.
this.value를 v1에 담아주고, anotherInteger를 v2에 담아서 삼항 연산자를 두 번 사용하여 비교한 것이다.
(v1 < v2 ? -1 : (v1 == v2 ? 0 : 1)); >> 첫번째 삼항 연산자에서 참이면 -1을 반환하고, 거짓이면 두번째 삼항연산자로 넘어가서
v1과 v2가 같으면 0을, 아니면 1을 반환하게 된다.
같으면 0, 오른쪽이 크면 음수(-), 작으면 양수(+)를 반환하게 하는 구조인데,
0이 나오면 자리바꿈을 할 필요가 없고, 오름차순과 내림차순일 때는 경우에 따라 달라진다.
만약, 오름차순인데 7다음 5가 나왔다고 하면 7이 더 크기 때문에 자리를 바꿔줘야하고,
내림차순인데 5 다음 7이 나오면 자리를 바꿔야 한다. 오름차순일 때와 내림차순일 때 자리 바꾸는 상황이 다르다.
>> 오름차순 : 왼쪽값이 더 크면 자리바꿈,
>> 내림차순 : 오른쪽 값이 더 크면 자리바
중요한 것은 0, 양수, 음수 셋중 하나의 값을 반환하게 되어 있고,
메서드를 호출해서 결과가 0인지 음수인지 양수인지에 따라서 자리바꿈을 할지 말지를 알려준다.
간단한 예제를 통해 자세히 알아보자,
public static void main(String[] args) {
String[] strArr = {"cat", "Dog", "lion", "tiger"};
Arrays.sort(strArr);
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;
}
return -1;
}
}
- 기본정렬기준
Arrays.sort(strArr);
System.out.println("strArr =" + Arrays.toString(strArr));
String[] strArr = {"cat", "Dog", "lion", "tiger"}; 배열을 선언해주고, sort로 정렬 하여 출력해보면
첫번째 출력은 strArr =[Dog, cat, lion, tiger] 이렇게 나오는데, Dog가 맨 앞에 간 이유는, 대문자이기 때문이다.
대문자와 소문자가 있으면 대문자가 앞에 간다.
그 다음
- 대소문자 구분 안하는 정렬 기준
Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER);
System.out.println("strArr =" + Arrays.toString(strArr));
이 코드는 대소문자를 구분하지 않고 출력이 된다.
출력 결과
strArr =[cat, Dog, lion, tiger]
str.Arr이 정렬 대상이고, String.CASE_INSENSITIVE_ORDER가 정렬 기준이다.
.CASE_INSENSITIVE_ORDER는 String 클래스에 있는 static 상수인데,
public static final Comparater<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
이렇게 선언이 되어 있고, Comparator이다. 자주사용을 해서 String클래스에 Comparator를 구현한 내부 클래스로 만들어 넣어놓은 것이다.
그래서 원래는 대문자와 소문자가 있으면 대문자가 앞으로 가지만, .CASE_INSENSITIVE_ORDER 정렬 기준에 의해 대소문자를 구분하지 않기 때문에, cat이 먼저 출력된 것을 확인할 수 있다.
참고로 정렬을 하기 위해선 항상 "정렬 대상" 과 "정렬 기준"이 있어야 한다.
static void sort(Object[ ] a) >> 이렇게 정렬 기준이 없는 경우에는 객체 배열에 담긴 객체가 Comparable 정렬 기준을 갖고 있는 경우에만 사용할 수 있다.
static void sort(Object[ ] a, Comparator c) >> sort가 작업을 하려면 이렇게 정렬 대상과 정렬 기준을 줘야한다.
그렇다면 첫번째 sort는 정렬 대상만 있었는데 어떻게 사용이 가능했던걸까?
- 기본정렬기준
Arrays.sort(strArr);
System.out.println("strArr =" + Arrays.toString(strArr));
얘는 String 클래스에 기본 정렬기준 Comparable을 갖고 있기 때문에 정렬기준을 따로 제시해주지 않아도 사용이 가능했던 것이다. 대신 Comparable은 기본 정렬 기준이기 때문에, 사전 순서대로 정렬이 된다. 만약 기본정렬(사전순서) 말고 다른 방법으로 정렬하고 싶다면 Comparator로 따로 구현하여 사용해야된다.
이 부분이 아까 위에서 얘기했던
Comparable : 기본 정렬기준을 구현하는데 사용,
Comparator : 기본 정렬기준 외에 다른 기준으로 정렬하고자할 때 사용
이것의 의미이다.
- 기본정렬기준의 역순
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;
}
return -1;
}
Descending 메서드에서 Comparator 인터페이스를 구현하여 새롭게 정렬기준을 만들었다.
여기서 c1.copareTo(c2) * -1 은 기본정렬기준에 *-1을 해서 양수 -> 음수, 0은 그대로 0, 음수 -> 양수로 바꿔준다.
그래서 결과를 출력해보면 strArr =[tiger, lion, cat, Dog] 이렇게 기본정렬기준의 역순으로 출력이된 것을 확인할 수 있다.
참고로 c1.compareTo(c2) * -1 이렇게 해도 되고, c2.compareTo(c1) 이렇게 c1과 c2를 바꿔해도 된다. 둘이 똑같다.
보충 설명
Integer 클래스의 코드를 살펴보자,
public final class Integer extends Number implements Comparable {
...
public int compareTo(Object o) {
return compareTo((Integer)o);
}
public int compareTo(Integer anotherInteger) {
int thisVal = this.value;
int anotherVal = anotherInteger.value;
// 비교하는 값이 크면 -1, 같으면 0, 작으면 1을 반환한다.
return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
}
.....
String, Integer, Float 등등 이러한 클래스들은 Comaparable (기본정렬기준)을 자체적으로 가지고 있다.
this.value(자기 자신의 값)와 anotherInteger 값을 삼항연산자를 사용하여 비교하고, 왼쪽이 크면 -1을 오른쪽이 크면 1을 반환해준다.
public int compareTo(Integer anotherInteger) {
int thisVal = this.value;
int anotherVal = anotherInteger.value;
// 왼쪽 값이 크면 음수를, 두 값이 같으면 0, 왼쪽 값이 크면 양수를 반환한다.
return thisVal - anotherVal; // 내림 차순의 경우 반대로 뺄셈하면 된다.
}
return thisVal - anotherVal; >> 이 부분은 위에서 봤던 Integer 클래스에서 봤
return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1)); 이것과 똑같다.
값을 빼서 0이 나오면 두 값이 같다는 것이고, 음수가 나오면 오른쪽값이 크다는 것이고, 양수가 나오면 왼쪽값이 더 크다는 것이다.
ex) 5-7 = -2 (음수) >> 오른쪽값이 더 크다, 5-5 = 0 >> 두 값이 같다. 7-5 = 2(양수) >> 왼쪽값이 더 크다.
두 값을 빼봐도 알 수 있는데, 삼항연산자를 사용하는 이유는 성능적으로 좀 더 빠르다고 한다.
총 정리
정렬은 두 대상을 비교하면서 자리바꿈을 반복하는 것이다.
정렬에는 다양한 방법이 있는데,
버블정렬, 삽입정렬, 선택정렬, 퀵정렬, 병합정렬 등등.....
이 많은 정렬 방법들이 두 대상을 비교해서 자리바꿈 한다는 것은 다 똑같다.
대신, 어떠한걸 선택해서 바꿀지 전략이 다른 것이다.
두 대상을 비교하여 자리바꿈을 반복한다는 것은 불변이다.
즉, 많은 정렬 방법들이 가지고 있는 공통점이다.
하지만, 정렬기준은 가변이다. 변할 수 있다.
한마디로 불변인 정렬 방법은 그대로 두고, 가변인 정렬기준을 제공해주면 된다.
버블정렬 코드 예시 (Comparable)
static void sort(int[] intArr) {
for (int i = 0; i < intArr.length - 1; i++) {
for (int j = 0; j < intArr.length - 1; j++) {
int tmp = 0;
if (intArr[j] > intArr[j + 1]) {
tmp = intArr[j];
intArr[j] = intArr[j + 1];
intArr[j + 1] = tmp;
}
}
}
}
1. 두 대상을 비교 (가변) >> 정렬기준
if (intArr[j] > intArr[j + 1]) {
2. 자리바꿈 반복
tmp = intArr[j];
intArr[j] = intArr[j + 1];
intArr[j + 1] = tmp;
}
예시의 버블정렬 코드에서는 정렬대상이나 방법이 바뀌어도 이 전체 정렬 로직 자체는 바뀌지 않는다, 불변이다.
두 대상의 비교를 어떻게 할건지 가변인 부분(정렬기준)만 바뀌면 되는 것이다.
단, 주의할 점은 현재 버블정렬코드 예시는 Comparable(기본정렬기준)이다.
만약 Comparator를 활용한 경우라면 외부에서 정렬기준을 따로 구현을 하고 그 기준을 받아와서 쓰기 때문에,
Comparable과 혼동하지 않도록 유의하자.