본문 바로가기
자료구조와 알고리즘/정렬

[JAVA] - Comparable과 Comparator의 차이 및 이해

by onggury 2023. 2. 5.

 

  우리는 무언가를 비교할 때 부등호를 이용해서 하죠.

 

예를들어 10과20을 비교할 때, 우리는 무엇이 큰지를 판단 할 수 있고 컴퓨터도 <, =, > 와 같은 비교연산자를 통해 바로 결과를 알려주죠.

 

그런데, 자바는 객체지향 언어입니다. 물론 그 속에 어떤 정수나, 실수 같은 값들을 연산할 일도 분명 많이 있지만, 객체끼리의 비교는 어떻게 할까요?

 

 

클래스에는 다양한 인스턴스 변수가 있습니다. 만약 Student 라는 클래스에 나이(age)와 반(classNumber)가 있다고 가정해 봅시다. 그리고 생성자를 통해 나이와 반을 매개변수로 받습니다.

 

 

class Student {
    int age;
    int classNumber;

    Student(int age, int classNumber) {
        this.age = age;
        this.classNumber = classNumber;
    }
}

 

 

 

만약 Student라는 a와 b라는 두개의 래퍼런스 변수로 두개의 클래스를 생성했다고 가정해 봅시다.

 

a는 나이 20, 반 1반을 매개변수로 넘겨주고

b는 나이 10, 반 2반을 넘겨줍니다.

 

둘중 뭐가 큰가요?

 

 

 

뭐가 큰지 답을 내신 분들은 무의식 중에 어떤 기준을 갖고 결정을 내리신 겁니다.

예를들어 나이의 값만 보고 'a가 크네', 혹은 반만 보고 'b가 더 크네'  라고 말이죠

 

 

 

이처럼 객체는 어떤 기준값이 있어야 비교가 가능합니다. 그 기준은 우리가 결정하는거죠. new 키워드로 생성된 인스턴스 그 자체로는 뭐가 크다 작다를 비교할 수가 없어요.

 

이 기준을 세우고 비교하는데 필요한것이 바로 Comparable 과 Comparator 입니다.

 

 


 

 

1. Comparable 과 Comparator란?

 

Comparable 과 Comparator은 인터페이스입니다.

 

인터페이스의 개념은 추상 메서드들의 집합니다.

 

추상 메서드는 간단히 말해서 정의되지 않은 메서드이죠. 마치 '자동차의 바퀴를 만들어라' 라고 말만 해두고 어떤 형태, 모양, 색깔인지는 정하지 않고 상속받은 자식 클래스에게 '결정권은 너에게 주마' 라고 던저주는거죠. 심지어 거절도 못해요. 무조건 정의해야 해요.

 

 

 

정리하자면

 

Comparable, Comparator은 인터페이스이며
이 인터페이스 안에 정의된 추상클래스를
반드시 재정의 해야한다.

 

 

라고 볼 수 있겠죠.

 

 

 

그러면 Comparator와 Comparable의 API 문서를 봅시다.

 

 

https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html#method.summary

 

Comparable (Java Platform SE 8 )

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method. Lists (and arrays) of o

docs.oracle.com

 

 

https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html#method.summary

 

Comparator (Java Platform SE 8 )

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. In the foregoing description, the notation sgn(expression) designates the mathematical s

docs.oracle.com

 

 

 

살펴보시면,

 

Comparable은 compareTo(T o) 라는 추상메서드가 있고

Comparator은 다양한 메서드가 있죠...

그런데 Comparator의 메서드들을 잘 보시면, 거의 다 defalt, static 입니다.

그리고 abstract 메서드만 보시면, compare(T o1, T o2) , equals(Object obj) 두개만 있죠. 그런데 equals의 설명을 보면 재정의 하지 않는것이 안전하다고 되어있네요? 그 이유에 대해선 좀 더 공부를 해봐야 할 것 같네요... 그래서인지 equals 메서드를 재정의 하지 않아도 컴파일 오류가 안납니다.

 

아무튼 중요한건 compare(T o1, T o2)는 필히 재정의 해야 한다는 점입니다.

 

 

 

정리하자면 이렇습니다.

 

Comparable은 compareTo(T o)를
Comparator는 compare(T o1, T o2)를
재정의 해야 한다.

 

 

여담으로 각 인터페이스의 상태를 보면, <T>라는게 있습니다. 이는 제너릭으로 type을 뜻합니다.

이 외에도 제너릭은 <E>, <K>, <V> 등등 다양한데 우선 <T>는 type이구나 라고 이해하고 넘어가셔도 이 글을 이해하는데 지장 없습니다.

 

 

 


 

 

