programming language/Java

[Java] Comparable, Comparator, 정렬 방법

공대키메라 2022. 4. 21. 23:05

필자는 무언가 정렬을 잘 못하는것을 최근 깨달았다.

 

그래서 이번에는 자바에서 정렬을 하는 방법을 학습하려고 한다. 

 

이게 이런게 참 기초이지만 모르는다는것에 부끄러움을 사알짝 느끼면서 오늘도 시자악~

1. Comparable

oracle에서 Comparable에 대한 설명을 가져왔다.

 

이 인터페이스는 그것을 구현하는 각 클래스의 객체에 total ordering을 수행한다.

이 ordering은 클래스의 natural ordering으로 여기지고, 클래스의 compareTo 메소드는 natural 비교 메소드로 여겨진다.

이 인터페이스를 구현하는 객체의 리스트는 자동적으로 Collections.sort(그리고 Arrays.sort)로  정렬될 수 있다.

이 인터페이스를 구현하는 객체는 정렬된 맵에서 키로 이용될 수 있거나 정렬된 set의 원소로 사용될 수 있다.
(comparator를 구체화할 필요없이) 

출처 : https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html

 

여기서 natural ordering이란 자연수런 순서로 밑에서 위로 가는 오름차순 형을 말하는 것으로 이해하면 된다.

 

이 친구의 특이한 점은 단 하나의 메소드만 존재한다는 것이다.

 

Method Summary

int compareTo(T o)
Compares this object with the specified object for order.

이 메소드는 음수, 영, 양수를 구체화된 객체보다 더 작을때, 같을 때, 클 때 반환한다. 

2. 구현 - Comparable 사용하기

구현(Implementation) 동화

필자 공대키메라의 학급(나는 학생이다 응애!)은 학생 이름과 개인에게 부여된 번호가 있다. 

어느날 공대 키메라의 반 학생들은 줄을 순서 상관없이 서 있었다.

어느 때는 순차적으로, 어느 때는 또 역순으로 줄을 서러면 어떻게 해야할까?

Student.java

public class Student implements Comparable<Student> {

    private String name;
    private int number;
    
    public Student(String name, int number) {
        this.name = name;
        this.number = number;
    }
    //getter setter 생략

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

Main.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student("shane",159));
        students.add(new Student("bangal", 555));
        students.add(new Student("chimera",444));
        students.add(new Student("docter kim",972));
        System.out.println("1. 넣은 순서대로 순회");
        for (Student student : students) {
            System.out.println("student.getName = " + student.getName());
            System.out.println("student.getNumber = " + student.getNumber());
        }

        System.out.println("2. 오름차순으로 정렬");
        Collections.sort(students , (o1, o2) -> o1.getNumber() - o2.getNumber());
        for (Student student : students) {
            System.out.println("student.getName = " + student.getName());
            System.out.println("student.getNumber = " + student.getNumber());
        }

        System.out.println("2. 역순으로 순회 - Comparator 객체 사용");
        Comparator<Student> comparator = new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o2.getNumber() - o1.getNumber();
            }
        };
        //Comparator<Student> comparator1 = (o1, o2) -> o2.getNumber() - o1.getNumber();

        Collections.sort(students, comparator);
        for (Student student : students) {
            System.out.println("student.getName = " + student.getName());
            System.out.println("student.getNumber = " + student.getNumber());
        }

        System.out.println("3. 오름차순 재정렬 - 내부 선언 로직 사용");
        Collections.sort(students);
        for (Student student : students) {
            System.out.println("student.getName = " + student.getName());
            System.out.println("student.getNumber = " + student.getNumber());
        }

    }
}

 

출력 결과 보기

더보기
1. 넣은 순서대로 순회
student.getName = shane
student.getNumber = 159
student.getName = bangal
student.getNumber = 555
student.getName = chimera
student.getNumber = 444
student.getName = docter kim
student.getNumber = 972

2. 오름차순으로 정렬
student.getName = shane
student.getNumber = 159
student.getName = chimera
student.getNumber = 444
student.getName = bangal
student.getNumber = 555
student.getName = docter kim
student.getNumber = 972

2. 역순으로 순회 - Comparator 객체 사용
student.getName = docter kim
student.getNumber = 972
student.getName = bangal
student.getNumber = 555
student.getName = chimera
student.getNumber = 444
student.getName = shane
student.getNumber = 159

3. 오름차순 재정렬
student.getName = shane
student.getNumber = 159
student.getName = chimera
student.getNumber = 444
student.getName = bangal
student.getNumber = 555
student.getName = docter kim
student.getNumber = 972
Process finished with exit code 0

 

이렇게 요리조리 내 마음대로 순서대로, 역순으로 정렬을 해봤다.

 

막상 사용해보니 그렇게 어렵지 않았다.

 

3. Comparator

