자바

java collection sort using Comparator

알쓸개잡 2023. 10. 17.

java  collection 프레임워크에서 Comparator 인터페이스를 이용하여 sorting 처리 방법에 대해서 기록한다.

Comparator 인터페이스

  • Comparator 인터페이스는 int compare(T o1, T o2) 형태의 FunctionalInterface이다.
  • Comparator 인터페이스 구현체를 collection 인스턴스의 sort() 메서드에 전달할 수 있다.
    • sort() 메서드 내부적으로 Comparator 인스턴스의 compare() 메서드를 호출한다.
    • sort() 메서드는 내부적으로 인자로 전달된 Comparator 인스턴스의 compare() 호출의 결과가 > 0인 경우 swap 한다.

Comparator.comparing()

Comparator 인스턴스는 몇 가지 유용한 default 및 static 메서드를 제공하는데 그중 하나가 comparing() 메서드다.

2개의 comparing() 메서드를 제공하는데 메서드 정의는 아래와 같다.

public static <T, U> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator)
{
    Objects.requireNonNull(keyExtractor);
    Objects.requireNonNull(keyComparator);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                          keyExtractor.apply(c2));
}

Comparator.comparing(Function<> keyExtractor, Comparator<> keyComparator) 메서드는

  • keyExtractro.apply() 호출 결과 리턴되는 인스턴스가 정렬의 대상이 된다.
  • keyComparator 인스턴스의 compare()를 통해서 정렬을 한다.

 

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
{
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

Comparator.comparing(Function<> keyExtractor) 메서드는

  • keyExtractor.apply() 호출 결과 리턴되는 인스턴스가 정렬의 대상이 된다.
  • 정렬의 대상이 되는 인스턴스는 compareTo 메서드를 구현하고 있어야 한다.

 

간단한 샘플코드를 통해서 사용법을 알아보자.

package com.example.collection.sorting.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@RequiredArgsConstructor
@Getter
@ToString
public class SortObject {
	private final Long id;
	private final String name;
	private final Grade grade;

	public enum Grade {
		A,
		B,
		C,
		D,
		E,
		;
	}
}

위와 같은 정렬 대상 객체를 정의하였다.

 

다음은 Comparator 인터페이스를 구현하는 구현체 클래스다.

Comparator 인터페이스 구현 클래스를 작성했지만 Comparator는 FunctionalInterface 이므로 lambda로 바로 사용이 가능하다.

package com.example.collection.sorting.comparator;

import com.example.collection.sorting.dto.SortObject;

import java.util.Comparator;

public class NameComparator implements Comparator<SortObject> {
	/*
	o1.getName() > o2.getName : return > 0
	o1.getName() == o2.getName : return 0
	o1.getName() < o2.getName() : return < 0
	*/
	@Override
	public int compare(SortObject o1, SortObject o2) {
		return o1.getName().compareTo(o2.getName());
	}
}

Comparator 인터페이스를 구현하는 구현체 클래스다.

 

다음과 같은 SortObject를 저장하는 List가 있다.

List<SortObject> collection;

@BeforeEach
void createList() {
    collection = new java.util.ArrayList<>(List.of(
        new SortObject(1L, "Kim", SortObject.Grade.A),
        new SortObject(2L, "Park", SortObject.Grade.E),
        new SortObject(3L, "Jung", SortObject.Grade.B),
        new SortObject(4L, "Kim", SortObject.Grade.D),
        new SortObject(5L, "Lee", SortObject.Grade.C),
        new SortObject(6L, "Choi", SortObject.Grade.E)
    ));
}

 

다음 테스트 코드는 SortObject의 name 속성에 따라서 오름차순으로 정렬한다.

@Test
@DisplayName("이름별 오름차순 정렬 테스트")
void name_ascending_sort_test() {
    collection.sort(new NameComparator());
    collection.forEach(System.out::println);

    System.out.println("================================");

    collection.sort(Comparator.comparing(SortObject::getName));
    collection.forEach(System.out::println);
}
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)
================================
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)

 

다음 테스트 코드는 SortObject의 name 속성에 따라서 내림차순으로 정렬한다.

package com.example.collection.sorting.comparator;

import com.example.collection.sorting.dto.SortObject;

import java.util.Comparator;

