상세 컨텐츠

본문 제목

제너릭스와 Comparable, Comparator

본문

  • 제너릭스 (Generics)

제너릭스는 처리 대상의 자료형에 의존하지 않도록 클래스 혹은 인터페이스를 구현하는 기능이다. 즉 제너릭 클래스(인터페이스)는 자료형 문제로부터 자유롭고 안전하다. 형식은 클래스(인터페이스) 이름 바로뒤에 <Type>형식의 매개변수를 붙여 선언한다.

 

      -  class        클래스 이름         <매개변수> {/* ... */}

         (만약 매개변수가 여러개면 <매개변수1, 매개변수2, ... > {/*...*/}

      -  interface  인터페이스 이름  <매개변수> {/* ... */}

 

 다음과 같이 매개변수의 이름을 작성하기 위해서는 규칙이 있다.

 

   1) 대문자는 1개를 사용한다.(소문자는 가급적 사용x)

   2) 컬렉션(collection) 내부 요소의 자료형은 element의 머리글자인 E를 사용한다.

       (컬렉션은 자바에서 제공하는 기본 자료구조를 모은것을 말한다.)

   3) 맵(Map) 내 키(key)와 값(value)의 자료형은 key와 value의 머리글자인 K와 V를 사용한다.

   4) 일반적인 자료형은 T를 사용한다.

 

 그리고 매개변수에 primitive type은 쓸 수 없다. 따라서 int는 Integer, double은 Double과 같이 reference type으로 써줘야 한다. 

 만약 제네릭 메서드를 이용하고 싶으면 다음과 같이 사용할 수 있다.

      

       - 접근 제어자  <제네릭타입>  반환타입  메소드명 ( 제네릭타입  파라미터 )

 

 여기서 주의할 점은 클래스를 생성할 때 제네릭 클래스로 만들어 매개변수가 있다면 이름을 똑같이해서 static 메서드에 적용하려고 할때 오류가 발생한다. 따라서 static 메서드에 이용할 제네릭은 클래스 제네릭과 동일하게 쓰려고 하지 않는 것이 좋다. (독립적으로 사용해야댐)

 

매개변수에는 와일드카드를 지정할 수도 있다.

   <? extends T> : 클래스 T의 하위 클래스를 전달받는다.

   <? super T> : 클래스 T의 상위 클래스를 전달받는다.

class GenericClass<T>{
    T x;
    //생성자
    GenericClass(T t){
        this.x = t;
    }
    //인스턴스 메서드
    T getx() {
        return x;
    }
}
public class Main {
    public static void main(String[] args)  {
        GenericClass<String> a = new GenericClass<>("ABC");//String type
        GenericClass<Integer> b = new GenericClass<>(15);//Integer type
        System.out.println(a.getx()); // "ABC" 출력
        System.out.println(b.getx()); // 15 출력
    }
}
  • Comparable과Comparator

 Comparable과 Comparator는 둘다 Interface이다. 따라서 두개를 사용하려면 인터페이스 내부에 구현된 메서드를 반드시 오버라이딩(재정의)해야한다. 이 두 interface는 언제 쓰일까? 우리가 어떤 자료형들을 특정 규칙에 맞게 정렬하고 싶을 때가 있다. 하지만 그때 Java내에 정해진 숫자의 오름차순, 내림차순, 알파벳순 등을 제외하고 따로 정의하고 싶은 경우인 경우 매우 답답할지도 모른다. 이럴때 두 interface를 이용해 내 기준에 맞게 interface 내 메서드를 오버라이딩한다면 쉽게 정렬할 수 있다. 즉 객체를 내가 원하는 기준으로 비교할 수 있게 해준다.

 대충 어느정도 흐름을 알았으니 Comparable부터 살펴보자.

 

  • Comparable

