[JAVA] Comparable 과 Comparator의 이해

Benjamin·2023년 5월 3일
0

JAVA

목록 보기
21/28

우선, Comparable과 Comparator는 모두 인터페이스(interface)이다.
즉, 이를 사용하고자 한다면 인터페이스 내에 선언된 메소드를 반드시 '구현'해야한다는 것이다.

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

compareTo(T o) 메소드 하나가 선언되어있는 것을 볼 수 있다. 이 말은 우리가 만약 Comparable을 사용하고자 한다면 compareTo 메소드를 재정의(Override/구현)을 해주어야 한다는 것이다.

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

메소드가 많지만, 우리가 실질적으로 구현해야하는것은 compare(T o1, T o2) 다.

Comparable과 Comparator

두 인터페이스가 하는 일은 객체의 정렬이 아닌, 객체의 비교이다.
primitive type은 자바 자체에서 제공되기에 부등호로 쉽게 비교가 가능하다.

만약 새로운 클래스 객체를 만들어 비교하고자 한다면 어떻게 될까? 예로들어 학생의 나이와 학급 정보를 갖고있는 클래스를 만든다고 가정해보자.

class Student {
 
	int age;			// 나이
	int classNumber;	// 학급
	
	Student(int age, int classNumber) {
		this.age = age;
		this.classNumber = classNumber;
	}
}

Student라는 객체를 어떻게 비교할 것인가?
나이기준? 학급기준?

이 부분이 포인트이다! 사용자가 기준을 정해주지않는 이상 어떤 객체가 더 높은 우선순위를 갖는지 판단할 수 없다.

이런 문제점을 해결하기 위해 바로 Comparable 또는 Comparator가 쓰이는 것이다.

차이점

비교대상!

Comparable의 compareTo(T o) 메소드는 파라미터(매개변수)가 한 개이고, Comparator의 compare(T o1, T o2) 메소드는 파라미터가 왜 두 개인 것일까?

  • Comparable = "자기 자신과 매개변수 객체를 비교"하는 것
    자기 자신과 파라미터로 들어오는 객체를 비교

  • Comparator = "두 매개변수 객체를 비교"한다는 것
    자기 자신의 상태가 어떻던 상관없이 파라미터로 들어오는 두 객체를 비교

Comparable

interface Comparable<T> { ... }

<T> = 하나의 객체 타입이 지정 될 자리

클래스를 만들 때 기본적으로 사용방법은 아래와 같다.

  public class ClassName implements Comparable<Type> { 
 
/*
  ...
  code
  ...
 */
 
	// 필수 구현 부분
	@Override
	public int compareTo(Type o) {
		/*
		 비교 구현
		 */
	}
}

compareTo() 메소드가 바로 우리가 객체를 비교할 기준을 정의해주는 부분이다.

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

아까 Student 클래스를 비교하자고 했는데, 이 예시로 다시 보자.

Student 클래스에 Comparable 을 implements 해야한다. 그리고 <> 사이에 들어갈 타입은 무엇일까? Student 객체와 또 다른 Student 객체를 비교하고 싶다면, <> 사이에 들어갈 타입 또한 Student가 되어야한다.

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) {
		/*
		 * 비교 구현
		 */
	}
}

이제 compareTo를 비교해야한다.

만약 나이를 기준으로 비교(대소 관계)를 하고자 한다면 어떻게 하면 될까?
자기 자신의 age(나이)와 매개변수로 들어온 o의 age(나이)의 값을 비교하면 된다.

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;
		}
	}
}

compareTo 메소드의 반환값이 int이다. 즉 값을 비교해서 정수를 반환하는것이다.
무슨 기준으로 1,0,-1을 반환하는걸까?

한 번 생각해보자. 우리는 "자기 자신"과 "상대방"을 비교하는 것이다. 즉, 자기 자신을 기준으로 삼아 대소관계를 파악해야 한다.

만약 내가 갖고 있는 값이 7라고 가정해보자. 그리고 상대방은 3이라고 가정한다면, 나 자신은 상대방보다 값이 4만큼 크다.

