본문 바로가기
JAVA

[JAVA] Comparable과 Comparator

by snow_white 2022. 3. 13.

이전 글과 함께 백준 문제 풀이 도중 2차원 배열 정렬을 해야하는 상황이 발생했다.

 

일차원 배열의 경우 Arrray.sort()를 통해 쉽게 오름차순 정렬이 가능하다.

import java.util.Arrays;

public class Comparable_Comparator {

	public static void main(String[] args) {
		int arr[] = {1, 7, 2, 6, 8, 5, 3, 4};
		Arrays.sort(arr);
		System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]
	}
}

 

하지만,

2차원 배열을 바로 Arrray.sort()를 통해 정렬하려고 하면 java.lang.ClassCastException: I cannot be cast to java.lang.Comparable 오류가 발생한다.

1차원 배열은 인덱스를 구분해서 앞, 뒤 데이터를 비교할 수 있지만, 이차원 배열은 그렇지 않다. 비교해야 할 대상을 정해줘야 한다!

import java.util.Arrays;

public class Comparable_Comparator {

	public static void main(String[] args) {
		int arr[][] = {{8,2}, {6,1}, {5,7}, {3,4}};
		Arrays.sort(arr); // java.lang.ClassCastException: [I cannot be cast to java.lang.Comparable
		// 실행 안 됨
       		System.out.println(Arrays.toString(arr));
	}
}

미리 정답을 알려드리자면 Comparator 인터페이스를 구현하여 비교해야 할 대상을 기준으로 정렬한다는 것이다.

이차원 배열을 각 행의 두 번째 수인 [][1]인덱스를 기준으로 오름차순 해보자.

이차원 배열을 정렬할 땐 따로 built-in된 클래스의 소스코드를 변경할 수 없으므로 Comparator 인터페이스로 구현하여 custom 클래스로 따로 작성하는 방법을 택한다. 혹은 아래와 같이 익명 구현 객체를 통해 Comparator에서 override한 compare 함수를 바로 구현할 수도 있다.

import java.util.Arrays;
import java.util.Comparator;

public class Main {
	public static void main(String[] args) {
		int arr[][] = {{8,2}, {6,1}, {5,7}, {3,4}};
		Arrays.sort(arr, new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[1] - o2[1]; // 두 번째 수를 비교하여 오름차순 정렬
            }
        });
 
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i][0] + " " + arr[i][1]);
        }
        System.out.println();
	}
}
/*
6 1
8 2
3 4
5 7
*/

Comparable<T> 인터페이스

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

Comparable 인터페이스는 객체를 정렬하는 데 사용되는 메소드인 compareTo() 메소드를 정의하고 있다.

자바에서 같은 타입의 인스턴스를 서로 비교해야만 하는 클래스들은 모두 Comparable 인터페이스를 구현하고 있다.

따라서 Boolean을 제외한 래퍼 클래스나 String, Time, Date와 같은 클래스의 인스턴스는 모두 정렬 가능하다.

이때 기본 정렬 순서는 작은 값에서 큰 값으로 정렬되는 오름차순이 된다.

 

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

 

Comparable 인터페이스는 다음과 같은 메소드를 사용하여 객체를 정렬한다.

메소드 설명
int compareTo(T o) 해당 객체와 전달된 객체의 순서를 비교함.

 

아래 코드는 Comparable 인터페이스의 compareTo() 메소드를 사용한 예제이다.

Car 클래스는 Comparable 인터페이스를 구현한다.

class Car implements Comparable<Car> {

    private String modelName;
    private int modelYear;
    private String color;

    Car(String mn, int my, String c) {
        this.modelName = mn;
        this.modelYear = my;
        this.color = c;
    }

    public String getModel() {
        return this.modelYear + "식 " + this.modelName + " " + this.color;
    }
    
    public int compareTo(Car obj) {
        if (this.modelYear == obj.modelYear) {
            return 0;
        } else if(this.modelYear < obj.modelYear) {
            return -1;
        } else {
            return 1;
        }
    }
}

