박종훈 기술블로그

[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 를 사용해보겠다. 참고로 IntegerComparable 인터페이스를 구현하고 있다.

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 가 있다고 하자. 참고로 IntegerComparable 인터페이스를 구현하고 있다.

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}]

categories: 스터디-자바

tags: Java , Sort , Array , List , Stream , Desc , Asc , Comparator , 정렬