Comparable은 매개변수 1개가 필요한 인터페이스로 매개변수는 위에서 배운 제너릭스로 구현할 수 있다. Comparable이 Comparator와 구분되는 점을 고르라면 Comparable은 자기자신의 객체와 다른 객체를 비교한다는 것이다. 그래서 비교하고 싶은 자기 자신의 객체 class에 implements하여 사용해야한다. 예제를 한번 만들어 볼건데 우선 Student 클래스를 만든 후 인스턴스 변수로 나이, 학급을 만든다. 그리고 Student 인스턴스들의 크기를 비교할 때 첫번째 우선순위로 학급을, 두번째 우선순위로 나이순을 기준으로 정렬하는 메서드를 구현해 볼 것이다.(기준은 오름차순)

class Student implements Comparable<Student>{
    //<> 안에 본인 Student 객체와 비교하고싶은 대상의 자료형 입력
    int age ;
    String name ;
    int classNumber ;
    // 생성자
    Student(int age, int classNumber, String name){
        this.age = age; this.classNumber = classNumber; this.name = name;
    }
    @Override
    public int compareTo(Student o){
        //우선순위 : classNumber > age
        if(this.classNumber>o.classNumber) return 1;
        else if(this.classNumber==o.classNumber){
            // classNumber가 같으면 다음 우선순위인 age비교
            if(this.age>o.age) return 1;
            else if(this.age==o.age) return 0;
            else return-1;
        }else return -1;
    }
}

 우선 자기자신인 Student 객체와 비교하고 싶은 대상의 자료형도 Student이므로 <> 안에 Student를 적어준다. 그리고 무조건 Comparable의 메서드인 compareTo를 오버라이딩해야한다. return값은 항상 int형이며 음수를 return하면 본인의 객체가 비교대상보다 작은 것이고 0을 return하면 둘이 같은 우선순위인 것이고 양수를 return하면 본인의 객체가 비교대상보다 큰 것이다. 보통 1, 0, -1로 표현한다.

 compareTo의 매개변수로는 비교대상의 객체자료형을 사용하고 본인의 값은 this.을 이용하여 사용 가능하다. 위와같이 비교기준이 여러개인 경우에는 우선순위가 높은 기준을 먼저 비교한뒤 첫번째 우선순위가 같은 경우에 두번쨰 우선순위로 비교를 시작한다.

import java.util.Arrays;

class Student implements Comparable<Student>{
    //<> 안에 본인 Student 객체와 비교하고싶은 대상의 자료형 입력
    int age ;
    String name ;
    int classNumber ;
    // 생성자
    Student(int age, int classNumber, String name){
        this.age = age; this.classNumber = classNumber; this.name = name;
    }
    @Override
    public int compareTo(Student o){
        //우선순위 : classNumber > age
        if(this.classNumber>o.classNumber) return 1;
        else if(this.classNumber==o.classNumber){
            // classNumber가 같으면 다음 우선순위인 age비교
            if(this.age>o.age) return 1;
            else if(this.age==o.age) return 0;
            else return-1;
        }else return -1;
    }
}
public class Main {
    public static void main(String[] args)  {
     Student 명훈 = new Student(17,1,"명훈");
     Student 창희 = new Student(18,1,"창희");
     Student 지연 = new Student(19,3,"지연");
     Student 명수 = new Student(19,2,"명수");
     Student 기준 = new Student(18,2,"기준");
     System.out.println(명훈.compareTo(창희));
     Student[] a = {창희,명훈,지연,명수,기준};
     Arrays.sort(a);
     for(int i=0; i<a.length;i++){
     System.out.println(a[i].name);
        }
    }
}

 실제 코드사용 예시를 보면  자기자신 변수명.compareTo(비교할 객체의 변수명) 으로 -1, 0, 1을 return받아 비교하는 방법이 있고, Arrays.sort()를 이용해 배열을 정렬시키는 방법이 있다. 해당 배열의 자료형에 comparable이 implements 되어있으면 Arrays.sort()를 했을 때  자동으로 compareTo 기준에 따라 오름차순 정렬이 된다. 따라서 위의 코드 결과는 다음과 같다.

------------------------------------------------------------------