public class Comparable01 {
    public static void main(String[] args) {

        Car car01 = new Car("아반떼", 2016, "노란색");
        Car car02 = new Car("소나타", 2010, "흰색");

        System.out.println(car01.compareTo(car02)); // 1
    }
}

Car 인스턴스 비교를 위해 compareTo() 메소드를 호출하였다.

compareTo() 메소드는 자기 자신과 매개변수로 들어온 객체를 받아와 Car의 연식을 비교하는 기능을 구현한다.

car01의 연식은 2016년, 매개변수로 들어간 car02의 연식은 2010년이다.

 

car01.compareTo(car02); 로 호출하면

compareTo()에서 this는 car01이 되고, 매개인자로 받는 obj 객체는 car02가 된다.

따라서 this.modelYear 는 2016, obj.modelYear 는 2010이 된다.

두 수를 비교해서 자기 자신의 값이 크면 양수를 반환하고,

매개인자로 들어온 값이 크면 음수를 반환한다.

두 개의 값이 같다면 0을 반환한다.

 

따라서 this.modelYear인 내 자신의 값이 더 크므로 반환 값은 1이 되고, 결과적으로 출력 결과도 1이 된다.

 


Collections.sort()

 

데이터를 정렬하는데 Collections.sort()도 많이 보았을 것이다.

 

List객체를 파라미터로 받는 경우는 제네릭 타입인 클래스가 Comparable를 구현하고 있어야 사용 가능하다.

Comaparator를 파라미터로 받는 경우에는 제네릭 타입 클래스가 Comparable을 구현하고 있지 않아도 사용할 수 있다.

이러한 경우에는 Comparator 인터페이스에 구현된 정렬 기준으로 정렬이 수행된다.

아래 코드를 보면 Collections.sort(players); 부분에서 빨간줄로 오류 표시가 난다.

방금 언급했듯이  Comparable를 구현하고 비교할 대상을 지정해야 사용 가능하기 때문이다.

애초에 players 리스트에서 어떤 필드를 가지고 비교할지 명시하지 않았으니 당연히 어떤 값으로 정렬하는지 모르는 게 당연하다.

import java.util.*;

public class Collections_prac {
	
	public static void main(String[] args) {
		List<Player> players = new ArrayList<>();
		players.add(new Player("Alice", 899));
		players.add(new Player("Bob", 982));
		players.add(new Player("Chloe", 1090));
		players.add(new Player("Dale", 982));
		players.add(new Player("Eric", 1018));
		
		Collections.sort(players); // Error!
	}
	
	static public class Player {
	    private String name;
	    private int score;

	    public Player(String name, int score) {
	        this.name = name;
	        this.score = score;
	    }
	}
}


위의 Player 클래스를 Comparable 인터페이스를 구현하도록 변경해본다.

import java.util.*;

public class Collections_prac {

	public static void main(String[] args) {
		List<Player> players = new ArrayList<>();
		players.add(new Player("Alice", 899));
		players.add(new Player("Bob", 982));
		players.add(new Player("Chloe", 1090));
		players.add(new Player("Dale", 982));
		players.add(new Player("Eric", 1018));

		Collections.sort(players);
		System.out.println(players.toString());
        //[Player [name=Chloe, score=1090], Player [name=Eric, score=1018], Player [name=Bob, score=982], Player [name=Dale, score=982], Player [name=Alice, score=899]]
	}

	static public class Player implements Comparable<Player> {
		private String name;
		private int score;

		public Player(String name, int score) {
			this.name = name;
			this.score = score;
		}

		private int getScore() {
			return this.score;
		}

		@Override
		public int compareTo(Player o) {
			return o.getScore() - getScore();
		}

		@Override
		public String toString() {
			return "Player [name=" + name + ", score=" + score + "]";
		}
	}
}

 

이제 Collections.sort() 메서드에는 Comparable 인터페이스를 구현한 Comparable 타입의 Player 객체의 리스트가 인자로 넘어가기 때문에 더 이상 컴파일 에러가 발생하지 않는다. 정렬 후 게이머 리스트를 출력해보면 원하는 바와 같이 점수가 제일 높은 Chloe 가 리스트의 맨 앞으로 위치하고, 점수가 제일 낮은 Alice 가 리스트의 맨 뒤에 위치하게 된다.