2. 그렇다면 둘의 차이는 무엇인가요?

 

 

두개의 용도는 객체끼리의 비교라는것은 알겠습니다.

 

그런데 각각의 용도는 뭘까요? 

 

각 인터페이스의 추상메서드 매개변수를 보면 compareTo는 하나를 받고, compare는 두개를 받죠.

 

 

바로 정리하자면 이렇습니다.

 

Comparable은 자기 자신과 매개변수로 들어오는 객체와의 비교
Coomparator는 자기 자신이 뭐던간에 매개변수 o1, o2로 들어오는 두 객체를 비교

 

 

 


 

 

3. Comparable

 

 

본격적으로 하나하나 알아보겠습니다.

 

 

Comparable의 추상메서드는 compareTo(T o) 이죠.

그리고 Comparable은 자기 자신과 매개변수로 들어오는 객체 o를 비교합니다.

 

 

예를들어 자신의 Student객체와 다른 Student 객체를 비교하고 싶으면 이렇게 정의할 수 있겠죠.

 

public class Student implements Comparable<Student> { ... }

 

 

 

그리고 compareTo(T o)를 재정의 해줘야 하죠?

 

여기서부터는 바로 예시와 함께 보겠습니다.

 

위 예시를 가져와서 Student라는 클래스가 있고 인스턴스 변수를 나이(age), 반(classNumber)를 선언합니다.

그리고 생성자를 통해 각 값을 받아옵니다.

 

 

그리고 나이(age)를 기준으로 compare 메서드를 통해 자기 자신의 Student 클래스와 매개변수 o로 받아온 다른 Student 클래스비교합니다.

 

 

class Student implements Comparable<Student> {
	int age;
	int classNumber;
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	public int compareTo(Student o) {
		if(this.age > o.age) {
			return 1;
		}
		else if(this.age == o.age) {
			return 0;
		}
		else {
			return -1;
		}
	}
}

 

 

compareTo 메서드를 보면 자기 자신의 나이(age)와 o로 받아온 나이(age)를 비교함으로써 각각 반환이 1, 0 -1 이란 정수값을 반환합니다. 

 

이는 무엇을 뜻할까요?

 

 

간단합니다. 만약 우리가 5란 숫자를 가지고 있고 비교할 대상이 3 혹은 8이라고 가정해봅니다.

 

나의 5와 상대방 3을 보니, 내가 +2 만큼 더 크죠.

나의 5와 상대방 8을 보니, 내가 -3 만큼 크죠.

 

 

즉, 나와 상대방을 비교해서 양수, 음수, 혹은 0(같음) 을 판단하는 겁니다.

 

 

그러면 1, 0, -1이 아니더라도 되겠네요?
return 1837174, return -474637373 라고 대충 숫자 때려박아도 되겠네요!!
양수, 음수, 0 이기만 하면 되니까요!!

 

 

 

 

물론 됩니다. 그런데 굳이 그렇게 쓸 이유가 있을까요?

혼자 개발하는것이라면 상관 없지만, 팀원과의 프로젝트라면 좀 불편하지 않을까요??

 

 

 

그러면 어짜피 나이(age)를 기준으로 두고 비교하니까
각 클래스의 나이를 빼기 연산을 통해 그 결과를 return 해도 되겠네요!

 

 

 

이것도 물론 됩니다.

 

class Student implements Comparable<Student> {
	int age;
	int classNumber;
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	public int compareTo(Student o) {
		
        	return this.age - o.age;
    	}
}

 

 

실제로 이렇게 작성해도 동작은 됩니다.

 

 

 

그런데 주의해야 할 점이 있습니다.

 

만약, 저 return 값이 해당 타입(현재 저 코드에는 int)의 범위를 벗어난다면 어떻게 될까요??

 

컴파일 오류는 나지 않습니다. 다만, Underflow, Overflow 로 인해 원하던 결과 값이 안나올 수도 있겠죠.

Underflow는 타입 범위의 하한선을 넘는것이고 Overflow는 상한선을 넘는것이라 보면 되겠습니다.

 

 

따라서 그냥 1, 0, -1로 표기하는게 좋을 듯 합니다.

 

 

 

 

그러면 이제 메인에서 예시를 보며 어떻게 결과가 나오는지 보겠습니다.

 

 

 

class Student implements Comparable<Student> {
	int age;
	int classNumber;
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	public int compareTo(Student o) {
		return this.age - o.age;
	}
}


public class test {

	public static void main(String[] args) {
		
		Student a = new Student(18, 1);
		Student b = new Student(16, 2);
		
		int compareAge = a.compareTo(b);
		
		if(compareAge > 0) {
			System.out.println("a객체가 b객체보다 큽니다.");
		}
		else if(compareAge == 0) {
			System.out.println("두 객체의 크기가 같습니다.");
		}
		else {
			System.out.println("a객체가 b객체보다 작습니다.");
		}
        }
}

 