몇몇 객체의 collection에서 total ordering을 수행하는 비교 함수. Comparators는 Collection.sorts 혹은 Arrays.sort같은 정렬 메소드로 넘겨질 수 있으며 정렬 순서를 정확히 제어할 수 있게 한다. 

Comparators는 특정 데이터 구조 (sorted set 또는 sorted map)의 순서를 또한 제어하는데 사용된다. 
또는 natural ordering을 가지지 않은 객체의 순서를 제공해기위해 사용된다.

출처 : https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html

 

Compatator 인터페이스는 사용자 정의 클래스 객체의 순서를 주기 위해 사용된다.

Comparator 객체는 같은 클래스의 비교하는 두 객체를 비교할 수 있다. 

출처 :  geeksforgeeks.org/comparator-interface-java/

 

 이 Comparator 인터페이스는 Comparable과 다르게 지원하는 메소드가 많다. 

 

필자는 이렇게 많은 메소드가 있는지도 몰랏고 사실 직접 compare만 정의해서 사용할 줄 알았다.

 

그리고 딱히 나머지 기능이 필요한것도 사실 잘 모르겠다. 누구 아는 사람 있으면 알려주셈... 

 

메소드 더보기

더보기

Modifier and TypeMethod and Description

 

int compare(T o1, T o2)
Compares its two arguments for order.
static <T,U extends Comparable<? super U>>
Comparator<T>
comparing(Function<? super T,? extends U> keyExtractor)
Accepts a function that extracts a Comparable sort key from a type T, and returns a Comparator<T> that compares by that sort key.
static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)
Accepts a function that extracts a sort key from a type T, and returns a Comparator<T> that compares by that sort key using the specified Comparator.
static <T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor)
Accepts a function that extracts a double sort key from a type T, and returns a Comparator<T> that compares by that sort key.
static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)
Accepts a function that extracts an int sort key from a type T, and returns a Comparator<T> that compares by that sort key.
static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor)
Accepts a function that extracts a long sort key from a type T, and returns a Comparator<T> that compares by that sort key.
boolean equals(Object obj)
Indicates whether some other object is "equal to" this comparator.
static <T extends Comparable<? super T>>
Comparator<T>
naturalOrder()
Returns a comparator that compares Comparable objects in natural order.
static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)
Returns a null-friendly comparator that considers null to be less than non-null.
static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)
Returns a null-friendly comparator that considers null to be greater than non-null.
default Comparator<T> reversed()
Returns a comparator that imposes the reverse ordering of this comparator.
static <T extends Comparable<? super T>>
Comparator<T>
reverseOrder()
Returns a comparator that imposes the reverse of the natural ordering.
default Comparator<T> thenComparing(Comparator<? super T> other)
Returns a lexicographic-order comparator with another comparator.
default <U extends Comparable<? super U>>
Comparator<T>
thenComparing(Function<? super T,? extends U> keyExtractor)
Returns a lexicographic-order comparator with a function that extracts a Comparable sort key.
default <U> Comparator<T> thenComparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)
Returns a lexicographic-order comparator with a function that extracts a key to be compared with the given Comparator.
default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor)
Returns a lexicographic-order comparator with a function that extracts a double sort key.
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor)
Returns a lexicographic-order comparator with a function that extracts a int sort key.
default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor)
Returns a lexicographic-order comparator with a function that extracts a long sort key.

 

4. 구현 - Comparator 사용하기

Main.java

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

    public static void main(String[] args) {
         List<Student> studentList = new ArrayList<>();

         studentList.add(Student.builder().name("shane").number(123).tall(190).build());
         studentList.add(Student.builder().name("bengalTiger").number(312).tall(180).build());
         studentList.add(Student.builder().name("chimera").number(231).tall(182).build());
         studentList.add(Student.builder().name("doctor Kim").number(456).tall(199).build());

        System.out.println(" 1. 이름 정렬 구현하기 ");
        Collections.sort(studentList, new SortByName());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }
        System.out.println();
        System.out.println(" 2. 키 정렬 구현하기 ");
        Collections.sort(studentList, new SortByTall());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }
        System.out.println();
        System.out.println(" 3. 키로 역순 정렬 구현하기 ");
        Collections.sort(studentList, new ReverseSortByTall());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }

    }

    static class SortByName implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getName().compareTo(o2.getName());
        }
    }

    static class SortByTall implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getTall() - o2.getTall();
        }
    }

    static class ReverseSortByTall implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o2.getTall()-o1.getTall();
        }
    }

    // 맘에 드는거 구현하기

}

 

출력 결과

더보기
1. 이름 정렬 구현하기 
student = Student(name=bengalTiger, number=312, tall=180)
student = Student(name=chimera, number=231, tall=182)
student = Student(name=doctor Kim, number=456, tall=199)
student = Student(name=shane, number=123, tall=190)

 2. 키 정렬 구현하기 
