ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Comparable 인터페이스
    Java 2019. 7. 29. 00:30
    반응형

     Comparable 인터페이스는 compareTo() 메서드를 통해 객체 간의 순서를 비교할 수 있도록 해줍니다. 즉, Comparable 인터페이스를 구현한 클래스는 그 인스턴스들에게 순서가 존재한다는 뜻입니다.

     Comparable 인터페이스를 구현하면 객체들의 배열을 Arrays.sort() 메서드를 통해 아래와 같이 손쉽게 정렬할 수 있습니다.

    public class Node implements Comparable<Node> {
    
        private String content;
        private int order;
    
        public Node(String content, int order) {
            this.content = content;
            this.order = order;
        }
    
        @Override
        public int compareTo(Node o) {
            return Integer.compare(order, o.order);
        }
    
        @Override
        public String toString() {
            return "content : " + content + ", order : " + order;
        }
    }
        @Test
        public void simpleOrderingNode() {
            Node[] nodes = new Node[3];
            nodes[0] = new Node("five", 5);
            nodes[1] = new Node("three", 3);
            nodes[2] = new Node("one", 1);
    
            Arrays.sort(nodes);
            Arrays.stream(nodes)
                    .forEach(n -> System.out.println(n.toString()));
    
    	//result
    	//content : one, order : 1
    	//content : three, order : 3
    	//content : five, order : 5
        }
    

     


    compareTo() 메서드의 일반 규약

     이 객체와 비교 대상 객체의 순서를 비교합니다. 이 객체가 주어진 대상 객체보다 작으면 음의 정수를, 같으면 0, 크면 양의 정수를 반환합니다. 만약 이 객체와 비교할 수 없는 타입의 객체가 주어진다면 ClassCastException 예외를 던집니다.

     

    • Comparable을 구현한 클래스는 모든 x, y에 대해 (x.compareTo(y)) == -(y.compareTo(x)) 여야 합니다.
    • Comparable을 구현한 클래스는 추이성을 보장해야 합니다.
      x.compareTo(y) > 0 && y.compareTo(z) > 0 이면 x.compareTo(z) > 0
    • Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0 이면 x.compareTo(z) == y.compareTo(z) 여야 합니다.
    • 다음 규약은 필수는 아니지만 지키는 것이 좋습니다. (x.compareTo(y) == 0) == (x.equals(y)) 여야 합니다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 합니다.

     compareTo() 메서드에서 기본 타입 필드를 비교할 때는 " > " 나 " < " 와 같은 관계 연산자보다는 박싱된 기본 타입 클래스의 정적 메서드인 compare() 메서드를 이용하는 것이 좋습니다. ex) Integer.compare(x, y)

     

     클래스에서 비교할 핵심 필드가 여러 개일 때는 가장 핵심적인 필드부터 차례대로 비교해 나가면 됩니다. 비교 도중, 비교 결과가 0이 아니라면 두 객체 간의 순서가 거기서 결정이 됩니다.

    public class SampleNumber implements Comparable<SampleNumber> {
        private int first;
        private int second;
        private int third;
    
        public SampleNumber(int first, int second, int third) {
            this.first = first;
            this.second = second;
            this.third = third;
        }
    
        @Override
        public int compareTo(SampleNumber o) {
            int result = Integer.compare(first, o.first);
    
            if (result == 0) {
                result = Integer.compare(second, o.second);
    
                if (result == 0) {
                    result = Integer.compare(third, o.third);
                }
            }
            return result;
        }
    
        @Override
        public String toString() {
            return "[first : " + first + "]" + "[second : " + second + "]" + "[third : " + third + "]";
        }
    }
        @Test
        public void 핵심필드가_여러개일때() {
            List<SampleNumber> sampleNumbers = Arrays.asList(
                    new SampleNumber(11, 22, 33),
                    new SampleNumber(11, 21, 33),
                    new SampleNumber(10, 25, 34),
                    new SampleNumber(15, 1, 4)
            );
            sampleNumbers.stream()
                    .forEach(num -> System.out.println(num.toString()));
    
            System.out.println("-------------------");
            Collections.sort(sampleNumbers);
    
            sampleNumbers.stream()
                    .forEach(num -> System.out.println(num.toString()));
        }
    

     

     SampleNumber 클래스의 compareTo() 클래스를 보면 비교 대상인 핵심 필드가 여러 개가 존재하다보니 값을 비교하는 if 문이 중첩적으로 발생하여 가독성이 떨어집니다. 이러한 코드는 아래와 같이 비교자 생성 메서드(comparingInt(), thenComparingInt()) 와 람다를 이용하여 메서드 체이닝 방식으로 간결하게 작성할 수 있습니다. 단 약간의 성능 저하가 뒤따릅니다.

    public class SampleNumber implements Comparable<SampleNumber> {
        private int first;
        private int second;
        private int third;
    
        public SampleNumber(int first, int second, int third) {
            this.first = first;
            this.second = second;
            this.third = third;
        }
    
        @Override
        public int compareTo(SampleNumber o) {
            return COMPARATOR.compare(this, o);
        }
    
        private static final Comparator<SampleNumber> COMPARATOR =
                Comparator.comparingInt((SampleNumber num) -> num.first)
                        .thenComparingInt(num -> num.second)
                        .thenComparingInt(num -> num.third);
    
        @Override
        public String toString() {
            return "[first : " + first + "]" + "[second : " + second + "]" + "[third : " + third + "]";
        }
    }

     


     

     알고리즘 문제를 풀다보면 자주 사용하게 되는 PriorityQueue<E>에서도 Comparable를 구현함으로써 손쉽게 우선순위 큐를 구현할 수 있습니다.

        @Test
        public void priorityQueueTest() {
            PriorityQueue<SampleNumber> priorityQueue = new PriorityQueue<>();
                    
            priorityQueue.add(new SampleNumber(4, 2, 4));
            priorityQueue.add(new SampleNumber(4, 1, 4));
            priorityQueue.add(new SampleNumber(1, 40, 40));
            priorityQueue.add(new SampleNumber(1, 29, 29));
    
            for (Iterator<SampleNumber> iterator = priorityQueue.iterator(); iterator.hasNext(); ) {
                SampleNumber next = iterator.next();
                System.out.println(next.toString());
            }
        }


     만약 내림 차순으로 구현할 경우, 간단하게 PriorityQueue의 생성자 매개변수에 Comparator를 주입시켜주면 됩니다.

    아래의 코드는 간결하게 람다로 작성하여 내림차순 정렬이 되도록 우선순위 큐를 구현한 것입니다.

        @Test
        public void priorityQueueTest() {
            PriorityQueue<SampleNumber> priorityQueue =
                    new PriorityQueue<>((num_1, num_2) -> num_2.compareTo(num_1));
                    
            priorityQueue.add(new SampleNumber(4, 2, 4));
            priorityQueue.add(new SampleNumber(4, 1, 4));
            priorityQueue.add(new SampleNumber(1, 40, 40));
            priorityQueue.add(new SampleNumber(1, 29, 29));
    
            for (Iterator<SampleNumber> iterator = priorityQueue.iterator(); iterator.hasNext(); ) {
                SampleNumber next = iterator.next();
                System.out.println(next.toString());
            }
        }

     

    마무리

     순서를 고려해야 하는 값 클래스를 작성한다면 Comparable 인터페이스를 구현하여, 그 인스턴스들이 컬렉션과 어우러지도록 하는 것이 좋습니다.  그리고 compareTo() 메서드에서는  " < " 나 " > " 와 같은 관계 연산자를 사용하여 비교하기 보다는 박싱된 기본 타입 클래스가 제공하는 compare() 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드(comparingInt(), thenComparingInt(), ...) 를 사용하는 것이 좋습니다.

     


     참고자료

    이펙티브 자바 3rd [인사이트 출판]

    반응형

    댓글

Designed by Tistory.