Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Comparator, Comparable Interfaces 본문

Java/Useful Interfaces

Comparator, Comparable Interfaces

defacto standard 2017. 10. 28. 01:04

Comparator, Comparable 인터페이스는,  List인터페이스를 구현하는(예를 들면 ArrayList 등) 컬렉션 리스트에 들어있는 객체들의 정렬을 할 때 주로 쓰인다.

 

Comparator는 기본 정렬기준 외에도 다른 정렬 기준을 정하고 1차정렬된 틀 안에서 2차 정렬을 수행할 때 사용된다.

Comparable은 기본 정렬기준을 구현하는데 사용한다.

 

Comparator와 Comparable 인터페이스의 소스는 다음과 같다.

public interface Comparator {
int compare(Object o1, Object o2);
boolean equals(Object obj);
}
public interface Comparable {
public int compareTo(Object o);
}


compare()와 compareTo()는 선언형태/이름이 다르나 두 객체를 비교한다는 관점에서는 동일하다. 결국 목적은 같다는 말이다.

compareTo(), compare()의 리턴타입은 int이다.


오름차순(Collections.sort())과 최대값(Collections.max()) 기준으로 말하자면

o1 와 o2가 같으면 0, (o1 - o2 = 0)

o1 보다 o2가 크면 양수 (o1 - o2 > 0)

o1 보다 o2가 작으면 음수(o1 - o2 < 0)


를 반환하도록 구성하면 된다.

 

Comparator는 2개의 객체를 인자로 받아서 서로 비교하고,

Comparable은 자료형에 해당하는 클래스가 직접 구현하여 this 객체를 통해 비교하게 된다.

 

보통은 Comparator 안에서 Comparable을 구현한 클래스에 대해서 정렬을 하지만, 여기서는 각각 어떤 역할을 하는지 알아보기 위해 따로 구분을 하겠다.

 

 

Comparator

 

보통 Primitive Array를 정렬할 때는 Arrays.sort()를 쓰며

List를 정렬할 때는 Collections.sort()를 쓴다.

 

실제로 Arrays.sort() 함수는 Primitive Array에 대한 여러 메서드가 오버로딩 되어있다.

 

Collection.sort()는

Collections.sort(List<T> arg0)
Collections.sort(List<T> arg0, Comparator <? super T> arg1)

2개의 sort()메서드만 오버로딩 되어있다.

 

Collections.sort(List<T> arg0) 의 경우에는 Primitive Type List를 정렬할 경우 쓰이고

Collections.sort(List<T> arg0, Comparator <? super T> arg1)의 경우에는 Object Type List를 정렬할 경우에 쓰인다.

 

정확히 말하면 Collections.sort(List<T> arg0)는 Comparable 인터페이스를 구현하는 클래스에 대해서,

Collections.sort(List<T> arg0, Comparator <? super T> arg1)는 Comparable을 구현하지 않거나 Comparator를 사용하여 정렬할 때 사용한다.

 

Java에서 제공하는 Wrapper클래스는 모두 Comparable을 구현하고 있기 때문에 위와 같이 말한 것이다.

 

여기서는 Object Type List에 대한 정렬이므로 Collections.sort(List<T> arg0, Comparator <? super T> arg1)를 사용하겠다.

 

객체는 클래스로부터 나오고, 클래스는 여러 개의 필드가 있을 수 있으며, 컴퓨터는 어떤 값을 기준으로 정렬해야할지 모르기 때문에 이 정보를 가지고 있는 클래스의 인스턴스를 만들어서 넘겨줘야한다.

 

이 클래스는 Comparator 인터페이스를 구현하며, 개발자는 이 인터페이스의 implementation method를 구현함으로써, 특정 클래스에 대한 특정 필드를 기준으로 정렬할지 정할 수 있다.


참고로, 정렬 뿐 아니라 Collections.max(), Collections.min() 등과 같은 것들도 List에 대한 최대, 최소값 등을 리턴할 수 있는데,