public class NameComparatorReverse implements Comparator<SortObject> {
	@Override
	public int compare(SortObject o1, SortObject o2) {
		return o2.getName().compareTo(o1.getName());
	}
}
@Test
@DisplayName("이름별 내림차순 정렬 테스트")
void name_descending_sort_test() {
    collection.sort(new NameComparator().reversed());
    collection.forEach(System.out::println);

    System.out.println("================================");

    collection.sort(Comparator.comparing(SortObject::getName, Comparator.reverseOrder()));
    collection.forEach(System.out::println);

    System.out.println("================================");

    collection.sort(new NameComparatorReverse());
    collection.forEach(System.out::println);
}

Comparator 인터페이스의 default 메서드인 reversed()를 호출하여 compare()의 수행 결과를 뒤집을 수 있다 (reversed).

혹은 NameComparatorReverse 인스턴스를 sort() 메서드에 전달하여도 결과는 동일하다.

SortObject(id=2, name=Park, grade=E)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=6, name=Choi, grade=E)
================================
SortObject(id=2, name=Park, grade=E)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=6, name=Choi, grade=E)
================================
SortObject(id=2, name=Park, grade=E)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=6, name=Choi, grade=E)

 

Comparator.thenComparing()

Comparator 인터페이스는 thenComparing() default 메서드를 제공한다.

thenComparing(Comparator<? super T> other) 메서드는 동일한 값을 가진 요소 내에서 또 다른 정렬을 하고자 할 때 사용한다.

테스트 코드에서도 name이 동일한 경우 grade 속성으로 내림 차순으로 정렬하도록 thenComparing()을 호출하였다.

thenComparing(Comparator<? super T> other) 메서드 정의는 아래와 같다.

default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        //compare 수행결과 서로 동일한 경우 other Comparator로 비교함
        return (res != 0) ? res : other.compare(c1, c2);
    };
}

추가적으로 thenComparing을 오버로딩한 메서드가 있는데 결국에는 위의 thenComparing을 호출한다.

default <U> Comparator<T> thenComparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator)
{
    return thenComparing(comparing(keyExtractor, keyComparator));
}

default <U extends Comparable<? super U>> Comparator<T> thenComparing(
        Function<? super T, ? extends U> keyExtractor)
{
    return thenComparing(comparing(keyExtractor));
}

 

 

다음 테스트 코드는 SortObject의 name 속성에 따라서 오름차순으로 정렬하고 동일한 name의 경우 grade는 내림차순으로 정렬한다. 

@Test
@DisplayName("이름별 오름차순 그리고 성적별 내림차순 테스트")
void name_descending_grade_ascending_sort_test() {
    Comparator<SortObject> comparator = new NameComparator()
        .thenComparing(SortObject::getGrade, Comparator.reverseOrder());

    collection.sort(comparator);
    collection.forEach(System.out::println);

    System.out.println("================================");

    Comparator<SortObject> comparatorLambda =
        Comparator.comparing(SortObject::getName)
            .thenComparing(SortObject::getGrade, Comparator.reverseOrder());
    collection.sort(comparatorLambda);
    collection.forEach(System.out::println);
}

 

다음은 실행 결과이다.

SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)
================================
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)

 

null 요소 정렬

Comparator 인터페이스는

  • public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)
  • public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) 

static 메서드를 제공하는데 이는 collection 내부의 null 요소를 제일 앞 혹은 제일 뒤로 정렬하는 역할을 한다.

@Test
@DisplayName("null 요소 정렬 테스트")
void name_ascending_sort_nulls_test() {
    List<SortObject> list = new ArrayList<>();
    list.add(new SortObject(1L, "Kim", SortObject.Grade.A));
    list.add(new SortObject(2L, "Park", SortObject.Grade.E));
    list.add(null);
    list.add(new SortObject(3L, "Jung", SortObject.Grade.B));
    list.add(new SortObject(4L, "Kim", SortObject.Grade.D));
    list.add(new SortObject(5L, "Lee", SortObject.Grade.C));
    list.add(null);
    list.add(new SortObject(6L, "Choi", SortObject.Grade.E));

    list.sort(Comparator.nullsFirst(Comparator.comparing(SortObject::getName)));
    list.forEach(System.out::println);

    System.out.println("================================");

    list.sort(Comparator.nullsLast(Comparator.comparing(SortObject::getName)));
    list.forEach(System.out::println);
}
null
null
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)
================================
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)
null
null

 

map의 value를 리스트로 정렬하기

hashmap의 value를 추출하여 정렬된 List로 추출하는 stream 내에서 사용할 수 있다.