student = Student(name=bengalTiger, number=312, tall=180)
student = Student(name=chimera, number=231, tall=182)
student = Student(name=shane, number=123, tall=190)
student = Student(name=doctor Kim, number=456, tall=199)

 3. 키로 역순 정렬 구현하기 
student = Student(name=doctor Kim, number=456, tall=199)
student = Student(name=shane, number=123, tall=190)
student = Student(name=chimera, number=231, tall=182)
student = Student(name=bengalTiger, number=312, tall=180)

5. Comparable vs Comparator

Comparable이던지, Comparator이던지 둘 다 정렬하는 기능을 가진 interface인데

 

둘의 차이점은 뭘까?

 

다음에서 해당 내용을 찾을 수 있었다.

 

출처 : https://www.javatpoint.com/difference-between-comparable-and-comparator

 

Comparable과 Comparator는 둘다 인터페이스고 컬렉션 원소를 정렬할 수 있다.

 

하지만, 둘 사이에 아래처럼 많은 차이점이 있다.

 

1) Comparable는 single sorting sequence를 제공한다. 다시 말해서, id, name, price같은 하나의 원소에 기초해 collection을 정렬할 수 있다. Comparator는 multiple sorting sequences를 제공한다. 다시망해서, id, name, price같은 다수의 원소에 기초해 collection을 정렬할 수 있다.
2) Comparble는 원래 클래스에 영향을 준다. 즉, 실제 클래스가 수정된다. Comparator는 원래 클래스에 영향을 주지 않는다. 즉, 실제 클래스는 수정되지 않는다.
3) Comparable는 compareTo 메소드를 지원한다. Comparator는 compare()메소드를 제공한다.
4) Comparable은 java.lang.package에 존재한다. Comparator는  java.util package에 존재한다.
5) Collection.sort(List)메소드로 Comparable type의 리스트 원소들을 정렬할 수 있다. Collection.sort(List, Comparator) 메소드로 Comparator type의 리스트 원소들을 정렬할 수 있다.

 

여기서 필자가 몰랐던 부분은 원래 객체에 영향을 미치냐, 안미치냐였다.

 

Comparable의 경우 원래 값도 변경이 되고, Comparator는 실제 클래스에는 영향을 주지 않는다고 하니 흥미롭다.

 

물론 Comparator의 경우에는 Multi sorting sequences를 통해 하나의 정렬 기준만 적용하는게 아닌, 여러개의 정렬 기준을 적용할 수 있는것 같다. 

 

이것을 궁금해서 어떻게 적용했는지 찾아봤다.

Multi Sorting in Comparator

Main.java - modified

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Main {

    public static void main(String[] args) {
         List<Student> studentList = new ArrayList<>();

         studentList.add(Student.builder().name("shane").number(123).tall(190).build());
         studentList.add(Student.builder().name("bengalTiger").number(312).tall(180).build());
         studentList.add(Student.builder().name("chimera").number(231).tall(182).build());
         studentList.add(Student.builder().name("doctor Kim").number(456).tall(199).build());

        System.out.println(" 1. 이름 정렬 구현하기 ");
        Collections.sort(studentList, new SortByName());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }
        System.out.println();
        System.out.println(" 2. 키 정렬 구현하기 ");
        Collections.sort(studentList, new SortByTall());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }
        System.out.println();
        System.out.println(" 3. 키로 역순 정렬 구현하기 ");
        Collections.sort(studentList, new ReverseSortByTall());
        for (Student student : studentList) {
            System.out.println("student = " + student);
        }

    }

    static class SortByName implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getName().compareTo(o2.getName());
        }
    }

    static class SortByTall implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getTall() - o2.getTall();
        }
    }

    static class ReverseSortByTall implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            return o2.getTall()-o1.getTall();
        }
    }

	//Multi-sorting 기능 구현
    static class MultiSorting implements Comparator<Student> {
        @Override
        public int compare(Student o1, Student o2) {
            int nameCompare = o1.getName().compareTo(o2.getName());
            int tallCompare = o1.getTall().compareTo(o2.getTall());
            return (nameCompare == 0) ? tallCompare : nameCompare;
        }
    }

    // ...

}

 


이렇게 java에서 제공하는 Comparable과 Comparator에 대해 알아보고 차이점, 그리고 각자 구현하는 방법을 알아봤다.

 

요즘 자주 드는 생각은 이게 기초가 참 중요하다는 생각이다.

 

그거 잠시 코드 빠르고 잘짜는것은 정형화된 형식으로 하면 되지만, 과연 좋은 방식인지, 다른 방법은 무엇이 있는지 고민하는 경우가 많은데, 그런 이유로 문서를 보게 되고 뒤를 찾다보면 이런 기초적인 부분을 보게 되더라!

 

이렇게 차근 차근 기본에 충실하도록 노력하겠다.