자바에서 배열이나 리스트를 정렬할 때, sort() 메서드를 사용하여 간편하게 정렬할 수 있다.
자바의 데이터 타입은 Primitive Type(원시 타입)과 Reference Type(참조 타입)으로 구분할 수 있다.
Primitive Type(원시 타입)
기본형(byte, char, short, int, long, float, dulble, boolean) 타입을 제공한다.
Reference Type(참조 타입)
원시 타입을 제외한 모든 타입을 말한다. 배열, 열거, 클래스, 인터페이스 타입이 참조 타입에 해당된다.
배열의 정렬 방법은 원시 타입과 참조 타입 각각 다르다. 참조 타입의 경우, Comparable 인터페이스를 구현해서 사용해야 한다.
배열은 기본적으로 오름차순으로 정렬된다.
int[] arr = {3, 6, 10, 1, 4, 2};
Arrays.sort(arr);
// Output: [1, 2, 3, 4, 6, 10]
내림차순으로 정렬한다면, Collections 클래스의 reverseOrder()를 사용한다.
+) Collections는 collection에 사용할 정적 유틸리티 메서드 모음이 있는 java.util.Collections
클래스를 말한다.
int[] arr = {3, 6, 10, 1, 4, 2};
Arrays.sort(arr, Collections.reverseOrder());
// Output: [10, 6, 4, 3, 2, 1]
객체로 이루어진 배열은 Comparable 인터페이스 구현이 필요하다.
class People implements Comparable {
private String name;
private int age;
public People(String name, int age){
this.name = name;
this.age = age;
}
@Override
public int compareTo(People people){
if(this.age < people.age) {
return -1;
} else if(this.age == people.age) {
return 0;
} else {
return 1;
}
}
}
자바에서는 Primitive Type의 변수는 부등호를 가지고 비교할 수 있다.
public class Test {
public static void main(String[] args) {
int a = 1;
int b = 2;
if(a > b) {
System.out.println("a > b");
}
else if(a == b) {
System.out.println("a == b");
}
else {
System.out.println("b > a");
}
}
}
그럼 나이와 이름을 갖고 있는 Member라는 클래스 객체가 생성된다면 이것은 어떻게 비교할까? 기준이 무엇일지 판단할 수 없다. 이를 해결하려면 정렬 기준을 정해줘야하고 Comparable 인터페이스를 구현했다. 그런데 찾아보면 Comparator라는 것도 확인할 수 있을 것이다.
Comparable인터페이스의 compareTo(T o)와 Comparator의 compare(T o1, T o2) 차이는 무엇일까?
+) comparable은 lang 패키지에 있지만, Comparator는 util 패키지에 있다.
즉, 두 인터페이스 모두 비교하는 것은 같지만 비교 대상이 다르다.
Comparable 인터페이스를 확인해보면 다음과 같다.
public interface Comparable<T> {
public int compareTo(T o);
}
<T>
는 객체 타입이 지정될 자리를 말한다.
예를들어, Member 클래스를 비교한다면 T
자리에 Member가 들어가고 Comparable을 implements한다.
class Member implements Comparable<Member> {
int age;
String name;
Student(int age, String name){
this.age = age;
this.name = name;
}
@Override
public int compareTo(Member o) {
// 비교 구현
if(this.age > o.age) {
return 1; // 자기자신 age > o의 age -> 양수
}
else if(this.age == o.age) {
return 0; // 자기자신 age == o의 age -> 같다
}
else {
return -1; // 자기자신 age < o의 age -> 음수
}
}
}
compareTo는 '값'을 비교해서 정수를 반환하는 방식이다. 1,0,-1은 각각 자기자신과 o의 값의 차를 의미한다. 즉 자기자신을 기준으로 비교하여 반환한 것이다.
+) 만약 비교 과정에서 범위를 넘어가게 된다면, underflow 또는 overflow가 발생할 수 있다.⭐️
파라미터(매개 변수)로 들어오는 두 객체를 비교한다.
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
확인해보면 Comparable과 인터페이스 형식이 유사하다.
이것을 가지고 클래스를 비교하는 코드를 작성한다면 다음과 같다.
import java.util.Comparator;
public class Member implements Comparator<Member> {
int age;
String name;
Student(int age, String name){
this.age = age;
this.name = name;
}
@Override
public int compare(Member o1, Member o2) {
/* 비교 구현 */
if(o1.age > o2.age) {
return 1;
}
else if(o1.age == o2.age) {
return 0;
}
else {
return -1;
}
/* 간단하게 작성한다면 다음과 같다.
return o1.Member - o2.Member;
*/
}
}
Comparable의 경우, 자기자신과 비교하는 파라미터를 비교하지만 Comparator는 새로 들어오는 두 인자를 비교한다.
정렬하기 위해서는 비교하는 작업이 필요하다. 일단 대부분의 언어는 옵션을 지정하지 않는 이상 '오름차순'을 기준으로 정렬한다.
방금 알아본 Comparator와 Comparable의 작동방식을 정렬에 적용하면 다음과 같다.
public class Test {
public static void main(String[] args) {
Member[] arr = new Member[10];
// 랜덤으로 배열 초기화
for(int i = 0; i < 10; i++) {
arr[i] = new Member((int) (Math.random() * 100));
}
// 정렬 이전
System.out.println("정렬 전 : ");
for(int i = 0; i < 10; i++) {
System.out.print(arr[i].age + " ");
}
System.out.println();
Arrays.sort(arr);
// 정렬 이후
System.out.println("정렬 후 : ");
for(int i = 0; i < 10; i++) {
System.out.print(arr[i].age + " ");
}
System.out.println();
}
}
class Member implements Comparable<Member> {
int age;
public Member(int age) {
this.age = age;
}
// 비교 기준을 생성한다.
@Override
public int compareTo(@NotNull Member o) {
return this.age - o.age;
}
}
만약 Comparable을 구현하지 않았다면?
ClassCastException
예외가 나온다. 클래스를 비교할 수 있는 기준이 정의되지 않았기 때문이다.
import java.util.Arrays;
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
Member[] arr = new Member[10];
// 랜덤으로 배열 초기화
for (int i = 0; i < 10; i++) {
arr[i] = new Member((int) (Math.random() * 100));
}
// 정렬 이전
System.out.println("정렬 전 : ");
for (int i = 0; i < 10; i++) {
System.out.print(arr[i].age + " ");
}
System.out.println();
Arrays.sort(arr, comp);
// 정렬 이후
System.out.println("정렬 후 : ");
for (int i = 0; i < 10; i++) {
System.out.print(arr[i].age + " ");
}
System.out.println();
}
static Comparator<Member> comp = new Comparator<Member>() {
@Override
public int compare(Member o1, Member o2) {
return o1.age - o2.age;
}
};
}
class Member {
int age;
public Member(int age) {
this.age = age;
}
}
Q. Arrays.sort(Object[] a)와 Arrays.sort(T[] a, Comparator<? super T>c)
차이는 무엇인가?🔎
Arrays.java를 살펴보자.
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}
c가 null이면 sort(a)를 호출한다.
이것은 다음 메서드를 호출한다.
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
즉, Comparator로 넘겨받은 인자가 null이면 Arrays.sort(Object[] a)를 호출하고, 정렬에 Comparable을 구현한 compareTo()
를 사용한다.
만약 c가 null이 아니라면 Comparator을 구현한 객체의 compare()
메서드를 사용한다.
Q. 내림차순으로 정렬하고 싶다면? 🔎
compareTo를 구현할 때, 부호를 바꿔주면 된다.
내림차순은 작은 원소가 큰 원소보다 앞에 있는 것이니, 선행 원소 < 후행 원소
일 때 선행 원소 - 후행 원소 < 0
이면 위치를 바꾸려면 음수 결과를 양수로 바꾸어야 한다.
Java에서 List 인터페이스는 다음과 같다.
public interface List<E> extends Collection<E> {
...
}
여기서 <E>
는 Generic을 나타내는데 타입 파라미터로 명시할 수 있는 것은 참조 타입(Reference) 뿐이다. 또한 사용자 정의 클래스도 가능하다.
그럼 List는 어떻게 정렬할까?
Collections.sort() 코드를 살펴보면 list 인터페이스의 sort() 메서드를 실행한다.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
list.sort()를 확인해보면 1) List를 Array로 변경하고, 2) Arrays.sort() 가 실행된다. 이 안에서 sort 알고리즘을 확인해보면 Timsort()를 사용하고 있다.
@SuppressWarnings({"unchecked", "rawtypes"})
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
Collections.sort()를 사용하면 다음처럼 정렬할 수 있다.
Collections.sort(arraylist);
Java8부터는 list.sort()를 제공한다. Comparator 인터페이스에서 구현된 메서드를 사용하여 정렬하면 된다.
arraylist.sort(Comparator.naturalOrder()); // 오름차순
arraylist.sort(Comparator.reverseOrder()); // 내림차순
arraylist.sort(String.CASE_INSENSITIVE_ORDER); // 대소문자 구분없이 오름차순 정렬
arraylist.sort(Collection.reverseOrder(String.CASE_INSENSITIVE_ORDER)); // 대소문자 구분없이 내림차순 정렬
즉, 결국 정렬을 수행하기 위해서 사용되는 메서드는 Arrays.sort()이다.
정렬 알고리즘은 primitive type과 reference type에 따라 구분된다.
자세한 알고리즘은 추후에 알아보도록 하겠다.
Summary
- Java의 정렬은 결국 Arrays.sort()로 정렬한다.
- 원시 타입은 부등호를 가지고 크기를 비교할 수 있지만, 참조 타입은 비교 기준이 정해져 있지 않으므로 Comparable 또는 Comparator를 구현한다.
Comparable - compareTo(T o)
: 자기 자신과 파라미터로 들어오는 객체를 비교한다.Comparator - compare(T o1, T o2)
: 자기 자신의 상태와 상관없이 파라미터로 들어오는 두 객체를 비교한다.- 선택한 정렬 알고리즘은 primitive type과 reference type에 따라 다르다.
- primitive type(기본형): DualPivotQuickSort
- reference type(참조형): TimSort
Reference