반대로 상대방이 9을 갖고 있다고 가정하면, 나는 상대방보다 2만큼 작다. 즉, -2 만큼 크다는 것이다.

한 마디로 자기 자신을 기준으로 상대방과의 차이가 얼마나 나느냐다.

즉, 이렇게 얼마나 큰지(차이 = 1,0,-1)를 대표적으로 나타내기위해 1,0-1을 썼던것이다.
따라서 꼭 1,0,-1이 아니라 양수,0,음수로 표현해도 된다. 또는 그냥 차이 값을 그대로 반환해도 된다.

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보다 크다면 양수가 반환 될 것이고,
		 * 같다면 0을, 작다면 음수를 반환할 것이다.
		 */
		return this.age - o.age;
	}
}

Test

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반
		
		
		int isBig = a.compareTo(b);	// a자기자신과 b객체를 비교한다.
		
		if(isBig > 0) {
			System.out.println("a객체가 b객체보다 큽니다.");
		}
		else if(isBig == 0) {
			System.out.println("두 객체의 크기가 같습니다.");
		}
		else {
			System.out.println("a객체가 b객체보다 작습니다.");
		}
		
	}
 
}
 
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;
	}
}

위 코드에서 this는 a객체 자신을 의미하고, o는 b객체를 의미하게 된다.

주의할 점!

두 수의 차를 이용해 양수,0,음수로 구분하여 구했다. 따라서 이 과정에서 자료형의 범위를 넘어버리는 경우가 발생할 수 있다!
예를들어, '-2,147,483,648 - 1' = '-2,147,483,649' 인데, 이 수는 int 자료형에서 표현할 수 없는 수로 2,147,483,647으로 int형의 최댓값으로 반환한다.
이렇게 주어진 범위의 하한선을 넘어버리는 것을 'Underflow' 라고 한다.

반대로 주어진 범위의 상한선을 넘어버리는 것을 Overflow라고 한다.

예로들어 다음과 같은 두 값이 있다고 해보자.

o1 = 1, o2 = -2,147,483,648

그리고 두 수를 위 처럼 return o1 - o2; 형식으로 하면 어떻게 될까?

우리는 '음수'가 나올 때 선행(자기 자신) 원소가 후행 원소보다 작다고 판단했다. 하지만 위 연산을 자세히 보자.

1 - (-2,147,483,648) = 2,147,483,649 이 되어야 하지만 -2,147,483,648 이 되어 음수값이 나와버린다. 그러면 1인 o1이 -2,147,483,648인 o2보다 작다는 상황이 발생해버린다.

따라서 compareTo나 이후 나오는 compare을 구현할 때 대소비교시 이런 overflow 혹은 underflow가 발생할 여지있는지 반드시 확인 후 사용해야한다.

Comparator

자기 자신이 아니라 파라미터(매개 변수)로 들어오는 두 객체를 비교한다.

interface Comparator<T> { ... }
compareTo와 마찬가지로 는 하나의 객체 타입이 지정될 자리다.

사용방법은 아래와같다.

import java.util.Comparator;	// import 필요
public class ClassName implements Comparator<Type> { 
 
/*
  ...
  code
  ...
 */
 
	// 필수 구현 부분
	@Override
	public int compare(Type o1, Type o2) {
		/*
		 비교 구현
		 */
	}
}

compare() 메소드가 바로 우리가 객체를 비교할 기준을 정의해주는 부분이다.

compareTo와 똑같이 Student를 예시로 들어보자

import java.util.Comparator;	// import 필요
public class ClassName implements Comparator<Student> { 
 
/*
  ...
  code
  ...
 */
 
	// 필수 구현 부분
	@Override
	public int compare(Student o1, Student o2) {
		/*
		 비교 구현
		 */
	}
}

기본적으로 compare메소드 매커니즘 자체는 compareTo와 같다.

다만, 자기 자신과 비교되느냐 안되느냐의 차이일 뿐이다.

이번에는 학급을 기준으로 정의해보자

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;
		}
	}
}

