Daily Notebook

[Java] Null Safe한 자바 컬렉션 정렬

by 배부른코딩로그
💡 자바 컬렉션을 정렬할 때, 값이 Null 일 수 있다는 것을 배제할 수 없다. Null Safe한 정렬을 해보자!

목표

  • Stream을 통해 컬렉션을 정렬할 수 있다.
  • Null Safe한 정렬을 통해 NullPointException 예외를 방지할 수 있다.
  • 정렬에 대한 예제 공간을 만든다.

 

Collection.sort() 를 이용한 정렬이 아닌, 스트림(Stream)을 이용한 정렬을 정리해보고자 한다.

 

Stream.sorted

만약, 숫자 리스트를 오름차순으로 정렬하고자 한다면 다음과 같을 것이다.

List<Integer> numbers = Arrays.asList(9, 5, 7, 1, 8);
List<Integer> sortedNumbers = numbers.stream()
.sorted(Comparator.comparing(Integer::intValue))
.collect(Collectors.toList());
System.out.println(sortedNumbers);

위 예제에 대한 결과는 [1, 5, 7, 8, 9] 이다.

Stream에서 제공하는 sorted 헬퍼 메서드를 활용하면 편하게 정렬이 가능하다.

정렬에 대한 기준은 Comparator 클래스에서 제공하는 comparing 메서드를 통해 람다식으로 쉽게 표현할 수 있다.

 

Comparator 클래스

Comparator 의 comparing() 메서드는 첫 번째 인자(keyExtractor)로 정렬 기준 키를 추출할 때 사용되는 함수를 
두 번째 인자(keyComparator)로 추출된 키를 비교하는데 사용될 Comparator를 설정할 수 있다.

 

Comparator.class를 살펴보면 static하게 지원하는 keyComparator는 다음과 같다.

public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}

메서드 명을 읽어보면 언제 써야할지 바로 감이 오게 된다.

List<Integer> numbers = Arrays.asList(9, 5, 7, 1, 8);
List<Integer> sortedNumbers = numbers.stream()
.sorted(Comparator.comparing(Integer::intValue, Comparator.reverseOrder()))
.collect(Collectors.toList());
// [9, 8, 7, 5, 1]

 

만약, 정렬 키값이 Null이라면 어떻게 될까?

그렇다. 개발자라면 자주 접하게 될 NullPointException을 마주하게 된다.

Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Comparator.lambda$comparing$ea9a8b3a$1(Comparator.java:436)
at java.base/java.util.TimSort.binarySort(TimSort.java:296)
at java.base/java.util.TimSort.sort(TimSort.java:221)
at java.base/java.util.Arrays.sort(Arrays.java:1306)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:353)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
at com.example.App.main(App.java:25)

 

Comparator Null Safe 정렬

메서드 구현은 Null Safe 하게 작동하는 것을 권장한다.
Java 8부터 Stream, Optional 등 Null Safe한 코드를 짤 수 있도록 도와주는 클래스들이 다수 출현했으며,
정렬 역시 Null Safe 는 권장이 아닌 해야할 처리이다. 정렬하는데 터지면 웃프자너유?

Comparator.class의 static 메서드들 중 아래를 활용할 수 있다.

/**
* Returns a null-friendly comparator that considers {@code null} to be
* less than non-null. When both are {@code null}, they are considered
* equal. If both are non-null, the specified {@code Comparator} is used
* to determine the order. If the specified comparator is {@code null},
* then the returned comparator considers all non-null values to be equal.
*
* <p>The returned comparator is serializable if the specified comparator
* is serializable.
*
* @param <T> the type of the elements to be compared
* @param comparator a {@code Comparator} for comparing non-null values
* @return a comparator that considers {@code null} to be less than
* non-null, and compares non-null objects with the supplied
* {@code Comparator}.
* @since 1.8
*/
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(true, comparator);
}
/**
* Returns a null-friendly comparator that considers {@code null} to be
* greater than non-null. When both are {@code null}, they are considered
* equal. If both are non-null, the specified {@code Comparator} is used
* to determine the order. If the specified comparator is {@code null},
* then the returned comparator considers all non-null values to be equal.
*
* <p>The returned comparator is serializable if the specified comparator
* is serializable.
*
* @param <T> the type of the elements to be compared
* @param comparator a {@code Comparator} for comparing non-null values
* @return a comparator that considers {@code null} to be greater than
* non-null, and compares non-null objects with the supplied
* {@code Comparator}.
* @since 1.8
*/
public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(false, comparator);
}

Java Docs에서 볼 수 있듯이 Null과 친근한 메서드임을 첫 줄에서부터 강조한다.

각각의 기능은 잘 지어진 이름에서 바로 파악이 가능하다.

 

Null Safe하게 개선된 정렬은 다음과 같다.

List<Integer> numbers = Arrays.asList(9, 5, 7, 1, null);
numbers.stream()
.sorted(Comparator.nullsFirst(Comparator.reverseOrder()))
.forEach(x -> System.out.print(x + " "));

Comparator.nullsFirst의 결과 값은 [null, 9, 7, 5, 1]가 된다. 이와 반대로

Comparator.nullsLast의 결과 값은 [9, 7, 5, 1, null]가 된다고 쉽게 예상할 수 있다.

 

예제에서 Integer::intValue 부분이 갑자기 사라진 것을 볼 수 있는데, 이는 객체가 아닌 원자값이기 때문이다.

Integer가 null이라서 null.inValue()가 불가능하여, 삭제시켜 버렸다. 주의하자 ㅎㅎ..

 

 

정렬 예제

추후에 채울 예정...

 

 

출처

 

Last Updated. 2022. 05. 24.

 

 

블로그의 프로필 사진

블로그의 정보

배부른코딩로그

배부른코딩로그

활동하기