Comparable / Comparator

goose_bumps·2024년 12월 7일

궁금증 저장소

목록 보기
2/3

객체 간의 비교는 어떻게 할 수 있을까?

원시타입의 경우 자바에서 부등호를 통해 비교가 가능하기 때문에 별다른 처리가 필요없지만 그 외에 타입 혹은 사용자 정의 타입을 어떻게 비교할까?

Comparable, Comparator 인터페이스를 사용하면 된다.

두 인터페이스 모두 객체를 비교할 때 사용하지만, 구현 메서드가 기능이 다르다.

1. Comparable

기본적인 정렬기준을 구현하는데 사용하며 java.lang 패키지에 정의되어 있다.
유일한 추상메서드는 compareTo(T o)이며 클래스 내부에서 구현한다.

인터페이스가 어떻게 정의되어 있는지 보자.

public interface Comparable<T>{
	public int compareTo(T o);
    }

메서드가 추상메서드 1개뿐이며, 제네릭 타입을 통해 메서드 매개변수의 타입을 정할 수 있다.

compareTo(T o)는 반환값에 따라 정렬 순서를 결정하는데

  • 음수(-): 현재 객체가 비교 대상 객체보다 작음.
  • 0: 현재 객체와 비교 대상 객체가 같음.
  • 양수(+): 현재 객체가 비교 대상 객체보다 큼.

하나의 정렬기준을 가지기 때문에 간단하고 효과적이지만, 여러 정렬기준이 필요할 경우 Comparator를 사용해야 한다.

Comparable을 어떻게 구현하는지 간단한 예제를 통해 확인해보자.

class Resident implements Comparable<Resident>{
    private final int age;
    private final String name;

    public Resident(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    @Override
    public int compareTo(Resident o) {
        return this.getAge() - o.getAge();
    }

    @Override
    public String toString(){
        return this.getName() + " : " + this.getAge();
    }
}

주민 정보를 클래스로 만들고 Comparable을 구현하여 정렬 기준을 나이의 오름차순으로 정렬하였다.
toString()을 오버라이딩 하여 출력 시 형식을 정의하고

        List<Resident> residentsList = Arrays.asList(
                new Resident(23,"kim"),
                new Resident(32,"Lee"),
                new Resident(28,"Park"),
                new Resident(24,"Choi"));

        Collections.sort(residentsList);
        System.out.println(residentsList);

주민 객체를 ArrayList에 담아 정렬시키면 내부적으로 정의된 compareTo() 기준에 따라 정렬된다.

[kim : 23, Choi : 24, Park : 28, Lee : 32]

앞에서 언급했듯이 하나의 정렬기준을 가지기 때문에 객체 s1, s2가 있을 경우 s1.compareTo(s2)>0이면 s2.compareTo(s1)<0이어야만 한다.

정수, 실수, 문자열을 제공하는 클래스들인 Integer, Double, String 등도 기본적으로 Comparable을 구현하고 있다.

정수, 실수 같은 경우에는 오름차순으로, 문자열은 사전식 정렬을 기본으로 가지는데 이를 자연 순서라고 한다.

        List<Integer> list = Arrays.asList(4,5,6,7,34,2);
        Collections.sort(list);
        System.out.println(list); //[2, 4, 5, 6, 7, 34]

무작위로 만든 정수 리스트를 Collectionsort()를 사용하면 자연 순서에 따라 정렬이 되는데 listInteger타입이기 때문에 오름차순으로 정렬이 된다.

정리

  • 원시타입은 자바에서 자체적으로 비교가 가능하지만 그 외에는 정렬기준을 구현해야 한다
  • Comparable< T >는 한 클래스에서 오직 하나의 정렬 기준만 정의할 수 있다
  • Integer,String 등의 클래스들도 기본적으로 Comparable< T >를 구현하고 있다
  • 여러 정렬기준을 가지려면 Comparator를 구현해야 한다

2. Comparator

ComparatorComparable과 달리 외부에서 정렬 기준을 정의할 수 있다.

Comparable의 추상 메서드가 compareTo(T o)이고 자기 자신과 매개변수로 들어오는 객체를 비교하는 반면, Comparator의 추상 메서드인 compare(T o1, T o2)는 매개변수로 들어오는 두 객체를 비교한다.

추가로 추상 메서드 외에 클래스 메서드와 디폴트 메서드를 가지고 있다.

1) Comparator의 추상메서드

가장 기본적인 compare(T o1, T o2)를 어떻게 사용하고 외부에서는 정렬 기준을 어떻게 정의하는지 알아보자.

우선, Comparator의 반환값은 다음과 같다

  • 음수 : o1o2보다 우선순위가 높다
  • 0 : 두 객체가 동등하다
  • 양수 : o2o1보다 우선순위가 높다