@Test
@DisplayName("map value list 정렬")
void map_sort_test() {
    Map<Long, SortObject> map = new HashMap<>();
    map.put(1L, new SortObject(1L, "Kim", SortObject.Grade.A));
    map.put(2L, new SortObject(2L, "Park", SortObject.Grade.E));
    map.put(3L, new SortObject(3L, "Jung", SortObject.Grade.B));
    map.put(4L, new SortObject(4L, "Kim", SortObject.Grade.D));
    map.put(5L, new SortObject(5L, "Lee", SortObject.Grade.C));
    map.put(6L, new SortObject(6L, "Choi", SortObject.Grade.E));

    Stream<SortObject> sortedList = map.entrySet()
        .stream()
        .map(Map.Entry::getValue)
        .sorted(Comparator.comparing(SortObject::getName));

    sortedList.forEach(System.out::println);
}
SortObject(id=6, name=Choi, grade=E)
SortObject(id=3, name=Jung, grade=B)
SortObject(id=1, name=Kim, grade=A)
SortObject(id=4, name=Kim, grade=D)
SortObject(id=5, name=Lee, grade=C)
SortObject(id=2, name=Park, grade=E)

 

TreeMap key 정렬

TreeMap의 key를 정렬하는데 Comparator 인터페이스가 사용된다.

Comparator 인스턴스가 지정되지 않은 경우 TreeMap의 Key 타입은 compareTo 메서드를 구현해야 한다.

 

Comparator 인스턴스 지정하지 않은 경우

SortObject를 key로 하는 TreeMap의 SortObject 인스턴스의 hash code 기준으로 정렬하는 코드이다.

package com.example.collection.sorting.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;

@RequiredArgsConstructor
@Getter
@ToString
public class SortObject implements Comparable<SortObject>{
	private final Long id;
	private final String name;
	private final Grade grade;

	@Override
	public int compareTo(SortObject o) {
		return Integer.compare(hashCode(), o.hashCode());
	}

	public enum Grade {
		A,
		B,
		C,
		D,
		E,
		;
	}
}
@Test
@DisplayName("TreeMap key 인스턴스의 compareTo 호출에 의한 정렬")
void treemap_test() {
	//TreeMap에 Comparator 인스턴스를 지정하지 않으면
    //SortObject 인스턴스의 compareTo를 호출하여 정렬한다.
    Map<SortObject, Long> treeMap = new TreeMap<>();
    treeMap.put(new SortObject(1L, "Kim", SortObject.Grade.A), 1L);
    treeMap.put(new SortObject(2L, "Park", SortObject.Grade.E), 2L);
    treeMap.put(new SortObject(3L, "Jung", SortObject.Grade.B), 3L);
    treeMap.put(new SortObject(4L, "Kim", SortObject.Grade.D), 4L);
    treeMap.put(new SortObject(5L, "Lee", SortObject.Grade.C), 5L);
    treeMap.put(new SortObject(6L, "Choi", SortObject.Grade.E), 6L);

    treeMap.forEach(((sortObject, aLong) ->
        System.out.println(sortObject + ", hash=" + sortObject.hashCode())));
}
SortObject(id=1, name=Kim, grade=A), hash=109228794
SortObject(id=2, name=Park, grade=E), hash=561959774
SortObject(id=4, name=Kim, grade=D), hash=580871917
SortObject(id=5, name=Lee, grade=C), hash=823723302
SortObject(id=6, name=Choi, grade=E), hash=1714078840
SortObject(id=3, name=Jung, grade=B), hash=2110756088

 

Comparator 인스턴스 지정

@Test
@DisplayName("TreeMap에 Comparator 지정에 의한 정렬 - 이름 정렬")
void treemap_sort_test() {
    Comparator<SortObject> comparator = Comparator.comparing(SortObject::getName);
    //SortObject에 compareTo 메서드 구현은 없어도 무방하다.
    SortedMap<SortObject, Long> treeMap = new TreeMap<>(comparator);
    treeMap.put(new SortObject(1L, "Kim", SortObject.Grade.A), 1L);
    treeMap.put(new SortObject(2L, "Park", SortObject.Grade.E), 2L);
    treeMap.put(new SortObject(3L, "Jung", SortObject.Grade.B), 3L);
    treeMap.put(new SortObject(4L, "Kim", SortObject.Grade.D), 4L);
    treeMap.put(new SortObject(5L, "Lee", SortObject.Grade.C), 5L);
    treeMap.put(new SortObject(6L, "Choi", SortObject.Grade.E), 6L);

    treeMap.forEach(((sortObject, aLong) ->
        System.out.println(sortObject + ", id=" + aLong)));
}
SortObject(id=6, name=Choi, grade=E), id=6
SortObject(id=3, name=Jung, grade=B), id=3
SortObject(id=1, name=Kim, grade=A), id=4
SortObject(id=5, name=Lee, grade=C), id=5
SortObject(id=2, name=Park, grade=E), id=2

