스스로 공부한 내용을 기록한 포스트로 틀린 부분이 있을 수 있으니 오류를 확인해보시고 적용하시길 강력히 당부드립니다.
https://st-lab.tistory.com/243 를 참고하여 작성한 포스트입니다.
알고리즘 문제를 풀다보면 정수들을 정렬해야하는 경우가 있다. primitive type인 int
로 선언된 정수들은 Arrays.sort()
로, wrapper class인 Integer
로 선언된 정수들은 Collections.sort()
로 손쉽게 오름차순으로 정렬할 수 있다.
내림차순의 경우 Collections.sort(list, Collections.reverseOrder())
로 정렬할 수 있는데 이것은 wrapper class인 경우만 가능하다. 따라서 배열을 내림차순으로 정렬하고 싶은 경우 int[]
가 아닌 Integer[]
로 선언해주어야 한다.
하지만 사용자가 임의로 선언한 객체를 원하는 기준으로 정렬하고 싶은 경우는 단순히 sort()를 사용할 수 없다. 이런 경우 자바에서 제공하는 Comparable
혹은 Comparator
인터페이스를 활용하여 객체를 정렬할 수 있다. 인터페이스는 특정 메서드의 구현을 강제하는 속성이 있기 때문에 두 인터페이스에서 반드시 정의해줘야하는 메서드가 있다.
이제 Comparable
과 Comparator
가 무엇인지, 둘의 차이는 어떤게 있는지, 활용은 어떻게 하는지 알아보자.
두 인터페이스를 사용하는 이유에 대해서 생각해보면 정렬하기 위해 사용한다고 생각할 것이다. 하지만 이것은 좁은 의미에서 맞는 말이라고 할 수 있다.
두 인터페이스를 사용하는 궁극적인 목표는 두 객체를 '비교'하기 위함이다.
예를 들어 나이와 키가 정보로 주어진 어떤 두 사람이 있다고 하자. 이 두 사람을 비교해 달라는 요청을 받았을 때 반드시 되물어야할 질문이 있다. "나이로 비교할까요? 키로 비교할까요?" , "나이로 비교한다면 나이가 많은 사람을 우선순위에 둘까요? 적은 사람을 우선순위에 둘까요?" 등 정해줘야할 기준이 모호할 것이다. 바로 이런 문제를 해결하기 위해 두 인터페이스가 필요하다.
또한 반드시 구현해줘야하는 메서드의 파라미터도 차이가 있다.
Comparable
의 경우 compareTo(T o)
메서드에서 파라미터를 1개를 받고 Comparator
의 경우 compare(T o1 T o2)
메서드에서 파라미터 2개를 받는다. 대략적으로 감이 왔겠지만 compareTo()는 나 자신과 다른 객체를 비교하는 것이고 Compare()은 두 객체를 비교하는 것이다. 이제 기본적인 내용은 알아봤고 각각의 특징에 대해서 알아보자.
public class Person implements Comparable<Person>{
int age;
int height;
public Person (int age, int height) {
this.age = age;
this.height = height;
}
@Override
public int compareTo(Person o) {
return this.height - o.height;
}
}
Person
이라는 사용자 정의 객체를 만들었다.Comparable
인터페이스를 사용하였고 반드시 CompareTo(T o)
메서드를 구현해주어야한다.int
임을 확인할 수 있는데 그렇다면 정수값을 가지고 어떻게 비교를 하는 것일까?int
의 범위를 넘어갈 수도 있는 경우에는 오류가 발생한다. 그런 경우에는 case를 나눠 <, ==, >를 사용하여 리턴값이 int 범위가 되도록 하여야한다.Integer.MIN_VALUE
)인 경우 리턴값이 2,147,483,649가 되므로 오류가 overflow 이슈가 발생한다.compareTo(T o1)
을 반드시 정의해줘야한다.public class Person implements Comparator<Person> {
int age;
int height;
public Person (int age, int height) {
this.age = age;
this.height = height;
}
@Override
public int compare(Person o1, Person o2) {
return o1.height - o2.height;
}
}
Person
이라는 사용자 정의 객체를 만들었다.Comparator
인터페이스를 사용하였고 반드시 Compare(T o1, T o2)
메서드를 구현해주어야한다.compareTo(T o)
과 비슷하지만 compare(T o1, T o2)
는 자기자신이 아니라 서로 다른 두 객체를 비교한다는 점에서 차이가 있다. 즉, 비교를 하는 과정에서 자기 자신은 두 객체와 상관이 없다는 뜻이다. Person personA = new Person(28, 183);
Person personB = new Person(25, 177);
Person personC = new Person(21, 173);
int isBig = personA.compare(personB, personC);
if(isBig > 0){
System.out.println("personB가 personC보다 큽니다.");
} else if (isBig == 0){
System.out.println("personB와 personC가 같습니다.");
} else {
System.out.println("personB가 personC보다 작습니다.");
}
// 출력결과
// personB가 personC보다 큽니다.
compare
메서드를 호출해야하는데 비교 대상이 아닌 (personA)의 메서드에 파라미터로 personB, personC를 넣는 것이나 비교 대상 중 하나인 personB의 compare메서드를 호출해서 파라미터로 personB, personC를 넣는 것이나 결과는 똑같다.Person onlyForCompare = new Person(0,0)
을 만들고 onlyForCompare.compare(personB, personC)
와 같이 사용할 수도 있을 것이다.익명 객체는 무엇일까? 쉽게 말해서 이름이 없는 객체(클래스)라는 것이다. 아래 코드들을 보면서 이해를 해보자.
public class Anonymous {
public static void main(String[] args) {
Rectangle a = new Rectangle();
// 익명객체1
Rectangle anonymous1 = new Rectangle() {
@Override
int get() {
return height;
}
};
// 익명객체2
Rectangle anonymous2 = new Rectangle() {
int depth = 5;
@Override
int get() {
return height * width * depth;
}
};
System.out.println(a.get());
System.out.println(anonymous1.get());
System.out.println(anonymous2.get());
}
static class Rectangle {
int width = 20;
int height = 30;
int get() {
return width;
}
}
}
Rectangle a = new Rectangle()
과 같은 방식일 것이다.Rectangle anonymous1 = new Rectangle() { //..구현부..//}
로 생성한다. 거의 유사하지만 우리는 {} 안의 구현부에 집중해야한다.Rectangle anonymous1 = new Rectangle() { //..구현부..// }
에서 구현부를 보면 우리가 일반적으로 변수를 선언하고, 메서드를 재정의(override)하는 부분들이 있기 때문에 Rectangle과는 다른 새로운 객체를 생성하였다. 하지만 어느 부분에서도 객체의 이름을 찾아볼 수 없다.익명 객체에 대한 설명을 간략하게 하였는데 이해가 잘 되지 않는다면 포스트 맨 위 참고 페이지를 참조하길 바란다.
익명 객체에 대해 길게 알아본 이유에 대해서 다시 짚고 넘어가야한다. 우리는 두 객체를 비교해주는 Comparator를 구현해서 기능만 사용하고 싶기 때문에 익명 객체를 활용하기로 하였다.
분명히 Comparator는 인터페이스이기 때문에 구현(상속)할 대상이 존재한다. 즉 익명객체로 만들 수 있다는 것이다.
따라서 이름은 정의되지 않지만 Comparator를 구현하는 익명객체를 만들어서 compare 메서드를 사용할 수 있을 것이다. 코드를 통해 구현방법을 알아보자.
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
// 익명 객체 구현 1
Comparator<Person> comp1 = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.age - o2.age;
}
};
}
// 익명 객체 구현 2
public static Comparator<Person> comp2 = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.age - o2.age;
}
};
}
// 외부에서 익명 객체로 Comparator를 생성하기 때문에 클래스에서 Comparator를 구현할 필요가 없다.
class Person {
int age;
int height;
public Person(int age, int height) {
this.age = age;
this.height = height;
}
}
이제 Comparable, Comparator 두 인터페이스의 차이는 이해가 되었을 것이다.
객체를 비교하기 위해 compareTo, compare를 사용하는 것은 사용자가 정의한 기준을 토대로 양수, 0, 음수를 중 하나를 반환하는 것이다.
정렬관계를 알아보기 전에 한 가지 알고 가야할 것이 있다. Java에서의 일반적인 정렬기준이다. java는 특별한 정의가 되어 있지 않는 한 오름차순으로 정렬을 한다.
예를 들어 {1, 3, 2} 배열이 있다고 해보자. 우리가 정렬 알고리즘을 사용하면 두 수를 비교하게 된다. 0번 인덱스의 1과 1번 인덱스의 3을 비교하는 과정에서 1 - 3을 하면 음수가 반환될 것이다. java는 오름차순을 기본으로 하기 때문에 compareTo와 compare가 반환하는 값이 음수이면 선행 원소가 후행 원소보다 작다는 뜻이므로 위치를 교환하지 않는다. 그렇다면 1번 인덱스의 3과 2번 인덱스의 2를 비교해보자. 3 - 2를 하면 양수가 반환되고 이 뜻은 선행원소가 후행원소보다 크다는 뜻이므로 위치를 교환한다. 정리를 하면 오름차순을 디폴트로 두는 java의 특성상 두 수를 비교할 때 반환되는 값이
이를 염두해두고 객체를 내림차순으로 정렬하고자 하면 어떻게 해야할까?
앞서 비교하는 메서드의 반환값이 음수이면 위치를 교환하고, 양수이면 위치를 교환하지 않는다고 하였다. 그렇다면 선행원소가 후행원소보다 클 때 반환되는 값이 양수가 되도록 하면 java는 위치를 교환하지 않을 것이다. 따라서 리턴값을 반대로 하면 원하는 대로 내림차순으로 정렬될 것이다. 쉽게 말해 (선행원소 - 후행원소)의 값이 음수일 때 반대로 바꿔주어 양수가 반환되도록하면 내림차순 정렬이 완성될 것이다. 즉, 우리가 사용했던 Person예제에서 compare(T o1, T o2)
의 반환부인 return o1.height - o2.height
를 return -(o1.height - o2.height)
로 바꿔주기만 하면 된다. 이를 좀 더 간단히 하면 return o2.height - o1.height
으로 바꿔줄 수 있다.