[Java] 데이터 정렬하기 (Sort) - Array, List, Stream
[Java] 데이터 정렬하기 (Sort) - Array, List, Stream
개발 중 데이터를 정렬해야 하는 상황이 자주 발생한다.
이번 글에서는 Array, List, Stream 에서 데이터를 정렬하는 방법을 코드와 함께 정리해본다.
공통
- 기본적으로 자바는 오름차순으로 정렬한다. 내림차순으로 하려면 추가적으로 처리가 필요하다.
Array
참고로 아래에서 사용하는 Arrays.sort
메소드는 return type 이 void
이다.
내부적으로 DualPivotQuicksort 를 사용한다. (코드)
Primitive Array - 오름차순 정렬
int[] randomNumbers = new int[]{57, 3, 88, 42, 76, 15, 91, 29, 64, 8};
Arrays.sort(randomNumbers);
결과: [3, 8, 15, 29, 42, 57, 64, 76, 88, 91]
Primitive Array - 내림차순 정렬
기본적으로 지원하지 않는다. 오름차순으로 정렬한 후, 배열을 뒤집어야 한다. 배열을 뒤집는 예시 코드는 다음과 같다.
int[] randomNumbers = new int[]{57, 3, 88, 42, 76, 15, 91, 29, 64, 8};
Arrays.sort(randomNumbers);
for (int i = 0; i < randomNumbers.length / 2; i++) {
int temp = randomNumbers[i];
randomNumbers[i] = randomNumbers[randomNumbers.length - 1 - i];
randomNumbers[randomNumbers.length - 1 - i] = temp;
}
결과: [91, 88, 76, 64, 57, 42, 29, 15, 8, 3]
내림차순으로 정렬을 해야한다면 개인적으로 이 방법을 사용하기보다는 Boxed Array 로 변환 후 아래의 방법을 사용하는게 더 좋을 것으로 생각된다. Primitive Array는 아래에서 설명할 Comparator
를 직접 사용할 수 없기 때문에 Boxed Array로 변환해야 한다.
다음과 같은 방식으로 Boxed Array로 변환할 수 있다.
Boxed Array로 변환하기 (stream 사용)
Integer[] boxedNumbers = Arrays.stream(randomNumbers).boxed().toArray(Integer[]::new);
Boxed Array로 변환하기 (loop 사용)
Integer[] boxedNumbers = new Integer[randomNumbers.length];
for (int i = 0; i < randomNumbers.length; i++) {
boxedNumbers[i] = randomNumbers[i];
}
Boxed Array - 오름차순 정렬
Primitive Array 오름차순 정렬과 동일하다.
Integer[] randomNumbers = new Integer[]{57, 3, 88, 42, 76, 15, 91, 29, 64, 8};
Arrays.sort(randomNumbers);
결과: [3, 8, 15, 29, 42, 57, 64, 76, 88, 91]
Boxed Array - 내림차순 정렬
객체의 경우 Comparator
를 활용할 수 있다.
Integer[] randomNumbers = new Integer[]{57, 3, 88, 42, 76, 15, 91, 29, 64, 8};
Arrays.sort(randomNumbers, Comparator.reverseOrder());
결과: [91, 88, 76, 64, 57, 42, 29, 15, 8, 3]
Object Array 정렬 - Comparable 활용
Object 의 경우에는 어떤 것을 기준으로 Sort를 할까?
우선 테스트를 위해 다음과 같은 객체를 만들어보겠다.
class User {
final int id;
// ... properties
private User(int id) {
this.id = id;
}
public static User of(int id) {
return new User(id);
}
public int getId() {
return id;
}
@Override
public String toString() {
return "User{id=" + id + '}';
}
}
User[] users = new User[]{
User.of(99),
User.of(1),
User.of(-1),
User.of(68)
};
Arrays.sort(users);
위 코드를 동작 시키면 어떻게 될까?
정답은 다음과 같은 에러가 발생된다.
class User cannot be cast to class java.lang.Comparable
Comparable interface 는 다음 메소드를 가지고 있다.
public int compareTo(T o);
compareTo
메소드를 적절히 구현해주면 원하는대로 정렬을 할 수 있다.
오름차순 정렬
@Override
public int compareTo(User u) {
return this.id - u.id;
}
@Override
public int compareTo(User u) {
return Integer.compare(this.id, u.id);
}
compare 메소드는 첫번쨰 인자를 x, 두번째 인자를 y 라고 하였을 때 아래 조건에 따라 값을 반환한다.
- 값이 동일할 경우
0
- y가 더 클 경우
-1
- x가 더 클 경우
1
내부 로직은 다음과 같다. (참고로 첨부)
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
정렬하고자 하는 대상이 int type 이 아닌 경우에도 로직을 참고하여 응용해주면 sort를 적용할 수 있다.
결과: [User{id=-1}, User{id=1}, User{id=68}, User{id=99}]
내림차순 정렬
내림차순으로 정렬을 하고 싶다면 순서를 반대로 해주면 된다.
@Override
public int compareTo(User u) {
return u.id - this.id;
}
@Override
public int compareTo(User u) {
return Integer.compare(u.id, this.id);
}
결과: [User{id=99}, User{id=68}, User{id=1}, User{id=-1}]
Object Array 정렬 - Comparator 활용
‘객체에 Comparable 을 구현하지 않고 sort 를 활용할 수 있는 방법은 없을까?’ 라고 생각이 든다면 Comparator
를 활용해볼 수 있다. Comparator
를 사용하면 정렬 방식을 유연하게 설정해야 할 때 적절하게 대응할 수 있다.
동일하게 users
를 사용하여 설명해보겠다.
오름차순 정렬
Arrays.sort(users, (a, b) -> a.id - b.id);
Arrays.sort(users, (a, b) -> Integer.compare(a.id, b.id));
Arrays.sort(users, Comparator.comparingInt(a -> a.id));
결과: [User{id=-1}, User{id=1}, User{id=68}, User{id=99}]
(참고) Intellj 에서는 Comparator.comparingInt
를 사용하는것을 추천한다. Comparator.comparingInt 의 코드는 다음과 같다.
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}
내림차순 정렬
마찬가지로 순서를 뒤집어주면 되는데 Comparator.comparingInt
의 경우에는 조금 다르게 처리해야하니 주의한다.
Arrays.sort(users, (a, b) -> b.id - a.id);
Arrays.sort(users, (a, b) -> Integer.compare(b.id, a.id));
User로 캐스팅을 해준 것에 유의하자. 캐스팅을 해주지 않으면 reversed 사용이 불가능하다. (컴파일러가 타입추론에 실패함.)
Arrays.sort(users, Comparator.comparingInt((User a) -> a.id).reversed());
혹은 다음과 같이 타입을 명확하게 Comparator<User>
로 정의하여 해결하는 방법도 있다.
Comparator<User> comparator = Comparator.comparingInt(a -> a.id);
Arrays.sort(users, comparator.reversed());
결과: [Entity{id=99}, Entity{id=68}, Entity{id=1}, Entity{id=-1}]
List
List 의 정렬도 Array 와 크게 다를 것은 없다.
Comparable 이 구현되어 있는 경우
다음과 같은 List 를 사용해보겠다. 참고로 Integer
는 Comparable
인터페이스를 구현하고 있다.
List<Integer> randomNumbers = new ArrayList<>(List.of(57, 3, 88, 42, 76, 15, 91, 29, 64, 8));
오름차순
randomNumbers.sort(Comparator.naturalOrder());
결과: [3, 8, 15, 29, 42, 57, 64, 76, 88, 91]
* Comparable 인터페이스를 구현한 상태라면
randomNumbers.sort(null);
으로도 정렬할 수 있다.
내림차순
randomNumbers.sort(Comparator.reverseOrder());
결과: [91, 88, 76, 64, 57, 42, 29, 15, 8, 3]
Comparator 활용
다음과 같은 List 를 사용해보겠다.
List<User> users = new ArrayList<>(List.of(
User.of(99),
User.of(1),
User.of(-1),
User.of(68)
));
오름차순
users.sort((a, b) -> a.id - b.id);
users.sort((a, b) -> Integer.compare(a.id, b.id));
users.sort(Comparator.comparingInt(a -> a.id));
결과: [3, 8, 15, 29, 42, 57, 64, 76, 88, 91]
내림차순
users.sort((a, b) -> b.id - a.id);
users.sort((a, b) -> Integer.compare(b.id, a.id));
users.sort(Comparator.comparingInt((User a) -> a.id).reversed());
결과: [User{id=99}, User{id=68}, User{id=1}, User{id=-1}]
Collections.sort
Collections.sort
로도 정렬을 할 수 있다. 내부 코드는 다음과 같다.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
내부적으로 list
에서 sort
메소드를 호출하는 방식으로 동작하기 떄문에 위의 내용과 동일하다. 이에 추가적으로 설명은 하지 않는다.
Stream
Stream 의 정렬도 Array, List 와 크게 다를 것은 없다. Stream 은 결과값으로 새로운 객체를 반환하니 해당 부분에 주의한다.
Comparable 이 구현되어 있는 경우
다음과 같은 Integer List 가 있다고 하자. 참고로 Integer
는 Comparable
인터페이스를 구현하고 있다.
List<Integer> randomNumbers = List.of(57, 3, 88, 42, 76, 15, 91, 29, 64, 8);
오름차순
따로 comparator
를 지정하지 않아도 사용 가능한 부분이 list 를 정렬하는 것과의 차이점이다. 물론 지정해도 된다.
List<Integer> sortedNumbers = randomNumbers.stream()
.sorted()
.toList();
List<Integer> sortedNumbers = randomNumbers.stream()
.sorted(Comparator.naturalOrder())
.toList();
결과: [3, 8, 15, 29, 42, 57, 64, 76, 88, 91]
내림차순
List<Integer> sortedNumbers = randomNumbers.stream()
.sorted(Comparator.reverseOrder())
.toList();
결과: [91, 88, 76, 64, 57, 42, 29, 15, 8, 3]
Comparator 활용
다음과 같은 List 를 사용해보겠다.
List<User> users = new ArrayList<>(List.of(
User.of(99),
User.of(1),
User.of(-1),
User.of(68)
));
오름차순
List<User> orderedUsers = users.stream()
.sorted((a, b) -> a.id - b.id)
.toList();
List<User> orderedUsers = users.stream()
.sorted((a, b) -> Integer.compare(a.id, b.id))
.toList();
List<User> orderedUsers = users.stream()
.sorted(Comparator.comparingInt(a -> a.id))
.toList();
결과: [User{id=-1}, User{id=1}, User{id=68}, User{id=99}]
내림차순
List<User> orderedUsers = users.stream()
.sorted((a, b) -> b.id - a.id)
.toList();
List<User> orderedUsers = users.stream()
.sorted((a, b) -> Integer.compare(b.id, a.id))
.toList();
List<User> orderedUsers = users.stream()
.sorted(Comparator.comparingInt((User a) -> a.id).reversed())
.toList();
결과: [User{id=99}, User{id=68}, User{id=1}, User{id=-1}]