구체적으로 말하자면 Comparable의 compareTo는 선행 원소가 자기 자신이 되고, 후행 원소가 매개 변수로 들어오는 o 가 되는 반면에, Comparator의 compare는 선행 원소가 o1이 되고, 후행 원소가 o2가 된다.

이 말은, o1과 o2를 비교함에 있어 자기 자신은 두 객체 비교에 영향이 없다는 뜻이다.

  • a.compare(b,c) :b,c를 비교하며 a 객체와 관련없이 두 객체의 비교값을 반환함
    a.compare 메소드에서 a와 비교하고 싶다면 a.compare(a, b);로 작성한다.

즉, 객체 자체와는 상관 없이 독립적으로 매개변수로 넘겨진 두 객체를 비교하는 것이 포인트다.

코드도 앞서했듯이 간단히 구현할 수 있다.

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의 classNumber가 o2의 classNumber보다 크다면 양수가 반환 될 것이고,
		 * 같다면 0을, 작다면 음수를 반환할 것이다.
		 */
		return o1.classNumber - o2.classNumber;
	}
}
 

Comparator또한 Underflow, Overflow가 발생할 수 있으므로 주의해야한다!

활용

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반
			
		//          ⋁
		int isBig = a.compare(a, b);
        //           ⋁
		int isBig2 = a.compare(b, c);
		//           ⋁
		int isBig3 = b.compare(a, c);
		
	}
}
 
// Student class 생략

Comparator를 통해 compare 메소드를 사용려면 결국에는 compare메소드를 활용하기 위한 객체가 필요하게 된다.

무슨 말인가 하면, a, b, c 객체가 생성되어있고, 이들을 비교를 하고 싶다면 어느 한 객체를 통해 compare메소드를 사용해야한다는 것이다.

보면 메소드를 호출하기 위한 대상(⋁ 표시 된 부분)은 사실 a이건, b이건, c이건 어떤 객체를 통해 호출하던 상관이 없다.
이 말을 조금 돌려서 생각해보면, 일관성이 떨어진다는 것이다.

물론 비교만을 위해 Student 객체를 하나 더 생성해주는 방법도 있다.

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반
		Student comp = new Student(0, 0);	// 비교만을 위해 사용할 객체
			
		//           ⋁
		int isBig = comp.compare(a, b);
        //            ⋁
		int isBig2 = comp.compare(b, c);
		//            ⋁
		int isBig3 = comp.compare(a, c);
		
	}
}

위 처럼 하면 우리가 Student클래스에서 변수로 두고 있던 age와 classNumber 변수는 굳이 쓸모가 없음에도 생성이 되어버린다는 단점이 있다.

우리가 원하는 것은 Comparator 비교 기능만 따로 두고 싶은 것인데, 이를 위해서는 "익명 객체(클래스)를 활용한다"

  • 익명객체 = 이름이 정의되지 않은 객체

익명객체

자바는 객체지향 언어다. 그래서 여러분이 어떠한 객체를 만든다고 한다면 class를 생성하여 이름을 정의한다. 그동안 우리가 예시로 들었던 Student 또한 Student라는 이름으로 정의된 객체다.

그럼 이름이 정의되지 않는다는 것은 무엇일까?
우리가 클래스를 생성할 때 class 키워드 다음에 이름을 정의했다. 하지만, 이름 없이 class를 정의할 수 있는가? 불가능 하다.

하지만, 우리의 고민처럼 특정 구현 부분만 따로 사용한다거나, 부분적으로 기능을 일시적으로 바꿔야 할 경우가 생길 때가 있다. 이럴 때 사용할 수 있는 것이 바로 익명객체이다.

public class Anonymous {
	public static void main(String[] args) {
	
		Rectangle a = new Rectangle();
		
		// 익명 객체 1 
		Rectangle anonymous1 = new Rectangle() {
		
			@Override
			int get() {
				return width;
			}
		};
		
		System.out.println(a.get());
		System.out.println(anonymous1.get());
		System.out.println(anonymous2.get());
	}
	
