참고 포스팅
내용에 대해서 자세하게 들어가기 전, 간단하게 소개하면 Comparable 과 Comparator는 정렬 시 기준을 정의하기 위한 인터페이스입니다.
Comparable
인터페이스는 compareTo(T o) 메소드 하나가 선언되어 있습니다. 만약, Comparable을 사용하고자 한다면 compareTo 메서드를 재정의해야 합니다.
Comparator
인터페이스는 compare(T o1, T o2) 메서드를 구현하면 사용이 가능합니다.
두 인터페이스의 진짜 역할은 "객체를 비교할 수 있도록 만든다" 는 것입니다.
이 말의 의미는 원시타입과 비교하면 쉽게 이해할 수 있습니다.
원시타입의 경우, 자바 자체에서 정의한 Comparable 인터페이스를 통해서 비교하기 때문에 저희가 편하게 비교 연산자를 통해서 동일한 타입의 두 변수를 비교할 수 있습니다.
하지만, 다음과 같이 학생이라는 클래스를 정의해보겠습니다.
class Student{
int age; // 나이
int grade; // 학년
Student(int age, int grade){
this.age = age;
this.grade = grade;
}
이와 같이 학생 클래스를 정의했습니다. 이 때, 만약 Student 클래스의 객체를 2개 생성한 뒤 비교할 경우, 어떤 값을 기준으로 비교를 해야할까요? 나이? 학년?
바로 이러한 점에서 Comparable 또는 Comparator가 사용된다는 것입니다.
객체는 본질적으로 비교 기준을 정해주지 않는 이상 어떤 객체가 우선순위가 높은지 판단할 수가 없습니다. 어떤 사람은 나이를 기준으로, 어떤 사람은 학년을 기준으로 판단하기 때문에 비교의 기준이 없으면, 명확하게 비교하기 어렵기 때문입니다.
그 다음으로는 두 인터페이스의 구현할 추상메서드의 매개변수 갯수의 차이에 대해 알아보겠습니다.
이는 Comparable은 자기 자신과 매개변수 객체를 비교하는 것이고, Comparator는 두 매개변수 객체를 비교하기 때문입니다.
본질적으로 비교하는 것은 같지만 비교 대상이 다르다는 것이 핵심입니다.
추가로, Comparable은 lang 패키지에 있어 import 해줄필요가 없지만, Comparator는 util 패키지에 있어 import 해줘야 합니다.
Comparable 인터페이스는 "자기 자신과 매개변수의 객체를 비교" 하는 것입니다.
위처럼 필수로 구현해야 하는 메서드인 "compareTo() 메서드가 우리가 객체를 비교하는 기준을 정의해주는 부분"입니다.
쉽게 설명하면, Comparable은 자기 자신과 매개변수를 비교하기 때문에 클래스명으로 생성한 객체 에서 compareTo(T o)를 호출하여 매개변수로 들어온 파라미터 o가 비교 객체가 됩니다.
예를 들면, 다음과 같이 구현할 수 있습니다.
class Student implements Comparable<Student> {
int age;
int classNumber
Student(int age, int className){
this.age = age;
this.classNumber = classNumber;
}
@Override
public int compareTo(Student o){
/**
* 비교 구현부
*/
}
}
비교 구현부에 따라, Student 객체의 비교 기준이 바뀌게 됩니다.
예를 들어서, 나이(age)를 기준으로 비교 구현을 해보도록 하겠습니다.
@Override
public int compareTo(Student o){
// 자기자신의 age가 o의 age보다 크다면 양수
if(this.age > o.age){
return 1;
}
// 자기 자신의 age가 o의 age가 같다면 0
else if(this.age == o.age{
return 0;
}
// 자기자신의 age가 o의 age보다 작다면 음수
else{
return -1;
}
}
값을 비교해서 정수를 반환해야 한다는 것입니다. 그럼 어떤 것을 기준으로 양수, 0, 음수를 반환하는 것일까요?
한 번 생각해보면, 우리는 자기 자신과 상대방을 비교하는 것입니다. 즉, 자기 자신을 기준으로 삼아 대소 관계를 파악해야 합니다.
일반적으로, 위처럼 조건문을 통해서 <, >, ==와 같이 비교 연산자를 통해서 대소비교를 하고 그에 따라 양수, 음수, 0을 반환하는 방식이 이해하기도 쉬울테고 가장 정석적인 방법입니다.
이를 조금 더 생각해보면, 객체 자신과 상대 객체와의 차이 값을 비교하여 반환하면 된다 라고 생각할 수 있습니다.
이는 다음과 같이 구현됩니다.
@Override
public int compareTo(Student o){
return this.age - o.age;
}
사실 우리가 편리하게 두 수를 비교하기 위해서 차이값을 구했지만, 여기에는 치명적인 단점이 있습니다. 뺄셈 과정에서 자료형의 범위를 넘어 버리는 경우가 발생할 수 있다는 것입니다.
예를 들어서, int 형인 경우에 크기가 4byte(32비트) 이며, 표현 범위가 -2^31 ~ 2^31-1 으로, 이를 풀어쓰면 다음과 같습니다. -2,147,483,648 ~ 2,147,483,647 입니다.
쉽게 말해서, -2,147,483,648 - 1 = -2,147,483,649 입니다. 하지만, int 자료형에서 표현할 수 없는 수로 2,147,483,647로 int 형의 최댓값을 반환하게 됩니다.
이러한 현상을 'Overflow' 라고 합니다.
public static void main(String[] args) {
int min = Integer.MIN_VALUE;
int max = Integer.MAX_VALUE;
System.out.println("min - 1 = " + (min-1));
System.out.println("max + 1 = " + (max+1));
}
예를 들어, 위와 같은 출력을 테스트해보면, 다음과 같이 결과가 나오게 됩니다.
그렇기 때문에 항시 compareTo 구현 또는 compare 구현 시에는 대소 비교에 있어서 이러한 Overflow가 발생하지 않는지 확인한 뒤에 사용해야 합니다.
특히나, primitive 값에 대해 위와 같은 예외를 만약 확인하기 어려운 경우, <,>,==으로 대소 비교를 해주는 것이 안전하며 권장되는 방식입니다.