객체의 List이고, 따라서 객체가 가지고있는 값을 사용하여 구해야 한다면 그때도 Comparator나 Comparable을 사용할 수 있다.


여기선 정렬로 예를 들어본다.


 

이름, 각 종목 점수 3개 등으로 구성된 Student 클래스가 있다고 가정하자.

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student {
private String name;
private int mathScore;
private int englishScore;
private int computerScore;
}


여기서 바로 Collection.sort()를 사용하여 리스트를 정렬하려고 한다면 Comparable로 캐스팅할 수 없다는 에러가 나온다.

객체에 대한 정렬을 하기 위한 것이므로 모두 다른 값을 넣은 후 진행하였다.

참고로 studentList 객체의 타입을 List가 아니라 List<Student>로 진행한다면 컴파일 자체가 안된다.

public class Main {
static public void main(String args[]) {
List studentList = new ArrayList<>();
studentList.add(new Student("학생1", 1,2,3));
studentList.add(new Student("학생3", 3,1,2));
studentList.add(new Student("학생2", 2,3,1));

System.out.println("정렬 이전 : " + studentList);
Collections.sort(studentList);
System.out.println("정렬 이후 : " + studentList);
}
}



위에서 말했듯이, 컴퓨터는 Student에 대한 정보는 있지만 어떤 필드를 기준으로 정렬해야 하는지는 알 수 없다.

이러한 정보를 제공할 수 있도록 하는 인터페이스가 'Comparator'이다.

 

Comparator의 정의는 다음과 같다.

Student 클래스에 대한 비교를 수행하는 Comparator

public abstract class StudentComparator implements Comparator<Student> {
}


StudentComparator를 상속받는 NameComparator, MathComparator, NameMathComparator

NameComparator : Student 객체들의 이름을 기준으로 오름차순 정렬

public class NameComparator extends StudentComparator{
@Override
public int compare(Student arg0, Student arg1) {
return arg0.getName().compareTo(arg1.getName());
}
}


MathComparator : Student 객체들의 수학점수를 기준으로 오름차순 정렬

public class MathComparator extends StudentComparator{
@Override
public int compare(Student arg0, Student arg1) {
return arg0.getMathScore() - arg1.getMathScore();
}
}


NameMathComparator : Student 객체들의 이름을 기준으로 오름차순 정렬. 이름이 같으면 수학점수를 2차 기준으로 오름차순 정렬

public class NameMathComparator extends StudentComparator {
@Override
public int compare(Student arg0, Student arg1) {
// 이름이 같은 경우 수학 점수를 비교해서 결정
if ( arg0.getName().compareTo( arg1.getName() ) == 0 )
return arg0.getMathScore() > arg1.getMathScore();

// 이름이 다른 경우 이름을 비교
else
return arg0.getName().compareTo( arg1.getName() ) > 0;
}
}


Comparator 인터페이스는 compare(Object arg0, Object arg1)이라는 메서드를 구현해야한다.

2가지 객체를 받아서 어떤 필드를 기준으로 정렬할 지를 정하는 것이다.

맨 아래의 NameMathComparator같은 경우는 우선 이름을 기준으로 정렬을 하고, 만약 이름이 같다면 수학점수를 기준으로 정렬하는 것을 의미한다.

 

이를 실행하기 위해서는 Main함수의 변경이 필요하다.

 

1. NameComparator 결과

이름을 기준으로 오름차순 정렬한다.

우선 NameComparator의 실행결과를 보기위해 name필드가 각각 학생1,학생3,학생2 값을 가지는 인스턴스들의 순서로 List에 넣었다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생1", 2,2,3));
studentList.add(new Student("학생3", 3,1,2));
studentList.add(new Student("학생2", 1,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList, new NameComparator());
System.out.println("Name 정렬 이후 : " + studentList + "\n");
}
}

 

빨간 네모친 부분이 봐야할 부분이다.

정렬 이전에는 학생1, 학생3, 학생2  순서로 list.add()한 순서로 나타난다.

 