	// 익명 객체 2
	static Rectangle anonymous2 = new Rectangle() {
		
		int depth = 30;
		@Override
		int get() {
			return width * height * depth;
		}
	};
}
 
class Rectangle {
	
	int width = 10;
	int height = 20;
	
	int get() {	
		return height;
	}
}

보통의 경우 다음과 같이 생성할 것이다.

Rectangle a = new Rectangle();

하지만, 익명객체의 경우는 다음과 같이 생성된다.

Rectangle a = new Rectangle() { //...구현부...// };

왜 익명 객체인 것일까? 얼핏보면 같은 객체 생성 방식인 것 같지만, 우리가 보아야 할 것은 { } 블럭 안의 구현부이다.

객체를 구현한다는 것은 무엇일까? 바로 변수를 선언하고, 메소드를 정의하며 하나의 클래스(객체)로 만든다는 것을 의미한다.

쉽게 생각해보면 위 Rectangle 클래스처럼 일반적인 클래스 구현 방식과, interface 클래스를 implements 하여 interface의 메소드를 재정의하거나, class 를 상속(extends)하여 부모의 메소드, 필드를 사용 또는 재정의 하는 것들 모두 객체를 구현하는 것이다.

이 때, 구현을 하는 클래스들은 모두 '이름'이 존재한다.

그러나 한 번 Rectangle anonymous2 = new Rectangle() {...} 이 부분을 한 번 봐보자. 구현부에서 분명히 변수를 선언하기도 하고, Rectangle 클래스의 메소드 get()을 '재정의(Override)'를 했다.

즉, 쉽게 생각하여 'Rectangle을 상속받은 하나의 새로운 class라는 것이다.'

분명 새로운 class인데 이름이 정의되지 않고 있다. (annoymous1 객체 또한 마찬가지)

Rectangle이 아닌가요? 라고 생각할 수 있지만 아니다. 한 번 두 코드를 비교해보자.

public class Anonymous {
	public static void main(String[] args) {
 
		Rectangle a = new Rectangle();
		ChildRectangle child = new ChildRectangle();
 
		System.out.println(a.get());		// 20
		System.out.println(child.get());	// 10 * 20 * 40
	}
}
 
class ChildRectangle extends Rectangle {
	
	int depth = 40;
	
	@Override
	int get() {
		return width * height * depth;
	}
}
 
class Rectangle {
 
	int width = 10;
	int height = 20;
 
	int get() {
		return height;
	}
}

위 코드는 Rectangle 이라는 클래스를 상속받아 ChildeRectangle 이라는 이름으로 정의 된 자식 클래스가 있다.
각 클래스는 a와 child 란 변수 명으로 객체를 담고 있다.

다음 익명 객체를 사용한 코드를 한 번 보자.

public class Anonymous {
	public static void main(String[] args) {
 
		Rectangle a = new Rectangle();
 
		Rectangle anonymous = new Rectangle() {
			int depth = 40;
			@Override
			int get() {
				return width * height * depth;
			}
		};
 
		System.out.println(a.get());			// 20 
		System.out.println(anonymous.get());	// 10 * 20 * 40
	}
}
class Rectangle {
 
	int width = 10;
	int height = 20;
 
	int get() {
		return height;
	}
}

앞서 본 상속받아 ChildRectangle 클래스를 만든 것과 같지만, 이 코드는 이름이 정의되어있지 않고, anonymous라는 이름으로 객체만 생성되어 있다.

이렇게 클래스 이름으로 정의되지 않는 객체를 바로 익명 객체라 하는 것이다.

이는 거꾸로 말하면, 이름이 정의되지 않기 때문에 특정 타입이 존재하는 것이 아니기 때문에 반드시 익명 객체의 경우는 상속할 대상이 있어야 한다는 것이다.

이 때, 상속이라 함은 class의 extends 뿐만 아니라 interface의 implements 또한 마찬가지다.

public class Anonymous {
	public static void main(String[] args) {
 
		Rectangle a = new Rectangle();
		
		Shape anonymous = new Shape() {
			int depth = 40;
			
			@Override
			public int get() {
				return width * height * depth;
			}
		};
 
		System.out.println(a.get());			// Shape 인터페이스를 구현한 Rectangle
		System.out.println(anonymous.get());	// Shape 인터페이스를 구현한 익명 객체
	}
 
}
 
class Rectangle implements Shape {
	int depth = 40;
	
	@Override
	public int get() {
		return width * height * depth;
	}
}
 
interface Shape {
 
	int width = 10;
	int height = 20;
 
	int get();
}

이제 다시 본론으로 가보자.

우리가 원하는 것은 무엇이었을까? 바로 Comparator 의 기능만 사용하고 싶은 것이다.
즉, Comparator의 구현을 통해 compare 만 사용하고 싶은 것이라는 뜻이다.

앞서 익명객체에서 설명한 것을 적용해보자.

분명히 Comparator라는 interface는 존재한다. 이는 구현(상속)할 대상이 존재한다는 것이다. 이는 익명객체로 만들 수 있다는 것이다.

즉, 이름은 정의 되지 않지만, Comparator을 구현하는 익명객체를 생성하면 되는 것이다.

이 때, Comparator 구현은 이 전에 class Student implements Comparator { ... } 에서 구현했던 방식을 그대로 차용하면 된다.

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으로 생성해도 된다.

이렇게 외부에서 Comparator을 구현하는 익명객체가 생성되었기 때문에, Student 클래스 내부에서 우린 Comparator을 구현해줄 필요가 없어졌다.

즉, 이 전에 a.compare(b, c) 이런식이 아니라, 위에서 생성한 익명객체를 가리키는 comp 를 통해 comp.compare(b, c) 이런 식으로 해주면 된다는 것이다.

  • 장점
    = 클래스를 상속(구현)할 때, 이름만 다르게 하면 몇 개던 여러개를 생성할 수 있듯이, 익명 객체 또한 마찬가지다. 다만, 이름이 없을 뿐이라는 것이다.
    즉, 익명 객체를 가리키는 변수명만 달리하면 몇 개든 자유롭게 생성할 수 있다.
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도 익명객체로 할 수 있지 않나요?라고 물을 수 있다. 물론 생성이 가능은 하다.

하지만 좀만 고민해보면 굳이 Comparable을 익명객체로 생성 할 필요도 없고 오히려 복잡해진다. 이유는 단순하다.

만약 Comparable을 익명객체로 다음과 같이 생성했다고 가정해보자.

public static Comparable<Student> comp = new Comparable<Student>() {
	@Override
	public int compareTo(Student o1) {
		// 구현부
	}
};

그러면 Comparable에서 자기 자신은 무엇인가? 익명 객체가 될 것이다.
Student객체가 아니라는 것이다. 즉, 익명의 객체와 Student가 비교하는 것이지, Student와 Student가 비교되는 것이 아니라는 것이다.

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반
        
		/*
		 * Stduent b 객체는 comp의 30이랑 비교되는 것이다.
		 * 즉, a.compareTo(b) 처럼 서로 다른 객체에 대한 비교가 불가능하다.
		 */
		int classBig = comp.compareTo(b);
        
		
	}
	
	// 학급 대소 비교 익명 객체
	public static Comparable<Student> comp = new Comparable<Student>() {
		int a = 30;
		@Override
		public int compareTo(Student o) {
			return a - o.classNumber;
		}
	};
}

