ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] interface 활용하기: 함수형 인터페이스, Comparator<E> 구현체를 이용해 List 정렬하기
    Today I Learned 2023. 5. 16. 16:13

     

    interface의 구현체를 정의해 특정한 로직에 활용하는 방식을 좀 더 적극적으로 활용해보자. List<Element>를 원하는 기준에 맞추어 정렬해야 하는 상황이 생겼다고 한다면, 어떻게 할 수 있을까?

     

    Java의 List<E> interface에는 다음과 같이 정렬을 위한 default 메서드 sort()가 존재한다. 다음의 코드를 살펴보자.

    // List.java
    
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    cf. interface의 default 예약어에 대해 간략하게 짚고 넘어가자면, 특정 메서드 시그니쳐를 interface에서 미리 구현해놓는 것이다. interface의 개별 구현체를 정의할 때, default 메서드에 대해서는 미리 구현되어 있는 것을 그대로 쓸 수도 있고, 필요에 따라 재정의할 수도 있다.

     

    해당 메서드에서 Comparator 타입의 인스턴스를 인자로 받아 Arrays.sort() 메서드에 다시 인자로 전달하는 것을 눈여겨보도록 하자. Comparator<E> 역시 interface로, List나 배열을 특정 기준에 맞추어 정렬하는 기준을 정하는 구현체의 메서드 시그니쳐가 정의되어 있다.

     

    Comparator<E>의 구현체를 정의하기 위해 변수를 정의해보면, 다음과 같이 코드블럭으로 구분된 영역에서 Comparator<E>에 정의되어 있는 메서드 시그니쳐를 구현해야 한다.

    // Student는 임의로 정의한 객체이다.
    
    Comparator<Student> comparator = new Comparator<Student>() {
        @Override
        public int compare(Student student1, Student student2) {
            // 일반적으로는 다음의 계산 결과를 반환하도록 메서드를 구현하는데,
            // 결과값이 0보다 작을 경우 student1이 student2보다 앞에 오도록,
            // 값이 0보다 클 경우 student1이 student2의 뒤에 오도록 정렬된다.
            // 즉 비교를 위한 기준 값의 오름차순으로 정렬된다.
            
            // Student의 id 값을 기준으로 정렬한다고 할 때 다음과 같이 작성할 수 있다. 
            return student1.id() - student2.id();
            
            // 또는 다음처럼 작성할 수도 있다.
            return student1.id().compareTo(student2.id());
        }
    };

     

    Comparator<E>에서 구현해야 하는 메서드는 compare() 하나이므로, 다음과 같이 람다표현식으로 대체할 수 있다.

    Comparator<Student> comparator = (student1, student2) -> student1.id() - student2.id();
    
    // 혹은 다음처럼 작성할 수도 있다.
    Comparator<Student> comparator = (student1, student2) -> student1.id().compareTo(student2.id());

     

    이런 방식으로 Comparator<E>의 구현체를 정의하는 경우 compare() 메서드에 비교를 위한 값이나 객체 두 개를 구체적으로 드러내야 한다. 해당 값으로 단순히 정수 값을 바로 가져다 쓸 수 있다면 괜찮겠으나, 만약 비교를 위한 값을 구하기 위한 추가적인 프로세스가 필요하다면 구현체의 compare() 메서드가 상당히 복잡해질 것이다.

     

     

    Comparator<E>에는 비교를 위한 기준 값으로 사용할 것이 무엇인지만을 나타내는 방식으로 구현체를 정의할 수 있는 방식이 존재한다. Comparator<E>에는 동일한 타입의 구현체를 반환하는 정적 팩터리 메서드 comparing()이 있는데, 한번 살펴보도록 하자.

    // Comparator.java
    
    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));
    }

     

    여기서는 comparing()의 인자로 전달되는 keyExtractor와, 반환되는 람다표현식에 주목해보자. 람다표현식에서는 c1, c2 두 개의 인자를 받고 있는데, 메서드에서 인자로 받은 keyExtractor의 apply() 메서드에 c1과 c2를 각각 인자로 전달해 도출된 결과값을 compareTo()하고 있다. 즉, comparing()에서 반환하는 람다표현식으로 Comparator에서 구현해야 할 compare()를 나타내고 있는 것이다.

     

    해당 람다표현식의 몸체를 이해하기 위해 Function<T, R>과 apply() 메서드를 간단히 살펴보자.

     

    Function<T, R>함수형 인터페이스의 하나로, 정의되어 있는 단 하나의 메서드 시그니쳐 'R apply(T)'를 각 구현체에서 구현해 사용하는 interface이다. 이때 T는 apply에 전달할 인자의 타입, R은 반환받을 타입을 나타낸다. Function<T, R>에는 메서드 시그니쳐가 apply() 단 하나만 존재하므로, 다음과 같이 람다표현식으로 apply를 어떻게 구현했는지를 바로 나타낼 수 있다.

    Function<Student, Long> function = student -> student.id();
    
    // 다음과 같이 나타낼 수도 있다.
    Function<Student, Long> function = Student::id;

     

    해당 함수형 인터페이스를 Comparator<T>의 static 메서드 comparing()에 다음과 같이 인자로 전달함으로써 해당 Comparator 구현체는 비교를 위해 Student 인스턴스의 id 값을 활용함을 나타낼 수 있다.

    Comparator<Student> comparator = Comparator.comparing(Student::id);

     

    이렇게 정의한 Comparator<Student> 구현체를 List<Student> 구현체의 sort() 메서드에 인자로 전달함으로써, 해당 List를 Student 인스턴스의 id 크기를 기준으로 오름차순으로 정렬할 수 있게 되었다.

    List<Student> students = new ArrayList<>();
    
    // students에 Student 인스턴스들을 삽입한다.
    
    students.sort(Comparator.comparing(Student::id));

     

     

    그러면 Comparator로 다소 복잡한 정렬 기준을 세워 List를 정렬하는 예시를 살펴보자.

     

    이번에도 간단한 수강신청 서버 애플리케이션이 있다고 해보고, 우선순위에 맞게 수강생들을 선별하기 위해 List를 정렬하는 로직을 만들어보자. 먼저 사용할 데이터들을 살펴보자.

    - Course: 특정 과목 (ex. 교양과목 '비판적사고와토론')
    - Map<Student, List<Enrollment>>: 특정 과목의 강의에 수강신청한 학생들과, 각 학생에 대한 과거 수강 기록 쌍의 모음

     

    그럼 이제 어떤 기준을 바탕으로 List<Student>가 도출되었을 때, 이 도출된 List<Student>에서 수강한 학기가 많은 학생이 List의 앞 index에 위치하도록 정렬해야 하는 상황을 가정해보고, 이를 만족시키기 위해 정렬에 사용할 Comparator 구현체를 한 번 생성해보자.

     

    수강 학기를 판별하는 기준을 두기 위해, 과거 수강 기록에서 특정 학기에 해당하는 강의가 하나 존재할 경우 그 학기를 이수한 것으로 가정하겠다. 그러면 비교를 위한 기준 값으로 사용해야 하는 것은 학기의 개수일 것이다. 이때 특정 학기의 강의가 하나 이상 존재하는 것만 확인하면 될 것이므로, 학기의 개수를 셀 때 HashSet을 이용해 수강 기록에 존재하는 학기의 식별자를 삽입하고 최종적으로 도출된 Set의 크기를 이용해 비교하도록 할 수 있을 것이다. 해당 기준으로 Student의 우선순위를 비교하는 Comparator를 생성했다. 이때 값이 큰 사람이 앞에 와야 하므로, reversed() 메서드를 호출해 역순으로 정렬하도록 했다.

     

    public Comparator<Student> comparatorForSubStrategy(
        Map<Student, List<Enrollment>> studentAndEnrollments
    ) {
        Comparator<Student> comparator = Comparator.comparing(student -> {
            List<Enrollment> enrollments = studentAndEnrollments.get(student);
            Set<Long> semesterIds = new HashSet<>();
            enrollments.forEach(enrollment -> semesterIds
                .add(enrollment.semesterId()));
            return semesterIds.size();
        });
        return comparator.reversed();
    }

     

    해당 Comparator<Student>를 이용해 최종적으로 List<Student>의 정렬을 수행하는 로직을 다음과 같이 정의했다.

     

    public class EnrollmentPrioritizationOptionalCourseStrategy
        implements EnrollmentPrioritizationStrategy {
        @Override
        public List<Student> prioritize(Course course,
                                        Map<Student, List<Enrollment>> studentAndEnrollments) {
            List<Student> prioritizedStudents;
            // 우선순위 판별 조건에 따라 학생들을 선별한다.
    
            Comparator<Student> comparatorForSubStrategy
                = comparatorForSubStrategy(studentAndEnrollments);
    
            prioritizedStudents.sort(comparatorForSubStrategy);
    
            return prioritizedStudents;
        }
    }

     

     

    References

    - List.java

    - Comparator.java

    - Function.java

    - 가장 빨리 만나는 코어 자바 9 - 3장. 인터페이스와 람다 표현식

     

     

     

     

    댓글

Designed by Tistory.