들어가며
알고리즘 문제를 풀다가 문득
아무생각 없이 쓰던 Comparable 과 Comparator 에 대해서 찾아보던중
너무 잘 설명해주신 글을 찾았다.
https://st-lab.tistory.com/243
내 포스팅은
잊지 않기 위해 요약 및 재포스팅 할 뿐
자세한 설명은 위 링크를 보고 이해하는 편이 훨씬 좋을 것이다.
Comparable? Comparator?
기본적으로 Comparable 과 Comparator는 interface 다.
그렇기 때문에 interface 내에 선언된 method를 반드시 구현 해야 한다.
공식 문서에 따르면
Comparable - compareTo(T o)
Comparator - compare(T o1, T o2)
를 구현해야 한다.
https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html
https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html
두 interface 모두 객체를 비교하는것은 마찬가지지만
Comparable은 자기 자신과 매개변수 객체를 비교,
Comparator는 두 매개변수 객체를 비교 한다는 것이 다르다.
또한, Comparable은 lang 패키지에 있기 때문에 따로 import 해 줄 필요가 없지만
Comparator는 util 패키지에 있기 때문에 import를 해 주어야 한다.
1. Comparable
Comparable은 자기자신과 매개변수 객체를 비교한다고 하였다.
Comparable interface는
public interface Comparable<T>{...} 라고 정의되어있는데
Comparable interface를 implements 할 때 T형 객체를 작성해주면 되는 것이다.
코드로 확인해보자
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) {
// 자기자신의 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;
}
}
}
두 Student 클래스를 비교한다고 가정했을 때,
Student 클래스에 Comparable interface를 implements 해주고
T형 객체에 비교 대상인 Student를 작성해주었다.
그리고 compareTo method를 override하여 해당 부분에
자기자신의 나이 age와 비교 매개변수 o의 age를 비교하도록 작성하였다.
compareTo method를 보면 int값을 반환하는 것을 볼 수 있다.
두 나이(값)를 비교해서 정수(양수, 0, 음수)를 반환하도록 하는 것이다.
그럼 무슨 기준으로 정수를 반환하는 것일까?
Comparable은
자기자신과 비교 매개변수 객체를 비교한다고 하였다.
만약
내가 5를 가지고 있고, 비교 값이 3이라고 하면
나는 비교 값보다 2만큼 큰 것이다. (+2, 양수)
내가 5를 가지고 있고, 비교 값이 5이라고 하면
나는 비교 값과 같은 것이고. (0)
내가 5를 가지고 있고, 비교 값이 7이라고 하면
나는 비교 값보다 2만큼 작은 것이다. (-2, 음수)
이 내용을 토대로 한다면
compareTo를 아래처럼 작성해도 무관하지만, 특수한 경우가 아니라면 굳이 추천하진 않는다
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) {
// 자기자신의 age가 o의 age보다 크다면 양수
if(this.age > o.age) {
return 1234;
}
// 자기 자신의 age와 o의 age가 같다면 0
else if(this.age == o.age) {
return 0;
}
// 자기 자신의 age가 o의 age보다 작다면 음수
else {
return -4321;
}
}
}
해당 코드는 잘 생각해보면 더 간단히 줄일 수 있다.
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) {
return this.age - o.age;
}
}
결국 자기자신과 비교 매개변수 객체를 비교하여 정수를 반환하면 되는 것이기 때문에
자기 자신에서 비교대상을 뺀다면(자기자신 - 비교대상)
자기자신 > 비교대상 = 양수
자기자신 == 비교대상 = 0
자기자신 < 비교대상 = 음수
길었던 3개의 조건식을 1줄로 바꿀 수 있게 된다.
그럼 처음부터 이 방식을 알려주지 왜 귀찮게 조건식을 먼저 알려 주었느냐?
이 방식에는 함정이 있다.
바로 뺄셈 과정에서 자료형의 범위를 넘어버리는 경우가 발생할 수 있기 때문이다.
코드를 예로들어 int의 자료형은
표현범위가 -2,147,483,648 ~ 2,147,483,647 이다.
만약 해당범위 밖으로 넘어가면 반대편의 값으로 넘어가게된다
ex) -2,147,483,648 -1 = 2,147,483,647 이 된다.
이렇게 되면 코드를 작성한 의도와는 다르게음수가 표현되지 않고 양수가 표현되어져서 비교 결과가 달라지게 된다.
그렇기 때문에 primitive 값의 표현 범위를 벗어나는 위 예처럼,
예외를 확신하기 어렵다면 3개의 조건식으로 대소비교를 해주는 것이 안전하다.
2. Comparator
Comparator는 두 매개변수 객체를 비교 한다고 하였다.
Comparator interface도 Comparable과 마찬가지로
public interface Comparator <T>{...} 라고 정의되어있는데
Comparator interface를 implements 할 때 T형 객체를 작성해주면 되는 것이다.
코드로 확인해보자
import java.util.Comparator; // import 필요
class Student implements 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) {
// o1의 학급이 o2의 학급보다 크다면 양수
if(o1.classNumber > o2.classNumber) {
return 1;
}
// o1의 학급이 o2의 학급과 같다면 0
else if(o1.classNumber == o2.classNumber) {
return 0;
}
// o1의 학급이 o2의 학급보다 작다면 음수
else {
return -1;
}
// return o1.classNumber - o2.classNumber;
}
}
처음에 설명한 것처럼,
Comparator는 compare(T o1, T o2) 처럼
두 매개변수 객체를 작성해주어야 한다.
두 매개변수 객체 중,
기준이 되는 객체(코드상 o1) 와 비교 대상 객체(o2)를 비교하도록 작성하였다.
Comparable 때처럼 조건식을 생략하고 간단히 작성도 가능하다.
물론 Comparator도 마찬가지로 overflow같은 예외의 경우가 있기 때문에 주의하여야 한다.
실제로 코드를 작성하여 비교하면 아래처럼 작성할 수 있다.
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
Student a = new Student(17, 2); // 17살 2반
Student b = new Student(18, 1); // 18살 1반
Student c = new Student(15, 3); // 15살 3반
// a객체와는 상관 없이 b와 c객체를 비교한다.
int isBig = a.compare(b, c);
if(isBig > 0) {
System.out.println("b객체가 c객체보다 큽니다.");
}
else if(isBig == 0) {
System.out.println("두 객체의 크기가 같습니다.");
}
else {
System.out.println("b객체가 c객체보다 작습니다.");
}
}
}
class Student implements 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) {
return o1.classNumber - o2.classNumber;
}
}
코드 자체가 어려울건 없다
다만 코드를 보면 Student a 가 전혀 사용되고 있지 않다.
물론 a를 생성하지 않고
b.compare(b,c) 와 같은 방식이나
비교대상군을 위한
Student comp = new Student(0, 0) 을 생성하여
comp.compare(b,c) 같은 식으로 사용하여도 상관은 없다.
하지만 해당 방식들은 일관성이 떨어질뿐더러
쓸모없는 변수 생성이 되기도 한다.
그럼 어떻게 해야될까?
바로 익명 객체(클래스)를 활용하면 된다.
바로 코드로 살펴보자
import java.util.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가 생성되기 때문에 클래스에서 Comparator을 구현 할 필요가 없어진다.
class Student {
int age; // 나이
int classNumber; // 학급
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
}
익명 객체의 경우 필요에 따라 main함수 밖에 정적(static) 타입으로 선언해도 되고, main안에 지역변수처럼 non-static으로 생성해도 된다.
static 방식을 채용하여 코드에 적용해보면 아래처럼 쓸 수 있다.
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
Student a = new Student(17, 2); // 17살 2반
Student b = new Student(18, 1); // 18살 1반
Student c = new Student(15, 3); // 15살 3반
// comp 익명객체를 통해 b와 c객체를 비교한다.
int isBig = comp.compare(b, c);
if(isBig > 0) {
System.out.println("b객체가 c객체보다 큽니다.");
}
else if(isBig == 0) {
System.out.println("두 객체의 크기가 같습니다.");
}
else {
System.out.println("b객체가 c객체보다 작습니다.");
}
}
public static Comparator<Student> comp = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.classNumber - o2.classNumber;
}
};
}
class Student {
int age; // 나이
int classNumber; // 학급
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
}
낭비되는 코드가 없는걸 확인할 수 있다.
익명 객체를 사용하면 좋은 점이 더 있다.
바로 익명 객체의 변수명만 달리해서 여러 비교 기준을 자유롭게 생성할 수 있다.
기존코드가 classNumber 만 비교하였다면
age 를 비교하는 익명객체를 간단히 추가할 수 있다.
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
Student a = new Student(17, 2); // 17살 2반
Student b = new Student(18, 1); // 18살 1반
Student c = new Student(15, 3); // 15살 3반
// 학급 기준 익명객체를 통해 b와 c객체를 비교한다.
int classBig = comp.compare(b, c);
if(classBig > 0) {
System.out.println("b객체가 c객체보다 큽니다.");
}
else if(classBig == 0) {
System.out.println("두 객체의 크기가 같습니다.");
}
else {
System.out.println("b객체가 c객체보다 작습니다.");
}
// 나이 기준 익명객체를 통해 b와 c객체를 비교한다.
int ageBig = comp2.compare(b, c);
if(ageBig > 0) {
System.out.println("b객체가 c객체보다 큽니다.");
}
else if(ageBig == 0) {
System.out.println("두 객체의 크기가 같습니다.");
}
else {
System.out.println("b객체가 c객체보다 작습니다.");
}
}
// 학급 대소 비교 익명 객체
public static Comparator<Student> comp = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.classNumber - o2.classNumber;
}
};
// 나이 대소 비교 익명 객체
public static Comparator<Student> comp2 = new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
};
}
class Student {
int age; // 나이
int classNumber; // 학급
Student(int age, int classNumber) {
this.age = age;
this.classNumber = classNumber;
}
}
마치며
사실 Comparable 과 Comparator는 정렬을 할때 많이들 쓰곤 한다.
하지만 본 글에서는 정렬에 관해서는 설명하지 않았다.
정렬까지 설명하려면 글이 더욱 길어지고
참고한 사이트를 그냥 그대로 복붙한 수준밖에 안될 것 같아서 작성하지 않았다.
(사실 예시코드나 대부분의 설명도 참고한 사이트의 것을 간략하게 적었을 뿐이긴 하다)
최근 개발자가 알아두면 좋은 기술 블로그 운영 팁 5가지 라는 글을 읽었는데
해당 글에서
글의 주제를 벗어나지 않도록 주의하기 라는 내용을 보았다.
나도 글을 작성하다보면
이걸 설명하려면 저것도 설명해야되고,
저걸 설명하려면 이것도 설명해야하고 식의 꼬리물기식 글 작성이 될 때가 많았다.
이런 글 작성 방법이
나 자신에게는
더 좋은 글을 작성하기 위해 계속해서 파고들기 때문에
공부 방법으로 너무 좋다라는 생각을 했지만,
글을 읽게 되는 사람의 입장에서는
알고 싶은 지식 외에 너무나 많은 사전지식을 강요하진 않았나 라는 생각도 들었다.
앞으로는 최대한 글 주제에 맞게 글을 써보려고 노력하고
그에 맞는 정보도 제공하되 무관하거나 너무 깊어지는 내용은
내용을 덜어내거나 참조를 다는 방법을 이용하여 글을 작성하도록 해야겠다.
마지막으로 한번 더
해당 포스팅 내용은 내 글보단 아래 링크를 타고 들어가서 정독하는 것이
정확히 이해하고 쉽게 이해하기 좋을 것이다.
https://st-lab.tistory.com/243
'개발자의 삶 > Java' 카테고리의 다른 글
[Java] 메소드 참조(Method References)에 대하여 ( 이중콜론 :: ) (0) | 2023.06.05 |
---|---|
[Java] 람다식(Lambda Expression)과 스트림(Stream)에 대하여 - (2) (1) | 2023.06.01 |
[Java] 람다식(Lambda Expression)과 스트림(Stream)에 대하여 - (1) (0) | 2023.05.31 |
[Java] 오토박싱(Autoboxing), 언박싱(unboxing) (1) | 2023.05.27 |
[Java] 원시 타입(Primitive type), 참조 타입(Reference type), 래퍼클래스(Wrapper Class) (0) | 2023.05.26 |