결과를 보면 SortObject(1L, "Kim", SortObject.Grade.A) 키의 값이 4로 대체되었다.

이유는 TreeMap의 put() 메서드는 동일한 정렬(여기서는 SortObject의 name 속성) 값이 있는 경우에는 기존 value를 대체한다.

동일한 정렬 값에 대해서 기존 value를 대체하지 않도록 하려면 putIfAbsent() 메서드를 사용한다.

@Test
@DisplayName("TreeMap에 Comparator 지정에 의한 정렬 - 이름 정렬")
void treemap_sort_test() {
    Comparator<SortObject> comparator = Comparator.comparing(SortObject::getName);
    //SortObject에 compareTo 메서드 구현은 없어도 무방하다.
    SortedMap<SortObject, Long> treeMap = new TreeMap<>(comparator);
    treeMap.putIfAbsent(new SortObject(1L, "Kim", SortObject.Grade.A), 1L);
    treeMap.putIfAbsent(new SortObject(2L, "Park", SortObject.Grade.E), 2L);
    treeMap.putIfAbsent(new SortObject(3L, "Jung", SortObject.Grade.B), 3L);
    treeMap.putIfAbsent(new SortObject(4L, "Kim", SortObject.Grade.D), 4L);
    treeMap.putIfAbsent(new SortObject(5L, "Lee", SortObject.Grade.C), 5L);
    treeMap.putIfAbsent(new SortObject(6L, "Choi", SortObject.Grade.E), 6L);

    treeMap.forEach(((sortObject, aLong) ->
        System.out.println(sortObject + ", id=" + aLong)));
}
SortObject(id=6, name=Choi, grade=E), id=6
SortObject(id=3, name=Jung, grade=B), id=3
SortObject(id=1, name=Kim, grade=A), id=1
SortObject(id=5, name=Lee, grade=C), id=5
SortObject(id=2, name=Park, grade=E), id=2

SortObject(1L, "Kim", SortObject.Grade.A) 키의 값이 1로 유지되지만 SortObject(4L, "Kim", SortObject.Grade.D) 키가 중복으로 인식되어 누락되었다.

TreeMap 사용 시 이러한 문제를 해결하기 위해서는 키 정렬 속성이 되도록 중복되지 않는 속성으로 정하는 것이 좋다.

중복되는 속성 값이 있을 수 있는 경우에는 sub 정렬 속성을 지정하여 복합 정렬이 실행되도록 하는 것이 좋다.

 

@Test
@DisplayName("TreeMap에 Comparator 지정에 의한 정렬 - 이름 정렬")
void treemap_sort_test() {
    Comparator<SortObject> comparator =
        Comparator.comparing(SortObject::getName).thenComparing(SortObject::getId);
    //SortObject에 compareTo 메서드 구현은 없어도 무방하다.
    SortedMap<SortObject, Long> treeMap = new TreeMap<>(comparator);
    treeMap.putIfAbsent(new SortObject(1L, "Kim", SortObject.Grade.A), 1L);
    treeMap.putIfAbsent(new SortObject(2L, "Park", SortObject.Grade.E), 2L);
    treeMap.putIfAbsent(new SortObject(3L, "Jung", SortObject.Grade.B), 3L);
    treeMap.putIfAbsent(new SortObject(4L, "Kim", SortObject.Grade.D), 4L);
    treeMap.putIfAbsent(new SortObject(5L, "Lee", SortObject.Grade.C), 5L);
    treeMap.putIfAbsent(new SortObject(6L, "Choi", SortObject.Grade.E), 6L);

    treeMap.forEach(((sortObject, aLong) ->
        System.out.println(sortObject + ", id=" + aLong)));
}
SortObject(id=6, name=Choi, grade=E), id=6
SortObject(id=3, name=Jung, grade=B), id=3
SortObject(id=1, name=Kim, grade=A), id=1
SortObject(id=4, name=Kim, grade=D), id=4
SortObject(id=5, name=Lee, grade=C), id=5
SortObject(id=2, name=Park, grade=E), id=2

Comparator.thenComparing() 메서드를 사용하여 이름이 중복되는 경우 중복되는 요소 내에서 다시 Id로 정렬하도록 하였다.

댓글

💲 추천 글