-1                                       // 명훈.comareTo(창희)의 결과
명훈 창희 기준 명수 지연  // 오름차순 정렬(Arrays.sort)한 배열의 결과

(사실 이름들이 한줄씩 띄어서 출력됨. 보기쉽게 늘여놓은 것임.)

------------------------------------------------------------------

 하나 주의해야할 점이 있다면 compareTo의 결과로 -1, 1을 안쓰고 어떤 계산의 결과값을 쓰는 경우가 있는데(1이 아닌 양수 or -1이 아닌 음수) Integer의 최솟값, 최댓값 범위 내에서만 오류없이 사용이 가능하다.

 

  • Comparator

 Comparator는 매개변수 2개가 필요한 인터페이스로 매개변수는 위에서 배운 제너릭스로 구현할 수 있다. Comparable과 다른점은 java.util 패키지에 포함되어있어 import해야하고, 2개의 다른 객체들을 서로 비교한다는 점이다. 비슷해 보이지만 자기자신과 다른 객체를 비교하는게 아니라는 디테일을 느낄수 있어야한다. compare 메서드를 사용하고 싶은 클래스에 implements하고 <>안에 비교하고 싶은 객체들의 자료형을 적는다.  이러한 형식 안에서 Comparable때와 같은 기준의 내용들을 구현해보자.

import java.util.Comparator;

class Student implements Comparator<Student> {
    //<> 안에 본인 비교하고싶은 대상들의 자료형 입력
    int age ;
    String name ;
    int classNumber ;
    // 생성자
    Student(int age, int classNumber, String name){
        this.age = age; this.classNumber = classNumber; this.name = name;
    }

    @Override
    public int compare(Student o1, Student o2) {
        // 우선순위 : classNumber > age
        if(o1.classNumber>o2.classNumber) return 1;
        else if(o1.classNumber==o2.classNumber){
            // classNumber가 같으면 다음 우선순위인 age 비교
            if(o1.age>o2.age) return 1;
            else if(o1.age==o2.age) return 0;
            else return -1;
        }else return -1;
    }
}
public class Main {
    public static void main(String[] args)  {
    Student 명훈 = new Student(17,1,"명훈");
    Student 창희 = new Student(18,1,"창희");
    Student 지연 = new Student(19,3,"지연");
    Student 명수 = new Student(19,2,"명수");
    Student 기준 = new Student(18,2,"기준");
    // 지연이라는 인스턴스변수를 통해 compare 메소드를 호출하지만
    // 지연은 아무런 영향을 끼치지 않는다. 따라서 창희.compare(창희,명훈)도 같은 결과
    System.out.println(지연.compare(창희,명훈));
    System.out.println(창희.compare(창희,명훈));
    }
}

 이렇듯  우선 compare 메서드를 살펴보면 compareTo와 달리  매개변수가 2개이고  그 2개를 서로 비교하여 -1, 0, 1을 return하고 있다. 이는 위에서 하던 방식과 크게 다르진 않다. 그리고 main 메서드에서 compare 메서드를 아무 인스턴스 변수 이름.compare(비교대상 1,비교대상2) 형식으로 사용하는 것을 볼 수 있다. 아무 인스턴스 변수 이름을 넣으라는게 정말 생성된 객체면 아무거나 넣어도 상관 없고 결과 값에 전혀 영향을 미치지 않는다. 따라서 위의 지연.compare(창희,명훈)과 창희.compare(창희,명훈)은 창희와 명훈의 비교 결과인 1을 반환한다.

 

 compare메서드 쓰는 방법이 되게 어정쩡해 보일 수 있다. 사실 위의 방법보다 더 깔끔하게 쓰는 방법이 있다. 바로 익명 클래스를 활용하는 방법이다. comparable은 자기자신과 다른 객체를 비교하는 거라 익명클래스가 잘 안맞지만 comparator는 익명클래스가 굉장히 잘 맞는다. 

import java.util.Comparator;