한 마디로 여러분이 자기 동일한 타입의 자신의 객체와 어떤 객체를 비교하고자 하면 Comparable을 익명객체로 선언한다고 한들, 동일한 타입 비교는 불가능하다는 것이다.

Comparable, Comparator 와 정렬

객체를 비교하기 위해 Comparable 또는 Comparator을 쓴다는 것은 곧 사용자가 정의한 기준을 토대로 비교를 하여 양수, 0, 음수 중 하나가 반환된다.

Java에서의 정렬은 특별한 정의가 되어있지 않는 한 '오름차순'을 기준으로 한다.

오름차순 정렬이란 무엇일까?
예로들어 {1, 3, 2} 배열이 있다고 가정해보자.

최종적으로 얻어야 할 배열 {1, 2, 3} 을 얻기 위해 정렬 알고리즘을 사용하게 될 것이다. 이 때, 정렬을 하기 위해 두 원소를 비교 하게된다.

index 0 원소와 index 1 원소를 비교한다고 가정해보자.

그럼 선행 원소인 1과 후행 원소인 3의 경우 대소관계는 1이 3보다 작다.

앞서 선행 원소와 후행 원소를 비교 할 때, 얼마큼 차이가 나는지를 반환한다고 헀다.
return o1 - o2; 를 한다면, 1-3 = -2로 '음수'가 나올 것이다.

