인터페이스 Comparable 과 Comparator
우선 두 개에 대해 배우기 전에 공통점을 알아야한다. 이 두가지는 인터페이스라는 것이다.
인터페이스라는 것은 내부에 선언된 메소드를 반드시 구현해야한다는 것을 명심해야한다.
그렇다면 각각의 인터페이스에서 이 메소드를 가장 집중해서 알아보아야한다.
Comparable은 CompareTo(T o)
Comparator는 Compare(T o1, T o2)
이 메소드들이 해당 인터페이스들의 중요 메소드라고 볼 수 있다.
얘들은 뭐하는 애들일까?
이 두 인터페이스를 사용하는 이유가 뭘까? 물론 정렬을 할 때 굉장히 많이 사용하지만 중요한 건 따로 있다.
객체를 비교할 수 있다
기본적으로 같은 자료형은 부등호로도 충분히 쉽게 비교를 할 수 있다. 하지만 우리가 새로운 객체를 만든다고 생각해보자
public class Test {
public static void main(String[] args) {
Student a = new Student(17, 2);
Student b = new Student(18, 1);
}
}
class Student {
int age;
int classNumber;
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
}
main 메소드를 보면 두개의 객페를 생성한 것을 알 수 있다.
이 두개의 값을 비교하려면 어떻게 해야할까? 나이? 학급? 어떤 것을 기준으로 비교하는게 좋을까?
어떤 객체가 더 높은 우선순위를 갖는지 판단 할 수가 없다.
이때 해당 문제점을 해결해 줄 수 있는 것이 바로 Comparable, Comparator이다.
또한 위에 말해준 메소드를 보면 Comparable은 한개의 매개변수를 가지고 Comparator는 두개의 매개변수를 가지고 있는 것을 볼 수 있다.
왜냐하면 Comparable은 자기자신과 파라미터로 들어온 객체를 비교하는 것이고
Comparator는 자기 자신은 신경끄고 파라미터로 들어온 객체만 비교하는 것이다.
Comparable
자기 자신과 매개변수 객체를 비교하는 것
위에서 말한 메소드를 다시 꺼내 보자면 Comparable의 가장 중요한 메소드는 CompareTo(T o)라는 것이다 이것이 바로 우리가 객체를 비교할 기준을 정의해주는 부분이 된다
우리가 class를 만들때 Class를 비교하려고 할 텐데 Comparable은 자기 자신과 매개변수 객체를 비교한다고 했다. 즉, 자기자신은 Class로 생성한 객체 자신이 되는 것이고 매개변수 객체는 Class.compareTo(T o)를 통해 들어온 o와 비교를 하는 것이다.
Student와 비교를 해보자
class Student implements Comparable<Student> {
int age;
int classNumber;
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
@Override
public int compareTo(Student o) {
if(this.age > o.age) {
return 1;
} else if (this.age == o.age) {
return 0;
} else {
return -1;
}
}
}
만약 여기서 우리가 나이의 값으로 비교를 하고 있다. 근데 여기서 중요한 것이 따로 있다.
양수, 0, 음수의 반환
우리는 자기 자신을 기준으로 삼고 대소관계를 파악하는 것이다. 때문에 자기자신이 기준값이 되고 매개변수로 들어온 객체의 특정값이 나보다 작다면 양수 같다면 0 , 크다면 음수라는 결과가 나오게 되는 것이다.
결국 중요한 것은 비교를 한다는 행위인 것이라는 것이다. 그럼 비교 대상의 값을 반환하는 방법을 사용하는 것은 안되는 것일까? 물론 가능하다.
그저 부등호 방식이 아닌 this.age - o.age 를 return하면 간단한 것이다. 하지만 이는 주의할 점이 존재한다
자료형의 범위에 문제가 나타나게 된다. int 형의 경우 -2,147,483,648 ~ 2,147,483,647 과 같은 범위가 존재하는데 혹시나 해당 범위의 값을 넘어가는 반환값을 가지게 되면 안된다는 것이다. 이와 같이 주어진 범위의 상한선을 넘기는 현상을 Overflow라고 한다.
그렇기 때문에 우리가 비교를 할 때 Overflow가 발생할 여지가 있는지를 꼭 확인하고 사용하는 것이 좋다.
특히 primitive값에 대해 위와 같은 예외를 만약 확인하기 어렵다면 부등호를 사용하는 것이 훨씬 안정적이다. 때문에 일반적으로 많이 사용하게 되는 것이다.
Comparator
두 매개변수 객체를 비교
여기서 앞서 배운 Comparable과의 차이점을 알 수 있다. 자기자신과는 전혀 상관없이 들어온 매개변수만 비교하니 말이다.
Comparator의 필수 구현 부분인 compare()를 알아 볼 것이다. 기본적으로 compare메소드는 compareTo와 메커니즘이 같다 계속해서 말하지만 자기자신과 비교하느냐 아니냐 이 두 개의 차이이다.
이번에도 Student를 예시로 들어 확인해보자 이번에는 classNumber를 기준으로 객체를 비교해보자
class Student implement Comparator<Student> {
int age;
int classNumber;
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
@Override
public int compare(Student o1, Student o2) {
if(o1.classNumber > o2.classNumber) {
return 1;
} else if(o1.classNumber == o2.classNumber) {
return 0;
} else {
return -1;
}
}
}
이렇게 비교를 할 수 있다. compareble에서는 선행 원소가 자기자신이고 후행 원소가 o1이 되었다면 comparator에서는 o1이 되고 후행 원소가 o2가 되는 것이다.
그 이후는 모두 같다 정말 두개의 차이는 그정도이다. 비교하는 행위와 대수비교 모든 것이 같지만 선행원소가 자기자신이냐 매개변수이냐의 차이가 되는 것이다.
익명 개체를 통한 Comparator의 활용
위에 보았던 것처럼 compare 메소드를 사용하려면 결국 객체가 필요하다. 객체와 객체가 비교를 하니까 말이다.
예를 들어 객체 a,b,c가 있다고 생각해보자 이들을 비교하려면 어느 한 객체를 기준으로 compare를 사용하는데 정확한 기준점이 존재하지 않아서 데이터의 일관성이 현저히 떨어진다.
물론 비교만을 위해서 Student 객체를 하나 더 생성해주는 방법이 있지만 그저 비교만 하고 버려질 객체를 생성하는 것은 너무 좋지않다.
그래서 우리는 비교기능을 따로 두고 싶다. 그러려면 어떻게 해야할까?
익명 객체(클래스)를 이용한다.
익명 객체가 뭘까? 바로 이름이 정의되지 않는 객체라는 뜻이다.
이름이 정의되지 않는 객체??? 우리가 클래스를 만들면 이름을 정의하는 것은 당연했다. 근데 갑자기 이름을 짓지 않겠다니? 당연히 불가능한 일이 아닌가? 무슨말인지 천천히 알아보자
익명 객체 만들기
public class Anonymous {
public static void main(String[] args) {
Rectangle a = new Rectangle();
// 익명 객체 1
Rectangle anonymous1 = new Rectangle() {
@Override
int get() {
return width;
}
};
System.out.println(a.get());
System.out.println(anonymous1.get());
System.out.println(anonymous2.get());
}
// 익명 객체 2
static Rectangle anonymous2 = new Rectangle() {
int depth = 30;
@Override
int get() {
return wodth * heigth * depth;
}
};
}
class Rectangle {
int width = 10;
int height = 20;
int get() {
return height;
}
}
위의 코드를 보면 익명 객체라고 주석이 달린 객체 생성 방식을 보면 우리가 알던 기존의 방식이 조금 다르다.
우리가 흔히 아는 객체 생성 Rectangle anonymous = new Rectangle() { 구현부 }가 중요하다.
우리는 여기서 객체의 구현에 집중해보아야 한다. 객체를 구현하는 것은 변수를 선언하고 메소드를 정의하고 하나의 클래스를 만드는 것이다.
무슨말이냐면 클래스를 구현하는 방식
- 일반적인 클래스와 같은 구현 방식
- interface 클래스를 implements해서 메소드를 재정의
- 클래스를 상속하여 부모의 메소드와 필드를 사용 또는 재정의
모두 객체를 구현하는 것이다. 이떄 구현을 하는 클래스들은 모두 이름이 존재한다.
자 anonymous2의 구현부를 살펴보자
- 변수를 선언 했는가? = O
- Rectangle 클래스의 메소드 get()을 Override를 했는가? = O
우린 새로운 클래스를 만든것과 다름이 없다 심지어 새로운 클래스인데 이름조차도 정의되어있지 않다.
이는 anonymous1 객체 또한 마찬가지이다.
아니 Rectangle을 통해 새롭게 구현한 것인데 그럼 이름이 Rectangle아닌가요?
아니다 anonymous코드를 비슷하게 만들었지만 이름을 정의한 방법으로 코드를 작성하면 어떻게 되는지 알아보자
public class Anonymous {
public static void main(String[] args) {
Rectangle a = new Rectangle();
ChildRectangle child = new ChildRectangle();
System.out.println(a.get());
System.out.println(child.get());
}
}
class ChildRectangle extends Rectangle {
int depth = 40;
@Override
int get() {
return width * height * depth;
}
}
class Rectangle {
int width = 10;
int height = 20;
int get() {
return height;
}
}
비슷하지만 ChildRectangle을 class로 새롭게 구성하고 Rectangle을 상속받게 하여 구현은 Anonymous2와 같게 구현했다. 하지만 이는 이름이 정의되었다 그리고 child라는 변수명을 통해 사용된다.
하지만 위에 익명 객체는 이것과 다르다는 것이다. 코드는 이름이 정의되어있지 않고, anonymous라는 이름의 객체만 생성되어 있는 것이다. 이것을 익명객체라고 하는 것이다.
다만 이름이 정의되지 않기 때문에 특정 타입이 존재하는 것이 아니다 익명 객체는 반드시 상속할 대상이 있어야 한다는 것이다.
익명 개체를 통한 Comparetor의 구현
comparator의 기능만을 사용하기 위해 익명 개체에 대한 장황한 설명을 해보았다. 즉, Comparator의 구현을 통해 compare만 사용하기 위해 어떻게 해야할까?
Comparator는 인터페이스이다. 이는 구현할 대상이 존재한다는 것이다. 바로 익명 개체로 만들수 있다라는 것이다.
그렇다면 우린 이제 두 가지의 익명 개체 구현 방법을 통해 어떻게 Comparator를 구현하는지 알아보자
public class Test {
public static void main(String[] args) {
// 익명 객체 구현 방법 1
Comparator<Student> comp1 = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.classNumber - o2.classNumber;
}
};
}
// 익명 객체 구현 방법 2
public static Comparator<Student> comp2 = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.classNumber - o2.classNumber;
}
};
}
// 굳이 Comparator를 상속받아 구현하지 않아도 된다.
class Student {
int age;
int classNumber;
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
}
위에서 두가지 방법으로 익명 객체를 만든 모습을 볼 수 있다.
- main 함수 밖에 static 타입으로 선언한 방식
- main 함수 안에 지역 변수처럼 non-static으로 선언한 방식
이렇게 구현해준다면 초기에 생각했던 객체를 기준으로 하는 compare 사용 방식이 아닌 comp를 사용한 비교방식을 사용하면 된다.
심지어 익명객체는 이름이 없을 뿐 클래스와 같다고 보면 된다고 했었다. 즉, 여러개가 생성가능 한 것이다. 그렇다면 굳이 classNumber뿐 아니라 age를 비교하는 comp를 만들어서 사용할 수 도 있다는 것이다.
주의
그렇다면 Comparable은 왜 안하는가 이는 비교 대상의 차이가 있는데 Comparable은 자기 자신과 비교하는 것이라고 설명했는데 우리가 만든것은 익명 개체이다. 즉, 이름이 없는 객체로써 자기 자신이라는 것 자체가 없다. 그래서 자기 자신과 비교가 되지 않기 때문에 만약에 만든다고 한들 동일한 타입 비교는 불가능 하다. 그저 Comparable을 구현한 곳에 변수를 만들어 어떠한 값과 비교하
는 수준밖에 되지 않을것이다.
Comparator 와 Comparable의 정렬 관계
객체를 비교하기 위해 Comparable 또는 Comparator를 쓴다는 것은 곧 정의한 기준을 토대로 비교하여 양수, 0, 음수 중 하나가 반환된다는 의미이다
Java는 기본적으로 오름차순을 기준으로 한다.
만약 {1, 3, 2} 배열이 있다면 이를 정렬하기 위해 두개의 원소를 비교하게 된다. 그럼 선행 원소인 1과 후행원소인 3을 비교한다 그러면 1 - 3 이 되고 결과는 음수를 반환하게 될 것이다.
즉, 오름차순은 선행 원소가 후행 원소보다 작다라는 뜻인 것이다. 이것은 compare 또는 compareTo를 사용해서 객체를 비교할때 음수가 나오면 두 원소의 위치를 바꾸지 않겠다는 뜻이다.
정리
음수 : 두 원소의 위치를 교환하지 않는다
양수 : 두 원소의 위치를 교환한다.
한번 예를 들어서 정리해보자
public class Test {
public static void main(String[] args) {
MyInteger[] arr = new MyInteger[10];
// 객체 배열을 초기화한다.
for(int i = 0 ; i < 10; i++) {
arr[i] = new MyInteger((int)Math.random() * 100));
}
}
}
class MyInteger {
int value;
public MyInteger(int value) {
this.value = value;
}
}
그저 만들기만한 객체인 MyInteger이다. 그럼 이제 이것을 활용해서 Comparable 과 Comparator를 구현해보자
1. Comparable를 상속받아 구현
public class Test {
public static void main(String[] args) {
MyInteger[] arr = new MyInteger[10];
// 객체 배열을 초기화한다.
for(int i = 0 ; i < 10; i++) {
arr[i] = new MyInteger((int)Math.random() * 100));
}
for(int i = 0; i < 10; i++) {
// 정렬 전의 값
}
Arrays.sort(arr);
for(int i = 0; i < 10; i++) {
// 정렬 이후의 값
}
}
}
class MyInteger implements Comparable<MyInteger> {
int value;
public MyInteger(int value) {
this.value = value;
}
// 자기 자신의 value를 기준으로 파라미터 값과의 차이를 반환한다
@Override
public int compareTo(MyInteger o) {
return this.value - o.value;
}
}
이렇게 코드를 작성해주면 Arrays.sort()를 통해 정렬을 진행하게 되면서 값이 정렬이 될 것이다. 하지만 Comparable을 상속해서 compareTo를 정의해주지 않는다면 예외가 발생한다
왜냐하면 sort를 통해 정렬을 진행하려 하는데 해당 클래스가 비교할 수 있는 기준이 정의되어 있지 않는 것이다. 따라서 정렬 자체가 불가능해진다.
2. Comparator로 구현
public class Test {
public static void main(String[] args) {
MyInteger[] arr = new MyInteger[10];
// 객체 배열을 초기화한다.
for(int i = 0 ; i < 10; i++) {
arr[i] = new MyInteger((int)Math.random() * 100));
}
}
static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
@Override
public int compare(MyInteger o1, MyInteger o2) {
return o1.value - o2.value;
}
};
}
class MyInteger implements Comparable<MyInteger> {
int value;
public MyInteger(int value) {
this.value = value;
}
}
Comparator는 익명 개체를 통해 정렬을 진행하도록 만들게 된다. 그렇다면 MyInteger에 비교할 메소드가 없는데 어떻게 비교하나요? 하지만 Arrays.sort()의 파라미터에는 Comparator또한 받기때문에 문제없다.
코드로 구현하자면 이렇게 작성해볼수 있다.
public class Test {
public static void main(String[] args) {
MyInteger[] arr = new MyInteger[10];
// 객체 배열을 초기화한다.
for(int i = 0 ; i < 10; i++) {
arr[i] = new MyInteger((int)Math.random() * 100));
}
for(int i = 0; i < 10; i++) {
// 정렬 전의 값
}
Arrays.sort(arr, comp);
for(int i = 0; i < 10; i++) {
// 정렬 이후의 값
}
}
static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
@Override
public int compare(MyInteger o1, MyInteger o2) {
return o1.value - o2.value;
}
};
}
class MyInteger implements Comparable<MyInteger> {
int value;
public MyInteger(int value) {
this.value = value;
}
}
차이점은 Arrays.sort에서 파라미터로 comp를 주었다는 것이다. 그러면 comp의 비교기준을 가지고 파라미터로 넘어온 객체 배열 arr를 정렬하게 되는 것이다.
이런식으로 Comparable와 Comparator의 오름차순 정렬 방법을 알 수 있었다.
내림차순
Java의 기본 정렬 방식은 오름차순이다. 그래서 위에 모든 구현은 오름차순을 기본으로 만들었지만 내림차순은 어떻게 할 수 있을까? 정답은 양수, 음수에 있다. 지금까지 말한것처럼 반환값이 음수라는 것은 선행 원소가 후행 원소보다 작다는 것이다. 그러면 반대로 생각하면 되는 것이다. 더 큰수를 앞으로 보내야 하는 내림차순은 o1 - o2 에서 o2 - o1로 하면 되는 것이다.
예를 들어 1과 3을 비교할때 오름차순은 음수를 반환하게 하여 그대로 두지만 내림차순은 그 반대로 양수를 반환하게 만들면 되는 것이다. 3 - 1로써 양수가 나타나게 되면 두개의 자리가 바뀌게 되면서 오히려 3이 앞으로 가게 되는 것이다.
이러한 형식으로 내림차순을 완성시켜주면 되는 것이다.