ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java 파먹기] 정렬 기준 : Comparable & Comparator
    JAVA 2025. 4. 17. 01:25
    728x90
    반응형

    개요

    Java에서 기본적으로 제공하는 인터페이스 Comparable과 Comparator에 대해 알아보겠습니다.

    기본적으로 Comparable과 Comparator는 제네릭 타입으로 동작하기 때문에 원시 타입은 사용할 수 없습니다.

    Comparable

    객체 내부의 요소를 활용한 정렬이 필요한 경우 사용됩니다.

    • java.lang 패키지에서 제공합니다.
    • 정렬이 필요한 객체 내부에 compareTo를 구현해야 합니다.
    @Getter
    public class Coupon implements Comparable<Coupon> {
        private int maxDiscount;
        private String name;
        
        public Coupon(int maxDiscount, String name) {
            this.maxDiscount = maxDiscount;
            this.name = name;
        }
    
        @Override
        public int compareTo(Coupon c) {
        	// check : 앞에 요소가 기준입니다. (지금 예시는 오름차순)
            // check : Integer.compare()은 Comparator가 아닙니다.
            return Integer.compare(this.maxDiscount, c.getMaxDiscount());
        }
    }

    1. 정렬 기준 및 반환 값

    Comparable는 비교 결과에 따라 -1, 0, 1을 반환하게 됩니다.

    • 항상 앞에 있는 값이 기준이 됩니다.
    Coupon coupon1 = new Coupon(3000, "최저가 쿠폰");
    Coupon coupon2 = new Coupon(1000, "회원가입 쿠폰");
    Coupon coupon3 = new Coupon(3000, "사장님이 미쳤어요");
        
    System.out.println(coupon2.compareTo(coupon1)); // -1
    System.out.println(coupon1.compareTo(coupon3)); // 0
    System.out.println(coupon3.compareTo(coupon2)); // 1

    2. 여러 요소 정렬 기준

    위에서 확인 한 정렬 기준 및 반환값을 활용하면 여러 요소를 기준으로 정렬할 수 있게 됩니다.

    @Override
    public int compareTo(Coupon c) {
        return Integer.compare(c.getMaxDiscount(), this.maxDiscount);
        
        int maxDiscountComparison = Integer.compare(c.getMaxDiscount(), this.maxDiscount);
        
        if(maxDiscountComparison != 0) {
            return maxDiscountComparison;
        }
        
        return this.name.compareTo(c.getName());
    }

     

    3. 재미로 확인하는 예시 내부 코드 확인하기

    Collection.sort()의 내부 코드를 확인하여 어떻게 compareTo가 적용되는지 확인할 수 있습니다.

    // java.util.Collections
    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null); // ⭐
    }
    
    // java.util.List
    @SuppressWarnings({"unchecked", "rawtypes"})
    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);
        }
    }
    
    // java.util.Array
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a); // ⭐
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
    
    // java.util.ComparableTimSort
    private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;
    
        // Find end of run, and reverse range if descending
        if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending
            // ⭐
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else { // Ascending
            while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)
                runHi++;
        }
    
        return runHi - lo;
    }

    4. Comparable 구현시 권장사항

     

    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

    공식 문서를 확인해보면 only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) 라는 내용이 있는 것을 확인 해볼 수 있는데 이 내용은 필수는 아니지만 엄청 권장하고 있습니다. 

    해당 내용을 지키지 않을 시 예기치 못한 동작을 유발할 수 있다고 하지만 아직 예시를 찾지는 못했습니다...

    5. 짧은 의견

    Null 처리를 직업해줘야 하며 메소드 체이닝이 불가능하다는 단점을 가지고 있습니다.

    실무에서는 해당 객체를 다양한 곳에서 일관된 정렬 기준이 필요할때 사용하면 좋을 것 같습니다.

    Comparator

    외부에서 정렬의 기준을 정의가 필요한 경우 사용됩니다.

    • java.util 패키지에서 제공합니다.
    • 별도로 compare를 정의 해야 합니다.
    Java 8 이후 부터는 람다식을 활용하여 Comparator를 쉽게 구현 가능하기 때문에 실무에서 주로 사용되며 메소드 체이닝으로 가독성이 좋습니다.
    @Getter
    public class Coupon {
        private Long maxDiscount;
        private String name;
    
        public Coupon(Long maxDiscount, String name) {
            this.maxDiscount = maxDiscount;
            this.name = name;
        }
    }

    1. Comparator 분리

    별도의 Class로 분리하여 재사용성을 높일 수 있습니다.

    • 아래 코드가 동작하는 이유는 위에 내부 코드에서 확인할 수 있습니다.
    public class CouponComparator implements Comparator<Coupon> {
        @Override
        public int compare(Coupon o1, Coupon o2) {
        	// 오름 차순
            return o1.getMaxDiscount().compareTo(o2.getMaxDiscount());
        }
    }
    
    List<Coupon> coupons = new ArrayList<>();
    coupons.add(new Coupon(2000L, "최저가 쿠폰");
    coupons.add(new Coupon(1000L, "회원가입 쿠폰");
    coupons.add(new Coupon(3000L, "벚꽃 쿠폰");
    
    Collections.sort(coupons, new CouponComparator());

    2. 람다식 활용

    Java 8 이후로 람다식을 활용하면 기존 코드를 획기적으로 줄일 수 있게 됩니다.

    List<Coupon> coupons = new ArrayList<>();
    coupons.add(new Coupon(2000L, "최저가 쿠폰");
    coupons.add(new Coupon(1000L, "회원가입 쿠폰");
    coupons.add(new Coupon(3000L, "벚꽃 쿠폰");
    
    // 1차 람다식
    coupons.sort((o1, o2) -> o1.getMaxDiscount().compareTo(o2.getMaxDiscount());
    
    // 2차 람다식
    coupons.sort(Comparator.comparing(Coupon::getMaxDiscount));

    3. 메소드 체이닝 (여러 조건)

    여러 기준과 조건으로 정렬할 수 있습니다.

    아래 코드는 우선 maxDiscount로 정렬 후 같다면 name으로 정렬하게 됩니다.

    // MaxDiscount로 정렬 후 같다면 Name으로 정렬
    coupons.sort(Comparator.comparing(Coupon::getMaxDiscount).thenComparing(Coupon::getName));
    
    // 해당 정렬의 역순
    coupons.sort(Comparator.comparing(Coupon::getMaxDiscount).reversed());

    4. NULL 처리 방법

    Comparator의 경우 Null 요소에 대해 손쉽게 처리가 가능합니다.

    // Coupon이 null인 경우 처리 가능
    Comparator<Coupon> couponSort = Comparator.nullsLast(
        Comparator.comparing(Coupon::getMaxDiscount)
    );
    
    // Coupon이 null인 경우와 MaxDiscount가 null인 경우 모두 처리 가능
    // - maxDisCount가 null인게 가장 마지막으로 정렬된다.
    // - 만약 앞으로 정렬하고 싶다면 nullsFirst를 사용하면 된다.
    // - 기본적으로 naturalOrder()는 오름 차순을 의미한다.
    Comparator<Coupon> couponSort = Comparator.nullsLast(
        Comparator.comparing(Coupon::getMaxDiscount, Comparator.nullsLast(Comparator.naturalOrder()))
    );
    
    // 내림 차순
    Comparator<Coupon> couponSort = Comparator.nullsLast(
        Comparator.comparing(Coupon::getMaxDiscount, Comparator.nullsLast(Comparator.reverseOrder()))
    );
    
    // 직접 구현 (오름 차순)
    // - 만약 maxDiscount의 타입이 원시타입 이라면?
    Comparator<Coupon> couponSort = Comparator.nullsLast(
        Comparator.comparing(Coupon::getMaxDiscount, Comparator.nullsLast((a, b) -> a - b))
    );

    결론

    굳이 사용 목적에 따라 구분한다면 아래와 같습니다.

    • 클래스 내부에 정렬 기준이 고정되어야 하고 일관된 기준이 필요한 경우라면 Comparable
    • 정렬 기준이 가변적이고 유연한 정렬이 필요한 경우라면 Comparator

    개인적인 생각으로 Comparator는 Comparable의 장점을 모두 가지고 있기 때문에 실무에서는 Comparable을 사용하는 것을 추천합니다.

    참고사항

    Hash 기반 자료 구조는 내부적으로 hashCode와 Equals만을 기준으로 효율적으로 데이터를 저장하고 조회하기 때문에 Comparable, Comparator 같은 정렬 기준을 사용할 수 없습니다.

    728x90
    반응형
Designed by Tistory.