Java

[Java] 자바의 두가지 정렬 방법(Comparable, Comparator)

nooblette 2024. 8. 1. 22:24

목차

    배경

    자바의 두가지 정렬 방법에 대해 알아본다. 정렬 메커니즘 중에 호출하게되는 compareTo()와 compare() 메서드를 이용한 객체 값 비교는 실무에서도 유용하고 미묘한 차이로 다르게 동작하는 케이스도 있어 정리하고 숙지해두면 매우 유용할 것 같다.

    Comparable 인터페이스와 compareTo() 메서드

    Comparable을 상속받는 클래스 내부에 compareTo() 메서드를 구현한다. Comparable은 java.lang.Comparable 패키지에 선언되어있다. Comparable 인터페이스의 추상 메서드인  compareTo()에 정렬 기준을 작성한다.
     
    Comparable 인터페이스와 compareTo()의 메서드 시그니쳐는 다음과 같다.

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

     
    Comparable 인터페이스를 상속받는 동일한 타입의 객체는 compareTo 메서드로 비교할 수 있다.
    동일하다면 0, 현재 객체가 비교 대상 객체보다 크다면 양수 (= 비교 대상 객체가 앞으로 와야 함), 현재 객체가 비교 대상 객체보다 작다면 음수 (= 비교 대상 객체가 뒤에 머물러야 함)를 반환한다.
     
    String, Integer, Date 등 자바의 기본 클래스들이 Comparable 인터페이스를 구현하고 있다.

    예시) Comparable 인터페이스를 구현하는 Person 클래스

    public class Person implements Comparable<Person> {
        private int age;
    
        public Person(int age) {
            this.age = age;
        }
    
        public int getAge() {
            return age;
        }
    
    
    	// 정렬 기준 작성
        @Override
        public int compareTo(Person other) {
            return Integer.compare(this.age, other.age);
        }
    		
    		... 생략
    }

     
    실행 결과는 다음과 같다.

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Person> people = new ArrayList<>();
            people.add(new Person(30));
            people.add(new Person(25));
            people.add(new Person(35));
    
            Collections.sort(people);
    
            for (Person person : people) {
                System.out.println(person.getAge());
            }
        }
    }
    
    >>>
    25
    30
    35

     

    Comparator 인터페이스와 compare() 메서드

    java.util.Comparator 패키지에 정의되어 있는 Collections.sort()에 정의된 Comparator 인터페이스를 사용하여 정렬 기준을 구현한다. 비교 대상이 되는 두 객체를 인자로 전달하여 비교한다.
    두 객체가 동일하다면 0, 첫번째 인자의 객체가 두번째 인자로 전달한 객체보다 크다면 양수 (= 비교 대상 객체가 앞으로 와야 함), 첫번째 인자의 객체가 두번째 인자로 전달한 객체보다 작다면 음수 (= 비교 대상 객체가 뒤에 머물러야 함)를 반환한다.

    public class PersonNameComparator implements Comparator<Person> {
        @Override
        public int compare(Person p1, Person p2) {
            return p1.getName().compareTo(p2.getName());
        }
    }

     

    예시) Comparator로 두 Person 객체를 비교

    실행 결과는 다음과 같다.

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Person> people = new ArrayList<>();
            people.add(new Person("Alice", 30));
            people.add(new Person("Bob", 25));
            people.add(new Person("Charlie", 35));
    
            Collections.sort(people, new PersonNameComparator());
    
            for (Person person : people) {
                System.out.println(person.getName() + ", " + person.getAge());
            }
        }
    }
    
    >>>
    Alice, 30
    Bob, 25
    Charlie, 35

     

    람다식으로 작성하기

    Comparator의 compare() 메서드를 상속받는 객체 혹은 익명 객체를 직접 정의하지 않고 람다식으로 간결하게 작성할 수도 있다.

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Person> people = new ArrayList<>();
            people.add(new Person("Alice", 30));
            people.add(new Person("Bob", 25));
            people.add(new Person("Charlie", 35));
    
            // 람다식을 이용한 정렬
            Collections.sort(people, (p1, p2) -> p1.getName().compareTo(p2.getName()));
    
            // 스트림을 이용한 정렬 및 출력
            people.stream()
                .sorted((p1, p2) -> p1.getName().compareTo(p2.getName()))
                .forEach(person -> System.out.println(person.getName() + ", " + person.getAge()));
        }
    }

    compareTo() 메서드와 equals() 메서드

    API 테스트를 진행하던 중에 분명 값이 동일함에도 값이 달라서 400 에러를 반환하는 경우가 발생한 적이 있다.
    당시에 두 값은 5와 5.0이였는데 eqauls() 메서드로 값이 동일한지 비교하고 있었다. 둘 다 BigDecimal 타입으로 선언되어 있어 타입 에러로인한 이슈(e.g. Integer와 Double)는 아닐 것이라고 생각했지만, 이 부분으로 인해 400 에러를 뱉고 있었고 테스트를 더이상 진행하지 못했다.
     
    그 원인으로는 equal() 메서드의 동작 방식이 있었고, 대안으로 compareTo() 메서드를 사용하는 것으로 해결했다.
     
    Java에서 BigDecimal 타입을 비교할 때 compareTo() 메서드를 사용하면 5와 5.0을 비교할 수 있다.
    이 메서드는 두 수치의 값을 비교하여 0을 반환한다(위 내용 참고). 즉, compareTo() 메서드는 두 값이 수치적으로 같은지를 확인하는 것이므로 5와 5.0은 동일하다고 간주한다.
     
    반면, equals() 메서드는 BigDecimal 객체의 스케일(소수점 이하의 자리수)까지 고려하여 비교한다.
    즉, equals 메서드를 사용하면 5와 5.0은 다른 것으로 취급되며, false를 반환한다. 이 미묘한 차이로 실패가 발생한다.
     

    예시)

    import java.math.BigDecimal;
    
    public class BigDecimalComparison {
        public static void main(String[] args) {
            BigDecimal a = new BigDecimal("5");
            BigDecimal b = new BigDecimal("5.0");
    
            // compareTo method - compares only the value
            if (a.compareTo(b) == 0) {
                System.out.println("Using compareTo: 5 and 5.0 are equal.");
            } else {
                System.out.println("Using compareTo: 5 and 5.0 are not equal.");
            }
    
            // equals method - compares value and scale
            if (a.equals(b)) {
                System.out.println("Using equals: 5 and 5.0 are equal.");
            } else {
                System.out.println("Using equals: 5 and 5.0 are not equal.");
            }
        }
    }
    
    >>>
    Using compareTo: 5 and 5.0 are equal.
    Using equals: 5 and 5.0 are not equal.