[Java] Comparable과 Comparator

6720·2023년 1월 31일
0

이거 모르겠어요

목록 보기
7/38

Comparator, 그리고 Comparable

Comparator, 그리고 Comparable은 둘 다 인터페이스(interface)임. 인터페이스는 사용하고자 한다면 인터페이스 내에 선언된 메소드를 반드시 구현해야 함.

Comparator 문서에 함수가 많긴 하지만 Compare(T o1, T o2) 하나만 구현해주면 됨.

Comparable 문서에 있는 함수 CompareTo(T o)

이 둘이 사용되는 이유는 본질적으로 객체는 사용자가 기준을 정해주지 않는 이상 어떤 객체가 더 높은 우선순위를 갖는지 판단할 수 없기 때문에 그 '객체'를 비교할 수 있도록 해주는 역할을 함.

반드시 구현해야하는 메소드 중에 그 하나가 Compare인 이유는?

원래 인터페이스는 추상 메소드로 인터페이스를 구현할 때 class [이름] implements [인터페이스 이름]에서 함수 이름 가져다가 재정의(오버라이드) 하면서 가져오도록 함.
(인터페이스의 목적은 여기 선언해둔 메소드를 반드시 사용하도록 강제하기 위함임. EX) 자동차에 바퀴, 엔진 등이 필수로 있어야 하는 것처럼)

Java 8 이후부터는 인터페이스에서도 일반 메소드를 구현할 수 있도록 함.
거의 대부분이 defaultstatic으로 선언된 메소드였는데 이런 메소드들이 함수를 구현하고 있음을 확인할 수 있음.

기존 구현된 함수를 반환하거나 등, 함수의 내용이 {...} 블럭 안에 구현이 되어있는 것을 볼 수 있음.

즉, defaultstatic으로 선언된 메소드가 아니면 이는 추상메소드이니 반드시 재정의를 해줘야 한다는 뜻임. -> compare(T o1, T o2)만은 반드시 재정의를 해줘야 한다는 뜻.

(bool equals(Object obj) 메소드는 모든 객체의 최상위 타입(객체)인 Object 클래스에서 정의되어있기 때문에 구현이 강제되지 않음.)

+) defaultstatic의 차이: 재정의 가능 vs 재정의 불가능

차이점

[용도]

본질적으로 비교하는 것 자체는 같지만, 비교 대상이 다름.

  • Comparable: 자기 자신과 매개변수 객체를 비교하는 것
  • Comparator: 두 매개변수 객체를 비교하는 것

[import]

  • Comparable: lang 패키지에 있어서 별도로 불러올 게 없음.
  • Comparator: util 패키지에 있어서 불러와 줘야함. (java.util.Comparator)

Comparable

[정의]

자기 자신과 매개변수 객체를 비교함.

보면 interface Comparable<T> {…} 라고 되어있는데 <T>는 하나의 객체 타입이 지정 될 자리라고 생각하면 됨.

제너릭에 대한 설명 추가 예정

위 포스팅에 대한 설명을 간추리자면 제네릭(Generic)은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미함.

우리가 직접 <>안에 타입을 집어넣어 정의하는 것과 같음.

[형태]

public class Test1 implements Comparable<Type> {

	...

	@override
	public int compareTo(Type o) {
		... 비교 구현 ...
	}
}

interface를 사용하기 위해선 class에 implements해주면 됨. 그러면 Comparable<T> 인터페이스에 정의된 compareTo(T o)사용이 가능함.

이 상태에서 Test1이라는 class를 만들 때, Test1을 비교하기 위해 Comparable<T>를 implements 한 것을 알 수 있음.

★★★★★

다시 Comparable의 역할을 생각해야 함. Comparable은 자기 자신매개변수 객체를 비교하는 것임.

여기에서 자기 자신은 Test1으로 생성한 객체 자신이 될 것이고, 매개변수 객체는 Test1.compareTo(o)를 통해 들어온 파라미터 o가 비교할 객체가 되는 것임.