class Student{
    //<> 안에 본인 비교하고싶은 대상들의 자료형 입력
    int age ; String name ; int classNumber ;
    // 생성자
    Student(int age, int classNumber, String name){
        this.age = age; this.classNumber = classNumber; this.name = name;
    }
}
public class Main {
    public static void main(String[] args)  {
    Student 명훈 = new Student(17,1,"명훈");
    Student 창희 = new Student(18,1,"창희");
    Student 지연 = new Student(19,3,"지연");
    Student 명수 = new Student(19,2,"명수");
    Student 기준 = new Student(18,2,"기준");
    System.out.println(comp.compare(창희,명훈));
    System.out.println(comp.compare(명수,지연));
    }
    //main 메서드 밖이라 main 메서드에서 익명 클래스를 사용하기위해 static 사용
    static Comparator<Student> comp = new Comparator<>(){
        @Override
        public int compare(Student o1, Student o2) {
            if(o1.classNumber>o2.classNumber) return 1;
            else if(o1.classNumber==o2.classNumber){
                // classNumber가 같으면 다음 우선순위인 age 비교
                if(o1.age>o2.age) return 1;
                else if(o1.age==o2.age) return 0;
                else return -1;
            }else return -1;
        }
    };
}

 다음과 같이 Student class에 implements하는 대신 main class에 따로 익명클래스를 만들어 사용이 가능하다. 이때 main 메서드 밖에 있으므로 main 메서드 안에서 사용하기 위해 static을 앞에 붙여주고 마무리로 ;를 붙여주는 것 빼면 위의 예제와 크게 다르지 않다. 익명클래스의 인스턴스인 comp를 이용해 compare 메서드를 실행하면 된다. 

 

 여기까지 comparable과 활용이 똑같은데 comparator도 Array.sort()에 적용이 가능할까? 당연히 가능하다. 익명클래스로 구현했을때 사용하는 방법을 알아보자. 위와 같이 구현을 똑같이한 후에 Array.sort(배열 이름, 익명클래스 인스턴스이름)으로 정렬만 시켜주면 기준에 맞는 오름차순 정렬이 한번에 된다. 아래 코드를 보자.

import java.util.Arrays;
import java.util.Comparator;
class Student{
    int age ; String name ; int classNumber ;
    Student(int age, int classNumber, String name){
        this.age = age; this.classNumber = classNumber; this.name = name;
    }
}
public class Main {
    public static void main(String[] args)  {
    Student 명훈 = new Student(17,1,"명훈");
    Student 창희 = new Student(18,1,"창희");
    Student 지연 = new Student(19,3,"지연");
    Student 명수 = new Student(19,2,"명수");
    Student 기준 = new Student(18,2,"기준");
    Student[] a= {명훈, 창희, 지연, 명수, 기준};
    Arrays.sort(a,comp);
    for(int i=0;i<a.length;i++){
        System.out.print(a[i].name+" ");
    } System.out.println();
    }
    static Comparator<Student> comp = new Comparator<>(){
        @Override
        public int compare(Student o1, Student o2) {
            if(o1.classNumber>o2.classNumber) return 1;
            else if(o1.classNumber==o2.classNumber){
                // classNumber가 같으면 다음 우선순위인 age 비교
                if(o1.age>o2.age) return 1;
                else if(o1.age==o2.age) return 0;
                else return -1;
            }else return -1;
        }
    };
}

 이렇게 할 경우결과는 다음과 같다.

-------------------------------------------------------------------------------------------

명훈 창희 기준 명수 지연  //오름차순 정렬(Arrays.sort)한 배열의 결과

-------------------------------------------------------------------------------------------

comparable이나 comparator 둘 다 Arrays.sort를 하면 이웃한 인덱스끼리 compare 혹은 comparTo 메서드에 의해 return된 값이 음수면 그대로 놔두고 양수면 서로 자리가 바뀌는 방법으로 오름차순 정렬이 되는 것이다. 반대로 음수와 양수가 반대로 나오게 설계하고 Arrays.sort를 하면 내림차순으로 정렬된다. .

 

/참조: https://st-lab.tistory.com/243

관련글 더보기