a.compareTo(b) 에서

  • a > b : 양수를 반환
  • a == b : 0을 반환
  • a < b : 음수를 반환

Comparator<T> 인터페이스

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

Comparator 인터페이스는 Comparable 인터페이스와 같이 객체를 정렬하는 데 사용되는 인터페이스이다.

Comparable 인터페이스를 구현한 클래스는 기본적으로 오름차순으로 정렬된다.

 

반면에 Comparator 인터페이스는 내림차순이나 아니면 다른 기준으로 정렬하고 싶을 때 사용할 수 있다.

즉, Comparator 인터페이스를 구현하면 조건을 추가적으로 구현하여 오름차순 이외의 기준으로도 정렬할 수 있게 되는 것이다.

이때 Comparator 인터페이스를 구현한 클래스에서는 compare() 메소드를 재정의하여 사용하게 된다.

 

Comparator 인터페이스는 다음과 같은 메소드를 사용하여 객체를 정렬한다.

메소드 설명
int compare(T o1, T o2) 전달된 두 객체의 순서를 비교함.
boolean equals(Object obj) 해당 comparator와 전달된 객체가 같은지를 확인함.
default Comparator<T> reversed() 해당 comparator의 역순인 comparator를 반환함.

 

다음 예제는 요소를 내림차순으로 정렬하여 저장하는 Player 인스턴스를 생성하기 위해 Comparator 인터페이스를 구현하는 예제이다.

 

import java.util.*;

import day0112.Collections_prac.Player;

public class Comparable_Comparator {

	public static void main(String[] args) {
		List<Player> players = new ArrayList<>();
		players.add(new Player("Alice", 899));
		players.add(new Player("Bob", 982));
		players.add(new Player("Chloe", 1090));
		players.add(new Player("Dale", 982));
		players.add(new Player("Eric", 1018));

		Collections.sort(players);
		System.out.println(players.toString());
        //[Player [name=Chloe, score=1090], Player [name=Eric, score=1018], Player [name=Bob, score=982], Player [name=Dale, score=982], Player [name=Alice, score=899]]
	}
	static Comparator<Player> comparator = new Comparator<Player>() {
	    @Override
	    public int compare(Player a, Player b) {
	        return b.getScore() - a.getScore();
	    }
	};
	
	static public class Player implements Comparable<Player> {
		private String name;
		private int score;

		public Player(String name, int score) {
			this.name = name;
			this.score = score;
		}

		private int getScore() {
			return this.score;
		}

		@Override
		public int compareTo(Player o) {
			return o.getScore() - getScore();
		}

		@Override
		public String toString() {
			return "Player [name=" + name + ", score=" + score + "]";
		}
	}
}

 

람다 함수로 대체

Comparator 객체는 메서드가 하나 뿐인 함수형 인터페이스를 구현하기 때문에 람다 함수로 대체가 가능하다.

import java.util.*;

import day0112.Collections_prac.Player;

public class Comparable_Comparator {

	public static void main(String[] args) {
		List<Player> players = new ArrayList<>();
		players.add(new Player("Alice", 899));
		players.add(new Player("Bob", 982));
		players.add(new Player("Chloe", 1090));
		players.add(new Player("Dale", 982));
		players.add(new Player("Eric", 1018));

		Collections.sort(players, (a, b) -> b.getScore() - a.getScore());
		System.out.println(players.toString());
	}
	static public class Player implements Comparable<Player> {
		private String name;
		private int score;

		public Player(String name, int score) {
			this.name = name;
			this.score = score;
		}

		private int getScore() {
			return this.score;
		}

		@Override
		public int compareTo(Player o) {
			return o.getScore() - getScore();
		}

		@Override
		public String toString() {
			return "Player [name=" + name + ", score=" + score + "]";
		}
	}
}

 

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

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

 

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

 

결과적으로 객체들을 만약 비교하고자 한다면 결국 Comparable 혹은 Comparator는 필수요소가 된다.

댓글