★★★★★

그렇다면 여기서 T라는 타입을 가지는 o는 어떤 타입이여야 할까?

Test1 객체와 또 다른 Test1 객체를 비교하고 싶다면 o 또한 Test1이 되어야 하지 않을까?

public class Test1 implements Comparable<Test1> {

	int index;
	int key;

	Test1(int index, int key) { // 객체의 형태가 무조건 이래야 하는거 아님!!!!
		this.index = index;
		this.key = key;	
	}

	public int compareTo(Test1 o) {
		if (this.index > o.index) return 1;
		else if (this.index == o.index) return 0;
		else return -1;
	} // compareTo 메소드의 반환 형태는 int 형태로 되어야 함.
}

[반환 형태]

compareToint 값을 반환하도록 되어있으므로 대소관계를 정수로 반환해야 한다는 것임.

만약 양수가 나오면 자기 자신이 더 큰 것, 음수가 나오면 상대가 더 큰 것, 0이 나오면 같은 것.

EX) 7과 n의 대소관계

  • n=3 → 양수 반환 (7-3 > 0)
  • n=7 → 0 반환 (7-7 == 0)
  • n=9 → 음수 반환 (7-9 < 0)

값을 반환할 때 양수나 음수의 특정값을 요구하는 것이 아니기 때문에 둘의 차를 구해 반환하도록 하면 될 것.

(return this.index - o.index) 이러면 굳이 기본적인 대소비교 내용을 넣을 필요가 없음.

→ 다만 이 결과가 int의 범위를 넘게된다면 overflow되므로 먼저 대소비교 내용을 넣은 후 1 0 -1을 넣는 편이 안전함.

Comparator

[정의]

두 매개변수 객체를 비교함.

즉, 자기 자신이 아니라 매개 변수로 들어오는 두 객체를 비교하는 것임.

Comparable과 마찬가지로 직접 <>안에 타입을 집어넣어 정의해야 함.

[형태]

public class Test1 implements Comparator<Type> {

	...

	@override
	public int compare(Type o1, Type o2) {
		... 비교 구현 ...
	}
}

이번에도 예를 들어서 Test1이라는 객체를 비교한다고 하자

public class Test1 implements Comparator<Test1> {

	int index;
	int key;

	Test1(int index, int key) {
		this.index = index;
		this.key = key;
	}

	public int compare(Test1 o1, Test1 o2) {
		if (o1.index > o2.index) return 1;
		else if (o1.index == o2.index) return 0;
		else return -1;
	}
}

Comparable과의 차이점은 자기 자신을 비교하는 것이 아닌 매개 변수로 받은 o1과 o2이라는 Test1 객체를 서로 비교하는 것임.

EX) main에서 compare 사용하기

import java.util.Comparator;
 
public class Test {
	public static void main(String[] args)  {
 
		Student a = new Student(17, 2);
		Student b = new Student(18, 1);
		Student c = new Student(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; // 이렇게 하면 under·overflow 발생할 수 있음.
	}
}

a.compare을 사용하여 b와 c를 비교하지만 정작 a 객체와는 관련없이 두 객체의 비교 값을 반환하게 됨.

만약 a도 비교 대상에 넣고 싶다면 a.compare(a, b) 이런식으로 매개 변수로 넣어주면 됨.

하지만 조금은 비효율적이면서 일관성이 떨어지는 구조라고 느낄것임.

왜냐하면 a.compare를 쓴다고 해서 a 객체가 매개 변수로 들어가지 않는 이상 관련이 없다는 것 때문임.

Comparator 비교 기능만 따로 두기 위해선 익명 객체*(클래스)를 활용하면 됨. 이런 익명 객체는 특정 구현 부분만 따로 사용한다거나, 부분적으로 기능을 일시적으로 바꿔야 할 경우가 생길 때 사용함.

익명 객체는 이름이 정의되지 않은 객체임.

위에서 봤던 Test1은 이름이 Test1이라는 뜻임.

import java.util.Comparator;
 
public class Test {
	public static void main(String[] args) {
    
		// 익명 객체 구현 1 (main 내부)
		Comparator<Student> comp1 = new Comparator<Student>() {
			@Override
			public int compare(Student o1, Student o2) {
				return o1.classNumber - o2.classNumber;
			}
		};
	}
 