그러나 NameComparator 인스턴스를 인자로 주고 이러한 기준에 따라 정렬을 해달라는 요청을 했을 때는

학생1, 학생2, 학생3 순서로 정렬되는 것을 알 수 있다.

 

2. MathComparator 결과

수학 점수를 기준으로 오름차순 정렬한다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생1", 2,2,3));
studentList.add(new Student("학생3", 3,1,2));
studentList.add(new Student("학생2", 1,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList, new MathComparator());
System.out.println("Math 정렬 이후 : " + studentList + "\n");
}
}


List 입력은 위와 같으며

MathComparator에 의해 이름이 아닌, mathScore기준으로 정렬된 것을 확인할 수 있다.

 

 

3. NameMathComparator

1차 정렬 기준은 이름이며, 이름이 같은 경우(동명이인) 2차 정렬 기준으로 수학 점수로 정렬한다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생2", 1,2,3));
studentList.add(new Student("학생1", 100,1,2));
studentList.add(new Student("학생1", 50,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList, new NameMathComparator());
System.out.println("Name-Math 정렬 이후 : " + studentList + "\n");

}
}


 

학생2/수학1점, 학생1/수학100점, 학생1/수학50점 의 순서로 값을 넣었으며

결과는 학생1/50점, 학생1/100점, 학생2/1점 의 순서로 예상한 대로 출력되는 것을 확인할 수 있다.

 

여기서 Collection.sort()는, 인자로 넘긴 list객체의 순서 자체를 변경한다는 사실 역시 알 수가 있다.

만약, 오름차순이 아니라 내림차순으로 정렬하고 싶다면 compare()이나 compareTo()의 반환값에 -1을 곱하면 된다.

 

 

 

Comparable

 

Comparator에서 if-else를 줄이기 위한 수단이면서, 특정 필드를 기준으로 정렬하겠다는 목적을 지닌다.

 

Student 클래스에 Comparable 인터페이스를 상속받으면 comparTo라는 메서드를 구현하게된다.

위에서 Comparator만 사용할때는
Collections.sort(List<T> arg0, Comparator <? super T> arg1) 를 무조건 사용할 수 밖에 없었다.

왜냐면, 어떤 필드를 기준으로 정렬해야할지 모르기 때문에 기준필드를 Comparator를 통해 정보를 주기 때문이다.

 

Comparable은 특정 필드를 기준으로 정렬하겠다는 정보를 주기때문에, 기본적으로 구현을해놓는다면

'이 필드를 기준으로 정렬할 것이다' 라는 것을 알려주는 것과 마찬가지이다.

 

따라서 Comparable 인터페이스를 상속받는 Student 클래스는

Collections.sort(List<T> arg0)를 사용하여 정렬할 수 있다.

 

이 말은, Collection.sort()를 사용하는 Primitive 타입을 저장하는 List들은 모두 Comparable을 구현한다는 것을 뜻한다.

Java API 문서를 보면 Comparable을 기본적으로 구현하는 클래스들을 확인할 수 있다.

 

이제 Student의 소스코드는 다음과 같다.

@Getter
@Setter
@AllArgsConstructor
@ToString
public class Student implements Comparable<Student> {
private String name;
private int mathScore;
private int englishScore;
private int computerScore;

@Override
public int compareTo(Student arg0) {
return this.getName().compareTo(arg0.getName());
}
}

Comparator에서 본 것 처럼, 이름 필드를 기준으로 정렬하게 된다.

Main함수는 다음과 같다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생2", 1,2,3));
studentList.add(new Student("학생3", 100,1,2));
studentList.add(new Student("학생1", 50,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList);
System.out.println("Name정렬 이후 : " + studentList + "\n");

}
}


Comparable 인터페이스를 상속받았기에 어떤 필드를 기준으로 정렬할지의 정보가 제공되고있다.

따라서 처음에는 Collections.sort(List<T> arg0, Comparator <? super T> arg1) 메서드를 사용하여 Comparator를 무조건 제공해줘야했다.

 