이 때, 자바에서는 오름차순을 디폴트 기준으로 삼고 있다고 했다. 이 말은 선행 원소가 후행 원소보다 '작다'는 뜻이다.
즉, compare 혹은 compareTo를 사용하여 객체를 비교 할 경우 음수가 나오면 두 원소의 위치를 바꾸지 않는다는 것이다.
반대로 compare 혹은 compareTo를 사용하여 객체를 비교 할 경우 양수가 나오면 두 원소의 위치를 바꾼다는 것이다.

그럼 규칙을 일반화 할 수 있다.

[두 수의 비교 결과에 따른 작동 방식]

  • 음수일 경우 : 두 원소의 위치를 교환 안함
  • 양수일 경우 : 두 원소의 위치를 교환 함

예로들어 다음과 같은 객체를 만든 뒤 이를 배열로 만들어서 생성 된 객체 배열을 정렬하고자 한다.

public class Test {
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
	}
	
}
class MyInteger {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
}

MyInteger 클래스는 사용자가 만든 객체라 아직 비교 기준이 없다.

앞서 배웠던 비교 기준을 생성한다. 우리는 비교 기준을 설정하는 방법 두 가지를 배웠다. Comparable과 Comparator다.

Comparable을 사용한다면 MyInteger 클래스에 구현(implements)을 해야 할 것이다.

class MyInteger implements Comparable<MyInteger> {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
 
	// 자기 자신의 value을 기준으로 파라미터 값과의 차이를 반환한다.
	@Override
	public int compareTo(MyInteger o) {
		return this.value - o.value;
	}
	
}

그리고 나서 정렬 메소드로 가장 자주 쓰이는 Arrays.sort()메소드에 한 번 돌려서 테스트를 해보자

import java.util.Arrays;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr);
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
	
}
 
class MyInteger implements Comparable<MyInteger> {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	@Override
	public int compareTo(MyInteger o) {
		return this.value - o.value;
	}
	
}


Comparable을 구현하지 않고 그냥 정렬했다면 어떻게 될까?

이렇게 예외가 던져지면서 프로그램이 종료가 되어버린다.

MyInteger 클래스를 Arrays.sort 안에서 정렬을 하면서 원소를 비교하려 하는데, 해당 클래스가 비교할 수 있는 기준이 정의되어있지 않아서 정렬 자체가 불가능한 것이다.

만약 Comparable 대신 Comparator을 쓴다면 어떻게 해야할까?

앞서 배운 것 처럼 익명객체를 생성하여 MyInteger에 대한 Comparator를 구현해주는 것이다. 다음과 같이 말이다.

Comparator<MyInteger> comp = new Comparator<MyInteger>() {
		
	@Override
	public int compare(MyInteger o1, MyInteger o2) {
		return o1.value - o2.value;
	}
};

전체코드는 다음과 같다.

import java.util.Arrays;
import java.util.Comparator;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
	}
 
	
	static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
		
		@Override
		public int compare(MyInteger o1, MyInteger o2) {
			return o1.value - o2.value;
		}
	};
}
 
 
class MyInteger {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
}

그러면 이제 우리가 만든 comp 익명 객체를 사용하여 정렬할 수 있도록 해야한다.

"그러면 Arrays.sort()에 어떻게 Comparator 익명객체를 기준으로 정렬을 시키는 것이죠?" 라고 물을 수 있다.