학생의 정보를 가진 Student 클래스는 점수와 이름을 필드값으로 가진다고 가정해보자.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(56,"Kang"),
                new Student(90,"Choi"),
                new Student(12,"Park"),
                new Student(76,"Lee")
        );
    }
}

class Student{
    int score;
    String name;

    public Student(int score, String name) {
        this.score = score;
        this.name = name;
    }
    public int getScore() {
        return score;
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString(){
        return "Student name = " + this.name + ", score = " + this.score;
    }
}

이때, 점수를 기준으로 오름차순으로 객체를 배열하고자 한다면 어떻게 해야할까?

먼저 익명 객체를 사용하여 점수를 비교기준으로 가지는 Comparator를 정의해야 한다.

        Comparator<Student> comparator = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getScore()- o2.getScore();
            }
        };

o1의 점수에서 o2의 점수를 빼서 음수가 나온다면 o1이 우선순위가 앞서기 때문에 이를 다르게 말하면 점수가 더 작은 o1이 앞으로 배치되므로 오름차순 정렬이 되는 것이다.
(만약, 내림차순으로 정렬할 경우 반대로 하면 된다)

이제 Collectionssort()를 사용하여 정렬기준인 comparator를 매개변수로 넣어주면 된다.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(56,"Kang")
        );

        Comparator<Student> comparator = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getScore()- o2.getScore();
            }
        };
        //정렬 전
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Na, score = 23, Student name = Kang, score = 56]

        //정렬 후
        Collections.sort(students,comparator);
        System.out.println(students);
        //[Student name = Na, score = 23, Student name = Kang, score = 56, Student name = Kim, score = 84]
    }
}

이름을 기준으로 정렬을 원할 경우 동일한 방법으로 구성하면 된다.
(String 클래스는 사전적 순서를 따르기 때문)

2) Comparator의 클래스 메서드

클래스 메서드(정적 메서드)를 사용하면 더 간결하게 사용이 가능하다.

대표적인 클래스 메서드로 comparing()reverseOrder()가 있다.

comparing()comparing(Function<? super T,? extends U> keyExtractor)으로 정의되어 있는데, 쉽게 말하면 매개변수로 특정 필드를 반환하는 Function을 입력하면 된다.

위에서 설명한 점수 기준의 오름차순 정렬을 클래스 메서드를 사용하여 만들어보자.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(56,"Kang")
        );

        //정렬 전
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Na, score = 23, Student name = Kang, score = 56]

        //정렬 후
        Function<Student,Integer> function = s -> s.getScore();
        students.sort(Comparator.comparing(function));
        System.out.println(students);
        //[Student name = Na, score = 23, Student name = Kang, score = 56, Student name = Kim, score = 84]
    }
}

Function에 람다식을 대입하여 매개변수로 사용가능하고 메서드 참조도 매개변수로 사용이 가능하다.

students.sort(Comparator.comparing(Student::getScore));

위와 같이 메서드 참조를 사용하면 더 간결화할 수 있다.

reverseOrder()는 자연적인 순서(오름차순)에 반대되는 정렬을 하는 기능인데 내림차순 정렬이 가능하게 해준다. Comparable을 구현한 클래스에 대해서만 적용이 가능하여 예시에 있는 Student 클래스에는 적용할 수 없다.
하지만, 간단한 예제는 보고 넘어가도록 하자.

class Student implements Comparable<Student>{
    int score;
    String name;

    public Student(int score, String name) {
        this.score = score;
        this.name = name;
    }
    public int getScore() {
        return score;
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString(){
        return "Student name = " + this.name + ", score = " + this.score;
    }

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

Student 클래스를 수정하여 Comparable< Student >를 구현하고 자연적 순서를 점수의 오름차순으로 정의하였다.
그렇다면 reverseOrder()를 호출하게 되면 점수의 내림차순으로 정렬이 될 것이다.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(56,"Kang")
        );

        //정렬 전
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Na, score = 23, Student name = Kang, score = 56]

        //정렬 후
        students.sort(Comparator.reverseOrder());
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Kang, score = 56, Student name = Na, score = 23]
    }
}

3) Comparator의 디폴트 메서드

대표적인 디폴트 메서드로 reversed()thenComparing()이 있다.

  • reversed(): 기존 Comparator를 반전(내림차순)으로 변환
  • thenComparing(): 다중 기준 Comparator 생성

reversed()는 말그대로 기존의 정렬 기준인 Comparator에 호출을 하면 그 반전으로 변환하는 것이다.
thenComparing()은 다중 혹은 중첩 기준인데 예를 들어, "점수로는 오름차순이면서 이름으로는 내림차순" 처럼 특정 기준 중에서 다시 기준을 잡아 정렬하는 것이다.

앞에서 점수의 오름차순으로 정렬된 예시를 반전시켜보겠다.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(56,"Kang")
        );

        //정렬 전
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Na, score = 23, Student name = Kang, score = 56]

        //정렬 후
        students.sort(Comparator.comparing(Student::getScore).reversed());
        System.out.println(students);
        //[Student name = Kim, score = 84, Student name = Kang, score = 56, Student name = Na, score = 23]
    }
}