하지만 지금은 Comparable인터페이스를 상속받은 클래스의 인스턴스에 대해서 정렬을 수행하므로 정렬기준 필드에 대한 정보가 들어있으므로 Collections.sort(List<T> arg0) 메서드를 사용할 수 있다.

 

결과는 다음과 같다.

이름을 기준으로 Comparable 인터페이스를 구현하였기 때문에 이름을 기준으로 정렬한다.

 

만약 1차 정렬과 2차 정렬을 하는 경우에는 어떻게 할까.

 

1. 처음부터 Comparable의 compareTo()에 순서를 정하여 모두 구현하는 방법

2. Comparator를 만들어서 동적으로 엮는 방법

 

두 방법 모두 내부 구현에 따라 소스가 더러워지는지 깨끗해지는지 다소 차이가 있을 수 있다.

 

 

Comparable + Comparator

 

그렇다면, Comparable 인터페이스를 구현한 클래스(Student)의 인스턴스가 들어있는 List의 정렬에 대해 Comparator를 넘긴다면 어떤 것을 기준으로 정렬할까.

 

현재 Student 클래스는 Name을 기준으로 정렬하는 Comparable 인터페이스를 구현하였다.

여기에, sort()함수 호출 시 Math점수를 기준으로 정렬하는 Comparator 인터페이스를 구현한 객체를 넘겨보겠다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생2", 1,2,3));
studentList.add(new Student("학생3", 100,1,2));
studentList.add(new Student("학생1", 50,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList);
System.out.println("NameComparable정렬 이후 : " + studentList + "\n");

Collections.sort(studentList, new MathComparator());
System.out.println("NameComparable + MathComparator 정렬 이후 : " + studentList + "\n");

}
}


결과는 다음과 같다.

 

그림이 작아서 잘 안보이는 경우 클릭하여 보기 바란다.

단순히 Collections.sort(List<T> arg0)를 사용한 경우에는, 당연하게도 Comparable 인터페이스의 구현 내용에 따라 이름 순서로 정렬이 되어있다.

Collections.sort(List<T> arg0, Comparator <? super T> arg1)를 사용한 경우에는, Comparator 인터페이스의 구현 내용에 따라 수학 점수 순서대로 정렬이 되어있다. Comparable은 무시되는 걸까?

 

이름이 같으면서 수학 점수가 다른 경우에는 어떻게 진행되는지 알아보았다.

public class Main {
static public void main(String args[]) {
List<Student> studentList = new ArrayList<>();
studentList.add(new Student("학생2", 50,2,3));
studentList.add(new Student("학생3", 100,1,2));
studentList.add(new Student("학생2", 1,3,1));

System.out.println("정렬 이전 : " + studentList + "\n");

Collections.sort(studentList);
System.out.println("NameComparable정렬 이후 : " + studentList + "\n");

Collections.sort(studentList, new MathComparator());
System.out.println("NameComparable + MathComparator 정렬 이후 : " + studentList + "\n");

}
}


결과는 다음과 같다.

정렬을 확인하기 위해 같은 이름을 가진 객체를 2개 넣었고, 값이 큰 객체를 먼저 넣었다.

 

Comparable에 기술된 정보만으로는, 단순히 이름만으로 비교하기 때문에 mathSocre에 대해서는 정렬되지 않았다. (2번째 줄)

 

Name을 비교하는 Comparable과 Math점수를 비교하는 MathComparator가 동시에 제공되는 경우에는 달랐다. (3번째 줄)

우선 이름을 기준으로 정렬이 되었으며, 이름이 같은 경우에는 mathScore를 기준으로 정렬되었다.

 

실제 어플리케이션을 만든다면 이름만을 기준으로 정렬할지, 점수만을 기준으로 정렬할 지 알 수 없기때문에,

나라면, 만약 반드시 순서대로 출력해야하는 필드가 존재하는 경우를 제외하고는 Comparable보다는 Comparator를 선호할 것 같다.

Comments