ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 컬렉션 - Comparable와 Comparator
    백엔드/자바 2020. 8. 17. 04:01

    Comparable와 Comparator는?

    객체의 정렬에 필요한 메서드(정렬 기준 제공)을 정의한 인터페이스.

     

    Comparable은 특정 클래스의 기본 정렬 기준(디폴트)를 구현하기 위해 사용된다.

     

    반면 Comparator는 기본 정렬이 아닌 그 이외의 정렬을 사용하고자 할 때 사용된다.

     

    Comparable과 Comparator는 모두 인터페이스로 이를 클래스에 구현해서 정렬 순서를 정할 수 있다.

     

    Comparable을 구현할 경우는 compareTo() 메서드를 구현해야 한다.

     

    Comparator를 구현할 경우는 compare() 메서드를 구현해야 한다.

     

    Comparable의 compareTo() 메서드는 1개 객체를 매개변수로 받는다.

     

    즉 compareTo()는 해당 메서드를 호출한 객체와 매개변수로 받은 객체를 비교한다.

     

    반면 Comparator의 compare() 메서드는 2개의 객체를 매개변수로 받아서 비교한다.

     


    인터페이스


    구현해야 할 메서드


    메서드가 받을 매개 변수


    Comparable


    compareTo()


    1개


    Comparator


    compare()


    2개


     

    Comparable과 Comparator의 필요성

    위에서 이미 Comparable과 Comparator에 대해 언급했지만, 본격적으로 들어가기 전에

     

    이 두개의 인터페이스가 왜 필요한지부터 알아보도록 하자.

     

     

     

    예를 들어 위와 같이 Name이라는 이름을 가진 클래스가 존재한다고 가정해보자.

     

    멤버 변수라곤 Name하나에 생성자 두개, toString 메서드로 구성된 간단한 클래스다.

     

     

     

    이제 리스트를 하나 만들고 리스트에 Num의 객체를 저장한다.

     

    이때 리스트 내에서 Num 클래스가 가진 숫자를 오름차순으로 정렬하고자 한다.

     

    그래서 Collections에서 제공하는 sort() 메서드를 이용해 정렬했다.

     

    그 다음 콘솔창에 정렬이 완료된 List를 출력해봤다. 결과는 과연 어떨까?

     

     

     

    결과는 위와 같이 실행 과정에서 오류가 나타났다. 근데 오류 메시지에서 다음과 같은 내용을 볼 수 있다.

     

    "Name cannot be cast to java.lang.Comparable"

     

    Name 클래스를 Comparable로 캐스트하지 못했다는 뜻인데, 이게 무슨뜻일까?

     

    우선 그 전에 다른 테스트를 한번 진행해보자.

     

     

    이번에는 List 안에 String 객체를 생성해서 이름을 입력해보자.

     

     

     

    결과는 위와 같다. 별도로 클래스를 만든 것과 달리 String 객체를 생성했을 때 오류가 없다.

     

    정렬도 제대로 됐고 출력도 정상적으로 잘 진행됐다. 왜 그럴까?

     

     

    우선 String 클래스을 들여다보자.

     

     

     

    빨간 박스 안을 보면 위에서 이야기했던 String클래스는 Comaparable을 구현했다.

     

    그리고 Comparable 구현하기 위해 compareTo()메서드까지 구현한 것을 볼 수 있다.

     

     

     

    반면 이전에 만들어둔 클래스인 Name에는 Comparable, ComparaTo() 모두 구현하지 않았다.

     

    이제 위에서 언급한 "Name cannot be cast to java.lang.Comparable"를 이해할 수 있을 것이다.

     

    Name 클래스는 Comparable 인터페이스를 구현하지 않아 정렬을 할 수 없었던 것이다.

     

    sort( )는 혼자서 정렬 기준을 만들고 모든 것을 뚝딱 정렬할 수 있는 메서드가 아니다.

     

    Comparable은 특정 클래스를 통해 만들어진 객체들의 정렬을 위해 존재하는 인터페이스다.

     

    Comparable 인터페이스의 구현을 위한 ComareTo()메서드는 클래스 내부에 구현하게 되있으며,

     

    정렬을 위해 쓰는 sort() 메서드를 사용할 때 정렬의 기준이 된다.

     

    즉 Comparable 구현은 클래스의 기본 정렬을 위한 인터페이스다.

     

    반면 Comparator는 클래스가 정렬을 할 때 새로운 기준을 주기 위해 존재한다.

     

    Comparator는 클래스 외부에 존재하며, 특정 클래스에게 새로운 정렬 기준을 부여하고 싶을 때 쓴다.

     

    더 자세한 사항은 예시로 보도록 하자.

     

    Comparable의 구현

    Comparable의 경우 클래스 내부에 구현하게 되고 compareTo() 메서드를 구현해주면 된다.

     

     

     

    class Student {
    
        // 멤버변수
        String name;
        int age;
    
        // 기본 생성자
        Student(){}
    
        // 매개변수를 가진 생성자
        Student(String name, int age){
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return name+":"+age;
        }
    }

     

    위와 같이 Student클래스가 있다고 가정하고, 이제 해당 클래스에 Comparable을 구현해보자.

     

     

     

    우선 위와 같이 클래스 옆에 Comparable 구현하겠다고 코드를 입력해준다.

     

     

     

    다음은 위와 같이 compareTo()을 오버라이딩해준다.

     

     

     

    코드를 완성했다면 위와 같은 스크린샷처럼 될 것이다.

     

     

     

    클래스 내에 compareTo()를 구현해줬다면 위와 같이 메인 메서드에서 입력하고 실행해보자.

     

     

     

    이번에는 오류없이 출력이 잘 된 것을 확인할 수 있다.

     

    근데 내용을 보자. 무언가 이상하다.

     

    바로 정렬이 안되있는 것을 확인할 수 있을 것이다.

     

    앞서 말했듯 Comparable은 클래스 기본 정렬을 위해 존재한다.

     

    근데 이 Comparable을 구현할 때 쓰는 compareTo() 메서드의 내용이 비어있기 때문에 정렬이 안된 것이다.

     

    이제 남은건 compareTo()의 내부에 정렬을 위한 코드를 입력하는 것이다.

     

    본인이 직접 만드는 클래스의 경우 본인이 직접 정렬 기준을 정할 수 있다.

     

    여기서는 이름과 나이 둘 중 하나를 기준으로 하는데, 이름을 오름차순으로 정렬되도록 할 예정이다.

     

     

     

    compareTo() 메서드는 위와 같이 구현해주면 된다.

     

    우선 전달된 객체가 Student 객체가 맞는지 확인하고, 맞다면 Student 객체로 형변환한다.

     

    그 다음 현재 객체의 이름인 name과 전달된 객체 s의 이름값을 비교해준다.

     

    이때는 String 자료형을 쓸 때 사용 가능한 compareTo를 이용해준다.

     

    앞서 말했듯 반환값이 int이므로 String일 경우엔 따로 메서드를 만들 필요 없이 compareTo를 쓰면 된다.

     

    만약 숫자를 비교할 때는 각 숫자끼리 뺄셈을 해서 결과가 양수면 1, 같으면 0, 음수면 -1을 반환하면 된다.

     

    이런식으로 하면 오름차순이 구현되고, 내림 차순은 반대로 하면 된다.

     

    잘 모르면 아래 조건을 외우자.

     

    - 오름 차순: 두 숫자를 뺄셈을 했을 때 결과가 양수면 1, 같으면 0, 음수면 -1

    - 내림 차순: 두 숫자를 뺄셈을 했을 때 결과가 양수면 -1, 같으면 0, 음수면 1

     

    이제 코드를 구현했으니 결과를 보도록 하자.

     

     

     

    이번엔 의도한대로 이름순으로 제대로 구현이 된 것을 볼 수 있다.

     

    만약 내림차순으로 구현하고 싶다면 return할때 - (마이너스)를 붙여주기만 하면 된다.

     

     

     

     

    단순히 마이너스 표시 하나만 붙여주는 것으로 내림차순으로 바뀌는 것을 볼 수 있다.

     

    전체코드는 다음과 같다.

     

     

    class Student implements Comparable{
    
        // 멤버변수
        String name;
        int age;
    
        // 기본 생성자
        Student(){}
    
        // 매개변수를 가진 생성자
        Student(String name, int age){
            this.name = name;
            this.age = age;
        }
    
        // toString 오버라이딩
        @Override
        public String toString() {
            return name+":"+age;
        }
    
        // compareTo 오버라이딩
        @Override
        public int compareTo(Object o) {
    
            // 파라미터로 전달된 객체가 Student객체인지 확인
            if(!(o instanceof Student)){
                return 0;
            }
    
            // 만약 Student 객체가 맞다면 형 변환
            Student s = (Student) o;
    
            // 현재 객체의 name과 전달된 객체의 name값을 비교
            // 만약 현재 객체 name값이 크면 -1, 같으면 0, 작으면 1
            return (this.name).compareTo(s.name);
        }
    }
    

     

    Comparator의 구현

    이전 단락에서 Comparable의 사용법에 대해서 익혀봤다.

     

    Comparable의 특징은 다음과 같다.

     

    - 특정 클래스의 기본 정렬 기준으로 사용됨. ( sort() 메서드의 기준이 됨 )

    - 클래스 내부에 코드를 구현함.

    - compareTo()를 구현해야 함.

     

    이제 따른 의문이 생겼다.

     

    우리는 우선 Comparable을 통해 객체의 이름을 오름차순으로 정렬하도록 기본 기준을 설정했다.

     

    내림 차순을 하고 싶다면 마이너스 하나만 붙여줘도 되니 불편할게 없다.

     

    그렇다면 만약 나이 순으로 정렬하고 싶다면? 혹은 클래스 내부를 수정할 수 없다면?

     

    불편하게 다시 코드를 뜯어 고쳐야할까?

     

    답변부터 말하자면 아니다. 이럴 때를 위해 사용해주는 것이 바로 Comparator다.

     

    Comparator의 특징은 다음과 같다.

     

    - 특정 클래스의 기본 정렬 기준 외 다른 기준을 줄 때 사용됨.

    - 다른 기준을 추가로 주는 것이므로 sort() 메서드 사용시 별도로 표기해야 함.

    - 클래스 외부에 코들르 구현함.

    - compare()를 구현해야 함.

     

    설명이 어렵다면 이제 나이를 기준으로 새로운 정렬 기준을 만들어주기 위한 Comparator를 실습해보자.

     

     

     

     

    우선 위와 같이 age_sort라는 클래스를 만들고 Comparator를 구현하도록 하자.

     

     

     

     

    위에서도 언급했듯 Comprator인터페이스 구현을 위해 compare()메서드도 구현하도록 하자.

     

    이대로라면 이전 단락에서 Comparable 구현할 때와 같이 아무런 반응도 없을 것이다.

     

    그러므로 이전에 Comprable에서 CompareTo를 구현하듯이 코드를 입력해보도록 하자.

     

     

     

    코드를 보면 CompareTo()와 큰 차이가 없다.

     

    그냥 들어온 두 객체를 비교해서 숫자 1,0,-1 셋 중 하나만 반환하면 된다.

     

    이제 직접 이 Comparator를 구현한 클래스를 사용해서 정렬을 해보자.

     

     

     

    메인 메서드의 코드는 위와 같다.

     

    기존 코드와 차이라면 sort() 메서드를 사용해서 정렬할 때 새로 만든 클래스의 객체를 전달해준 것 뿐이다.

     

    이렇게 할 경우 sort()는 클래스 내부의 compareTo()를 사용하는 것이 아니라

     

    Comparator를 구현한 클래스로 새로 만들어져 전달된 객체 안의 compare()를 사용하게 된다.

     

     

     

    결과는 위와 같다. 나이를 기준으로 오름차순 정렬한 것을 볼 수 있다.

     

    Comparator는 위에서 이야기한대로 클래스 외부에 만들어지고

     

    sort() 메서드를 사용할 때 객체로 제공되어 새로운 정렬 기준으로 사용되는 것을 알 수 있다.

     

    전체 코드는 다음과 같다.

     

     

    public class Map_Test {
    
        public static void main(String[] args) {
    
            List list = new ArrayList();
            
            list.add(new Student("나영인", 12));
            list.add(new Student("김철수", 15));
            list.add(new Student("한의준", 14));
            list.add(new Student("장민희", 10));
    
            Collections.sort(list, new age_sort());
    
            System.out.println(list);
        }
    }
    
    class Student implements Comparable{
    
        // 멤버변수
        String name;
        int age;
    
        // 기본 생성자
        Student(){}
    
        // 매개변수를 가진 생성자
        Student(String name, int age){
            this.name = name;
            this.age = age;
        }
    
        // toString 오버라이딩
        @Override
        public String toString() {
            return name+":"+age;
        }
    
        // compareTo 오버라이딩
        @Override
        public int compareTo(Object o) {
    
            // 파라미터로 전달된 객체가 Student객체인지 확인
            if(!(o instanceof Student)){
                return 0;
            }
    
            // 만약 Student 객체가 맞다면 형 변환
            Student s = (Student) o;
    
            // 현재 객체의 name과 전달된 객체의 name값을 비교
            // 만약 현재 객체 name값이 크면 -1, 같으면 0, 작으면 1
            return (this.name).compareTo(s.name);
        }
    }
    
    class age_sort implements Comparator{
    
        @Override
        public int compare(Object o1, Object o2) {
    
            // o1, o2 객체가 모두 Student 객체인지 확인
            if(!(o1 instanceof Student && o2 instanceof Student)){
                return 0;
            }
    
            // 두 객체 모두 Student객체가 맞다면 형변환
            Student s1 = (Student) o1;
            Student s2 = (Student) o2;
    
            // 두 객체의 나이값을 비교
            // s1 객체의 나이가 s2객체의 나이보다 크면 1 반환, 같으면 0반환, 작으면 -1 반환
            return ((s1.age-s2.age)>0)?1:(s1.age==s2.age)?0:-1;
        }
    }

     

    Comparator와 익명 클래스

    이전 단락에서 Comparator를 사용하기 위해선 새로운 클래스를 하나 만들어야 했다.

     

    하지만 클래스를 만들 수 없는 상황이라면? 익명 클래스로도 Comparator를 사용할 수 있다.

     

     

     

    기존에 Comparator를 구현한 클래스를 사용하기 위해선 위와 같이 써야 했다.

     

    하지만 정렬 기준을 익명 클래스로 구현하기 위해선 아래처럼 해야 한다.

     

     

     

    compare() 메서드를 구현하는 것은 기존의 Comprator 사용법과 같다.

     

    하지만 해당 클래스의 이름은 Comparator로 고정해서 써야 한다.

     

    최종 완성 코드는 아래와 같다.

     

     

     

     

    코드와 결과물은 위와 같다.

     

    나이순으로 제대로 정렬된 것을 확인할 수 있다.

     

    기존에 외부 클래스에 Comparator를 구현한 것과 차이라면 말 그대로 익명클래스라 이름이 없다는 것이다.

Designed by Tistory.