클래스 메서드인 comparing()을 호출 후 reversed()를 호출하여 기존에 선정한 정렬 기준을 반전시켰다.

이번에는 다중기준의 예시를 보여줄 것인데 설명을 위해 이름이 "Na"와 동점자인 "Park"을 추가하겠다.

public class Main {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(23,"Park"),
                new Student(56,"Kang")
        );

        //정렬 전
        System.out.println(students);

        //정렬 후
        students.sort(Comparator.comparing(Student::getScore).thenComparing(Student::getName));
        System.out.println(students);
        //[Student name = Na, score = 23, Student name = Park, score = 23, Student name = Kang, score = 56, Student name = Kim, score = 84]
    }
}

점수로 오름차순이 되었고 동점자의 경우 이름의 오름차순이 적용되었다.
"Na"가 "Park"보다 사전적으로 앞서기 때문에 우선순위를 가지는 것이다.

3. Stream에서의 사용

Stream의 중간연산 중 sorted()라는 정렬 기능을 가진 메서드가 있다.
이 메서드의 매개변수로 Comparator가 들어가며, 매개변수를 입력하지 않을 경우 기본 정렬 기준(Comparable)으로 정렬한다.

하지만, Stream이라고 크게 다를 건 없고 기존과 동일하게 Comparator를 매개변수로 넣어주면 그만이다.

1) 기본 정렬 기준으로 정렬

위에서 다룬 학생들의 점수 오름차순 정렬을 Stream으로 구현해보자.

        Stream<Student> studentStream = Stream.of(
        		new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(23,"Park"),
                new Student(56,"Kang"));

        studentStream.sorted().forEach(System.out::println);

이 코드는 ClassCastException이 발생한다. 왜일까? sorted()에 매개변수를 입력하지 않았기 때문에 기본 정렬 기준으로 정렬을 하는데, 문제는 Student 클래스는 Comparable을 구현하지 않았다.

다음과 같이 코드를 수정하면 해결이 된다.

class Student implements Comparable<Student>{
    int score;
    String name;

    public Student(int score, String name) {
        this.score = score;
        this.name = name;
    }
    public int getScore() {
        return score;
    }
    public String getName() {
        return name;
    }
    @Override
    public String toString(){
        return "Student name = " + this.name + ", score = " + this.score;
    }

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

Student name = Na, score = 23
Student name = Park, score = 23
Student name = Kang, score = 56
Student name = Kim, score = 84

기본 정렬을 점수의 오름차순으로 정렬하였기 때문에 출력값도 점수의 오름차순으로 나왔다.

2) Comparator를 기준으로 정렬

Comparator에 대해서는 앞에서 설명했기 때문에 여기서는 예시를 통해서 알아보자.

점수에 대해서는 내림차순으로 정렬하고 동점자가 있을 경우 이름의 오름차순으로 정렬해보겠다.

공부한 내용을 복습할 겸 추상 메서드, 클래스 메서드, 디폴트 메서드를 전부 사용해서 구현해보자.

  • compare()thenComparing()을 사용하여 구현하는 방법
        Stream<Student> studentStream = Stream.of(new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(23,"Park"),
                new Student(56,"Kang"));

        Comparator<Student> scoreComparator = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getScore()- o1.getScore();
            }
        };

        Comparator<Student> nameComparator = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getName().compareTo(o2.getName());
            }
        };

        studentStream.sorted(scoreComparator.thenComparing(nameComparator)).forEach(System.out::println);
  • comparing()thenComparing()에 메서드 참조를 사용하여 구현하는 방법
        Stream<Student> studentStream = Stream.of(new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(23,"Park"),
                new Student(56,"Kang"));

        studentStream.
        sorted(Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getScore)).
        forEach(System.out::println);
  • Function과 람다식 사용하는 방법
        Stream<Student> studentStream = Stream.of(new Student(84,"Kim"),
                new Student(23,"Na"),
                new Student(23,"Park"),
                new Student(56,"Kang"));

        Function<Student,Integer> scoreFunction = s -> s.getScore();
        Function<Student,String> nameFunction = s -> s.getName();

        studentStream.
        sorted(Comparator.comparing(scoreFunction).reversed().thenComparing(nameFunction)).
        forEach(System.out::println);

Student name = Kim, score = 84
Student name = Kang, score = 56
Student name = Na, score = 23
Student name = Park, score = 23

3가지 방법 모두 동일하게 출력이 되었다.

Stream은 정렬과 출력을 동일한 방법으로 하여 코드의 재사용성을 높이기 위해 만들어진 것이기 때문에 Comparable/Comparator 를 Stream에서 사용한다고 해서 다른 것은 전혀 없다.

0개의 댓글