이 부분은 걱정 할 것이 없다. Arrays.sort()에는 단순히 배열만 파라미터로 받는 것이 아니라 Comparator 또한 파라미터로 받기도 한다.

즉, 우리가 쓸 메소드는 다음과 같다.


간략하게 요약하자면, Comparator 파라미터로 넘어온 c의 비교 기준을 갖고 파라미터로 넘어온 객체배열 a을 정렬하겠다는 의미다.
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Arrays.html#sort(T%5B%5D,java.util.Comparator)

import java.util.Arrays;
import java.util.Comparator;
 
public class Test {
	
	public static void main(String[] args) {
		
		MyInteger[] arr = new MyInteger[10];
		
		// 객체 배열 초기화 (랜덤 값으로) 
		for(int i = 0; i < 10; i++) {
			arr[i] = new MyInteger((int)(Math.random() * 100));
		}
 
		// 정렬 이전
		System.out.print("정렬 전 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
		
		Arrays.sort(arr, comp);		// MyInteger에 대한 Comparator을 구현한 익명객체를 넘겨줌
        
		// 정렬 이후
		System.out.print("정렬 후 : ");
		for(int i = 0; i < 10; i++) {
			System.out.print(arr[i].value + " ");
		}
		System.out.println();
	}
 
	
	static Comparator<MyInteger> comp = new Comparator<MyInteger>() {
		
		@Override
		public int compare(MyInteger o1, MyInteger o2) {
			return o1.value - o2.value;
		}
	};
}
 
 
class MyInteger {
	int value;
	
	public MyInteger(int value) {
		this.value = value;
	}
	
	
}

오름차순이 아닌 내림차순으로 구현하고 싶다면 어떻게 해야할까?

[두 수의 비교 결과에 따른 작동 방식]

  • 음수일 경우 : 두 원소의 위치를 교환 안함
  • 양수일 경우 : 두 원소의 위치를 교환 함

이 방식을 이용하자.

정렬 알고리즘에서는 두 원소를 compare 혹은 compareTo 를 써서 양수값이 나오냐, 음수값이 나오냐에 따라 판단을 한다는 것이다.

내림차순으로 정렬하고 싶은 경우 두 원소를 비교한 반환값을 반대로 해주면 되는 것 아닌가?
쉽게 말해 두 값의 차가 양수가 된다면 이를 음수로 바꿔 반환해주고, 만약 음수가 된다면 그 값을 양수로 바꾸어 반환해주면 된다는 것이다.

/ Comparable
public int compareTo(MyClass o) {
	return -(this.value - o.value);
}
 
// Comparator
public int compare(Myclass o1, MyClass o2) {
	return -(o1.value - o2.value);
}

위와 같이 반환값의 부호(sign)을 바꿔주는 것이다.

위 식을 좀더 간단히 아래와같이 쓸 수 있다.

// Comparable
public int compareTo(MyClass o) {
	return o.value - this.value;	// == -(this.value - o.value);
}
 
// Comparator
public int compare(Myclass o1, MyClass o2) {
	return o2.value - o1.value;		// == -(o1.value - o2.value);
}

보면 알겠지만, Comparator는 익명객체로 여러개를 생성할 수 있지만, Comparable의 경우 compareTo 하나 밖에 구현할 수 없다.

그렇다보니, 보통은 Comparable은 여러분이 비교하고자 하는 가장 기본적인 설정(보통은 오름차순)으로 구현하는 경우가 많고, Comparator는 여러개를 생성할 수 있다보니 특별한 정렬을 원할 때 많이 쓰인다.

쉽게 말해 Comparable은 기본(default) 순서를 정의하는데 사용되며, Comparator은 특별한(specific) 기준의 순서를 정의할 때 사용된다는 것이다.

참고로 여러분이 쓰는 String의 경우 두 String간의 문자열 비교를 위해 compareTo()를 썼을 것이다. 이 메소드가 가능했던 이유가 바로 String 클래스에 Comparable을 implements하여 compareTo() 메소드를 구현하고 있기 때문에 그렇다.


참고
https://st-lab.tistory.com/243

0개의 댓글