본문으로 바로가기

[Java] 자바 8 Comparator API 데이터 정렬

category Backend/Java 2022. 2. 25. 17:12

목차

    1. Comparator

    @FunctionalInterface
    public interface Comparator<T> {
    
        // 필수로 구현해야 하는 추상 메소드
        int compare(T o1, T o2);
    
        default Comparator<T> reversed() {
            return Collections.reverseOrder(this);
        }
    
        default Comparator<T> thenComparing(Comparator<? super T> other) {
            Objects.requireNonNull(other);
            return (Comparator<T> & Serializable) (c1, c2) -> {
                int res = compare(c1, c2);
                return (res != 0) ? res : other.compare(c1, c2);
            };
        }
    
        default <U> Comparator<T> thenComparing(
                Function<? super T, ? extends U> keyExtractor,
                Comparator<? super U> keyComparator)
        {
            return thenComparing(comparing(keyExtractor, keyComparator));
        }
    
        default <U extends Comparable<? super U>> Comparator<T> thenComparing(
                Function<? super T, ? extends U> keyExtractor)
        {
            return thenComparing(comparing(keyExtractor));
        }
    
        public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
            return Collections.reverseOrder();
        }
    
        public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
            return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
        }
    
        public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
            return new Comparators.NullComparator<>(true, comparator);
        }
    
        public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
            return new Comparators.NullComparator<>(false, comparator);
        }
    
        public static <T, U> Comparator<T> comparing(
                Function<? super T, ? extends U> keyExtractor,
                Comparator<? super U> keyComparator)
        {
            Objects.requireNonNull(keyExtractor);
            Objects.requireNonNull(keyComparator);
            return (Comparator<T> & Serializable)
                (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                                  keyExtractor.apply(c2));
        }
    
        public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
                Function<? super T, ? extends U> keyExtractor)
        {
            Objects.requireNonNull(keyExtractor);
            return (Comparator<T> & Serializable)
                (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
        }
    
    }
    • Comparator는 파라미터로 들어오는 두 객체를 비교하는 함수형 인터페이스입니다.
      • int compare(T o1, T o2);
    • 주로 정렬(sort)할 때 자주 사용합니다.
    • 추가적으로 이름이 비슷한 인터페이스인 Comparable는 자기 자신과 매개변수 객체를 비교하는 인터페이스입니다.
      • public int compareTo(T o);

    Comparator 인터페이스

    2.  기본 정렬(오름차순)

    • Comparator는 주로 정렬 시 많이 사용합니다.
    • 예시로 컬렉션(Collection)을 정렬해보겠습니다.
    • sort()는 매개변수로 Comparator를 받습니다.
    • sort()는 원본 데이터를 변경합니다. (예제에서는 sort()를 사용하지만 stream 사용을 권장합니다.)
    public interface List<E> extends Collection<E> {
    
        default void sort(Comparator<? super E> c) {
            Object[] a = this.toArray();
            Arrays.sort(a, (Comparator) c);
            ListIterator<E> i = this.listIterator();
            for (Object e : a) {
                i.next();
                i.set((E) e);
            }
        }
     }

     

    1. List<Integer> 정렬(오름차순)

    • 다양하게 Comparator를 구현할 수 있으나 Integer 클래스에 compareTo 메서드를 활용하는 게 가장 간단합니다.
    Comparator<Integer> integerComparator = Integer::compareTo;
    List<Integer> numberList = Arrays.asList(2,3,1,4);
    
    // 익명 클래스
    numberList.sort(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return (o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1);
        }
    });
    
    // 람다
    numberList.sort((o1, o2) -> (o1 < o2) ? -1 : ((o1 == o2) ? 0 : 1));
    
    // 람다 + Integer Method
    numberList.sort((o1, o2) -> o1.compareTo(o2));
    
    // 메소드 레퍼런스
    numberList.sort(Integer::compareTo);
    
    // 출력
    numberList.forEach(System.out::println);
    /*
    1
    2
    3
    4
    */

     

    2. List<String> 정렬(오름차순)

    • String 클래스에도 compareTo, compareToIgnoreCase(대소문자 무시)를 메소드를 활용할 수 있습니다.
    Comparator<String> stringComparator = String::compareTo;
    Comparator<String> stringIgnoreCaseComparator = String::compareToIgnoreCase;
    List<String> stringList = Arrays.asList("apple", "can", "be" ,"BE", "CAN", "APPLE");
    
    stringList.sort(String::compareTo); // 대소문자 구분O
    stringList.forEach(System.out::println);
    /*
    APPLE
    BE
    CAN
    apple
    be
    can
    */
    
    stringList.sort(String::compareToIgnoreCase); // 대소문자 구분X
    stringList.forEach(System.out::println);
    /*
    APPLE
    apple
    BE
    be
    CAN
    can
    */

    3. reversed()

    • 정렬 규칙 반대로 변경 (EX. 내림차순 -> 오름차순)

    1. List<Integer> 정렬 (내림차순)

    List<Integer> numberList = Arrays.asList(2,3,1,4);
    
    Comparator<Integer> integerComparator = Integer::compareTo;
    
    numberList.sort(integerComparator.reversed());
    numberList.forEach(System.out::println);
    /*
    4
    3
    2
    1
    */

     

    2. List<String> 정렬 (내림차순)

    List<String> stringList = Arrays.asList("apple", "can", "be" ,"BE", "CAN", "APPLE");
    
    Comparator<String> stringComparator = String::compareTo;
    Comparator<String> stringIgnoreCaseComparator = String::compareToIgnoreCase;
    
    stringList.sort(stringComparator.reversed());
    stringList.forEach(System.out::println);
    /*
    can
    be
    apple
    CAN
    BE
    APPLE
    */
    
    stringList.sort(stringIgnoreCaseComparator.reversed());
    stringList.forEach(System.out::println);
    /*
    can
    CAN
    be
    BE
    apple
    APPLE
    */

    4. reverseOrder() / naturalOrder()

    • Comparator의 static 메소드로 내림차순 정렬, 오름차순 정렬 기능을 수행합니다.
    • 위의 예시보다 더 간단하게 정렬을 수행할 수 있습니다.

    1. List<Integer> 정렬

    List<Integer> numbers = Arrays.asList(3,1,2,4);
    
    numbers.sort(Comparator.naturalOrder()); // 오름차순
    numbers.forEach(System.out::println);
    /*
    1
    2
    3
    4
    */
    
    numbers.sort(Comparator.reverseOrder()); // 내림차순
    numbers.forEach(System.out::println);
    /*
    4
    3
    2
    1
    */

     

    2. List<String> 정렬

    List<String> alphabetList = Arrays.asList("B", "A", "C", "D");
    
    alphabetList.sort(Comparator.naturalOrder()); // 오름차순
    alphabetList.forEach(System.out::println);
    /*
    A
    B
    C
    D
    */
    
    alphabetList.sort(Comparator.reverseOrder()); // 내림차순
    alphabetList.forEach(System.out::println);
    /*
    D
    C
    B
    A
    */

    5. comparing()

    • Comparator의 static 메소드로 기본형이 아닌 객체를 쉽게 정렬할 수 있도록 도와줍니다.
    • comparing(키 추출, 키 비교)
    Comparator<T> comparing(
    	Function<? super T, ? extends U> keyExtractor, 
    	Comparator<? super U> keyComparator)
    {
    	// (. . .) 생략
    }

    [Student 클래스 생성]

    public class Student {
    
        int age;
        String name;
        ClassName className;
    
        public enum ClassName {
            A, B, C
        }
    
        Student(int age, String name, ClassName className) {
            this.age = age;
            this.name = name;
            this.className = className;
        }
    
        public int getAge() {
            return age;
        }
    
        public String getName() {
            return name;
        }
    
        public ClassName getClassName() {
            return className;
        }
    }

     

    1. Student 객체 정

    • 원본 데이터 변경 방지를 위해 stream을 이용해 정렬했습니다.
    • Funtional in nature, 스트림이 처리하는 데이터 소스를 변경하지 않는다.
    List<Student> students = Arrays.asList(
            new Student(2, "Kim", Student.ClassName.A),
            new Student(3, "Shin", Student.ClassName.B),
            new Student(3, "Lee", Student.ClassName.C),
            new Student(2, "Kang", Student.ClassName.C),
            new Student(1, "Chul", Student.ClassName.A),
            new Student(1, "Jang", Student.ClassName.B),
            new Student(3, "Ahn", Student.ClassName.A),
            new Student(1, "Hawng", Student.ClassName.C),
            new Student(2, "Lim", Student.ClassName.B)
    );
    
    // 나이 순 정렬(오름차순)
    System.out.println("나이 순 정렬(오름차순)");
    Comparator<Student> comparingAge = Comparator.comparing(Student::getAge, Comparator.naturalOrder());
    students.stream()
            .sorted(comparingAge)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
    /*
    나이 순 정렬(오름차순)
    1 : Chul : A
    1 : Jang : B
    1 : Hawng : C
    2 : Kim : A
    2 : Kang : C
    2 : Lim : B
    3 : Shin : B
    3 : Lee : C
    3 : Ahn : A
    */
    
    
    // 이름 순 정렬(오름차순)
    System.out.println("이름 순 정렬(오름차순)");
    Comparator<Student> comparingName = Comparator.comparing(Student::getName, Comparator.naturalOrder());
    students.stream()
            .sorted(comparingName)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
    /*
    이름 순 정렬(오름차순)
    3 : Ahn : A
    1 : Chul : A
    1 : Hawng : C
    1 : Jang : B
    2 : Kang : C
    2 : Kim : A
    3 : Lee : C
    2 : Lim : B
    3 : Shin : B
    */
    
    // 반별 순 정렬(오름차순)
    System.out.println("반별 순 정렬(오름차순)");
    Comparator<Student> comparingClassName = Comparator.comparing(Student::getClassName, Comparator.naturalOrder());
    students.stream()
            .sorted(comparingClassName)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
    /*
    반별 순 정렬(오름차순)
    2 : Kim : A
    1 : Chul : A
    3 : Ahn : A
    3 : Shin : B
    1 : Jang : B
    2 : Lim : B
    3 : Lee : C
    2 : Kang : C
    1 : Hawng : C
    */

    6. thenComparing()

    • 정렬 규칙 추가합니다.
    • EX) 반 이름 정렬(오름차순) + 나이 정렬(내림차순)

    1. 예시

    List<Student> students = Arrays.asList(
            new Student(2, "Kim", Student.ClassName.A),
            new Student(3, "Shin", Student.ClassName.B),
            new Student(3, "Lee", Student.ClassName.C),
            new Student(2, "Kang", Student.ClassName.C),
            new Student(1, "Chul", Student.ClassName.A),
            new Student(1, "Jang", Student.ClassName.B),
            new Student(3, "Ahn", Student.ClassName.A),
            new Student(1, "Hawng", Student.ClassName.C),
            new Student(2, "Lim", Student.ClassName.B)
    );
    
    // 반 이름 정렬(오름차순)
    Comparator<Student> comparingClassName = Comparator.comparing(Student::getClassName, Comparator.naturalOrder());
    
    // 나이 정렬(내림차순)
    Comparator<Student> comparingAge = Comparator.comparing(Student::getAge, Comparator.reverseOrder());
    
    students.stream()
            .sorted(comparingClassName.thenComparing(comparingAge))
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
            
    /*
    3 : Ahn : A
    2 : Kim : A
    1 : Chul : A
    3 : Shin : B
    2 : Lim : B
    1 : Jang : B
    3 : Lee : C
    2 : Kang : C
    1 : Hawng : C
    */

    7. nullsFirst() / nullsLast()

    • 정렬 시 데이터에 null 이 존재함으로써 발생하는 NullPointerException를 처리할 수 있습니다.
    • null 데이터는 맨 앞에 정렬 / null 데이터는 맨 뒤에 정렬 (정렬 null 처리)

    1. NullPointerException 발생

    List<Student> students = Arrays.asList(
            new Student(2, "Kim", Student.ClassName.A),
            new Student(3, "Shin", Student.ClassName.B),
            new Student(3, "Lee", null),
            new Student(2, "Kang", null),
            new Student(1, "Chul", Student.ClassName.A),
            new Student(1, "Jang", Student.ClassName.B),
            new Student(3, "Ahn", Student.ClassName.A),
            new Student(1, "Hawng", null),
            new Student(2, "Lim", Student.ClassName.B)
    );
    
    // 반 이름 정렬(오름차순)
    Comparator<Student> comparingClassName = Comparator.comparing(Student::getClassName, Comparator.naturalOrder());
    
    students.stream()
            .sorted(comparingClassName)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });

     

    2. nullsFirst()

    // 반 이름 정렬(오름차순) + NULL First
    Comparator<Student> nullFirst = Comparator
            .comparing(Student::getClassName, Comparator.nullsFirst(Comparator.naturalOrder()));
    
    students.stream()
            .sorted(nullFirst)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
    /*
    3 : Lee : null
    2 : Kang : null
    1 : Hawng : null
    2 : Kim : A
    1 : Chul : A
    3 : Ahn : A
    3 : Shin : B
    1 : Jang : B
    2 : Lim : B
    */

     

    3. nullsLast()

    // 반 이름 정렬(오름차순) + NULL Last
    Comparator<Student> nullLast = Comparator
            .comparing(Student::getClassName, Comparator.nullsLast(Comparator.naturalOrder()));
    
    students.stream()
            .sorted(nullLast)
            .forEach(o -> {
                System.out.println(o.getAge() + " : " + o.getName() + " : " + o.getClassName());
            });
    /*
    2 : Kim : A
    1 : Chul : A
    3 : Ahn : A
    3 : Shin : B
    1 : Jang : B
    2 : Lim : B
    3 : Lee : null
    2 : Kang : null
    1 : Hawng : null
    */