결과 : a객체가 b객체보다 큽니다.

 

 

메인에서 Student 객체를 래퍼런스 변수 a와 b를 통해 만들고 각각 new라는 키워드로 인스턴스를 만들었습니다.

 

 

a 래퍼런스 변수는 age = 18, classNumber = 1 라는 인스턴스 변수를 가지게 되었고

b 래퍼런스 변수는 age = 16, classNumber = 2 라는 인스턴스 변수를 가지게 되었습니다.

 

 

그리고 compareAge 라는 변수는 a 래퍼런스 변수로 불러와진 compareTo 메서드로 b 객체를 매개변수로 보냅니다.

그리고 compareTo 메서드로 각각 나이로 비교 하고 1, 0, -1을 반환하겠죠.

 

if문을 통해 그 값이 양수, 0, 음수인지를 판단하여 결과를 도출합니다.

 

 

 

직접 인스턴스 변수값을 변경해서 진행해보세요. 이해가 더욱 쉽습니다.

 

 

 

 


 

 

 

4. Comparator

 

 

Comparable을 이해하셨다면, Comparator는 쉽습니다.

 

Comparator가 뭔지 다시 짚고 넘어가자면

 

자기 자신이 뭐던간에 매개변수 o1, o2로 들어오는 두 객체를 비교  합니다.

 

 

 

그러면 구현은 어떻게 할까요?

 

 

class Student implements Comparator<Student> {
	int age;
	int classNumber;
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	public int compare(Student o1, Student o2) {
		if(o1.age > o2.age) {
			return 1;
		}
		else if(o1.age == o2.age) {
			return 0;
		}
		else {
			return -1;
		}
	}
}

 

 

Comparable의 compare 메서드와 거의 비슷합니다.

 

 

Student 클래스에 Comparator를 구현하고,

 

생성자를 통해 인스턴스 변수를 초기화 시킨 후,

 

compare 메서드를 재정의 함으로써 비교를 하는데,

 

Comparable 인터페이스의 compareTo 메서드와는 달리

 

자기 자신과 전혀 상관없는 Student 객체 o1과 o2를 받아와서 비교를 한 후 양수, 0, 음수를 반환합니다.

 

 

 

여기서도 주의할 점이 있습니다.

 

Comparable은 java.lang 패키지 소속이라 따로 import 할 필요가 없지만,

Comparator는 java.util 패키지라 반드시 import를 해줘야 합니다.

 

 

 

그러면 메인을 포함해서 전체 코드를 보겠습니다.

 

 

import java.util.Comparator

class Student implements Comparator<Student> {
	int age;
	int classNumber;
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
	
	public int compare(Student o1, Student o2) {
		if(o1.age > o2.age) {
			return 1;
		}
		else if(o1.age == o2.age) {
			return 0;
		}
		else {
			return -1;
		}
	}
}



public class test {

	public static void main(String[] args) {
		
		Student a = new Student(15, 1);
		Student b = new Student(18, 2);
		Student c = new Student(17, 3);
		
		int compareAge = a.compare(b, c);
		
		if(compareAge > 0) {
			System.out.println("b 객체가 c 객체보다 큽니다.");
		}
		else if(compareAge == 0) {
			System.out.println("두 객체의 크기가 같습니다.");
		}
		else {
			System.out.println("b 객체가 c 객체보다 작습니다.");
		}
		
	}

}

 

결과 : b 객체가 c 객체보다 큽니다.

 

 

 

이것도 직접 타이핑하시면서 값을 바꿔가며 결과를 내보시면 이해가 금방 되실겁니다.

또한 코드 설명도 compareTo와 똑같기때문에 넘어가겠습니다.

 

 

 


 

 

다음번엔 이를 활용해서 객체들을 정렬하는 방법을 알아보겠습니다.

 

 

우리가 값을 정리할 땐, 버블 정렬 / 선택 정렬 / 삽입 정렬 등을 통해 서로의 값을 비교연산자로 비교해가며 정렬을 진행하지요.

 

 

기본적으로 오름차순을 디폴트로 두고 설명이 나옵니다.

 

오름차순을 보면 항상 앞의 값이 뒤에 값보다 작죠.

 

어떤 기준을 두고 객체를 비교할 때 o1 보다 o2가 더 크면 뭘 반환하던가요? 음수를 반환하죠.

 

이를 이용해서 객체들을 정렬합니다.

 

 

다음 글에 정리해서 적어보도록 하겠습니다.