	// 익명 객체 구현 2 (main과 같은 위치)
	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;
	}
 
}
int result = comp1.compare(b, c);
// 실행 방법

*익명 객체

익명? 어디서 들어보지 않았나? 람다에서 들어봤을 것임.

람다는 익명 함수였음. 이름을 따로 정의하지 않고 실행할 수 있는 함수가 람다였음.

그렇다면 익명 객체도 비슷할 것임.

public class Main {
	public static void main(String[] args) {
//==============================
		Test1 a = new Test1() {
			@override
			int get() { return x; }
		}
//==============================
	}
}

class Test1 {
	int x = 1;
  int y = 2;

	int get() {
		return y;
	}
}

객체를 구현한다는 것은 변수를 선언하고, 메소드를 정의하여 하나의 클래스(객체)로 만든다는 것임.

조금 더 자세히 말하자면, interface를 implements하여 interface의 함수를 재정의하거나, class를 extends하여 부모의 함수, 필드를 사용 또는 재정의 하는 행위가 모두 객체를 구현한다는 뜻임.

위의 방식대로 객체를 구현하는 방식은 모두 이름이 존재한다는 것이었음.

하지만 Test1 a를 보면 class Test1의 함수인 get()을 재정의했음. 결국 a는 Test1을 상속받은 하나의 새로운 객체가 됐다는 것임. 그럼에도 이름은 정의되고 있지 않음.

+) Comparable과 익명 객체

사실 Comparable을 굳이 익명 객체로 사용할 필요는 없음. 왜냐하면 자기 자신과 하나의 매개 변수와의 비교기 때문임.

만약 익명 객체로 Comparable을 생성했다고 할 때, 결국 자기 자신은 Test1 객체가 아닌 익명 객체라는 것임.

비교를 하기 위해선 둘의 타입이 같아야 하는데 Test1과 익명 객체의 비교가 불가능 해진다는 것임.

정렬의 관계

자바에서는 오름차순이 기본 설정임.

이는 o1이 o2보다 작아야 한다는 뜻이며 o1-o2는 음수여야 한다는 뜻임. 반대로 양수가 나온다면 o2가 더 작은 것으로 판단하여 o2를 o1의 앞으로 옮길 것임.

다시 정리하자면 (o1 - o2)

  • 음수: 교환 X
  • 양수: 교환 O

가 될 것임.

방금 말했지만 오름차순은 자바 정렬에서의 기본 설정이기 때문에 별 걱정할 필요 없지만, 내림차순의 경우는 말이 달라짐.

만약에 음수가 나오면 교환을 해주면 되고 양수가 나오면 교환을 안해주면 됨.

원래 다음과 같았던 오름차순 반환 값을

public int compareTo(Test t) {
	return this.value - t.value;
}

public int compare(Test t1, Test t2) {
	return t1.value - t2.value;
}

이렇게 반대로 바꿔주면 된다는 뜻임.

public int compareTo(Test t) {
	return t.value - value.value;
}

public int compare(Test t1, Test t2) {
	return t2.value - t1.value;
}

결론

사실 여러 코딩테스트를 풀어오면서 확인할 수 있었겠지만 간단하게 정렬하는 과정에서는 생각외로 Comparable이나 Comparator가 잘 쓰이지 않음.

Comparable이나 Comparator는 보통 객체를 비교할 때 쓰여서 그런 것 같음.

참고 링크

profile
뭐라도 하자

0개의 댓글