[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()가 불가능하여, 삭제시켜 버렸다. 주의하자 ㅎㅎ..
정렬 예제
추후에 채울 예정...
출처
- Stack Excahnge - handle null in Comparator class, dariosicily, 2019-09-23
- Comparator (Java Platform SE 8 ) (oracle.com), Oracle Java8 Docs
Last Updated. 2022. 05. 24.
블로그의 정보
배부른코딩로그
배부른코딩로그