본문 바로가기
독서/모던 자바 인 액션

함수형 인터페이스를 통한 람다의 활용

by kriorsen 2023. 9. 24.

다양한 함수형 인터페이스

 


3.1 Predicate

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);

    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
}

Predicate는 동작 파라미터화의 두 번째 장에서 다루었던 인터페이스입니다. 여러 디폴트 메서드로 Predicate의 변환을 유연하게 할 수 있게 도와주며 필터링을 하는 상황에서 유용하게 쓰일 수 있습니다.

기존 코드에서는 Predicate의 조합을 사용한 적은 없는데 기본 메서드들을 활용하면 아래와 같은 코드를 작성할 수 있습니다.

 

예제 코드

public class FilteringBooks {
    public static void main(String[] args) {
        List<Book> bookshelf = Arrays.asList(new Book(510, Science),
                                             new Book(100, Economy),
                                             new Book(120, Science));

       Predicate<Book> bookThickPredicate = (Book book) -> book.getPage() >= 500; 
       Predicate<Book> bookCategorySciencePredicate = (Book book) -> Science.equals(book.getCategory());

       List<Book> thinBooks = filterBooks(bookshelf, bookThickPredicate.negate()); // negate 메서드 활용
       List<Book> thickScienceBooks = filterBooks(bookshelf, bookThickPredicate.and(bookCategorySciencePredicate)); // and 메서드 활용
       List<Book> scienceOrEconomyBooks = filterBooks(bookshelf, bookCategorySciencePredicate.or((Book book) -> Economy.equals(book.getCategory()); // or 메서드 활용
    }

    public static List<Book> filterBooks(List<Book> bookshelf, Predicate<Book> bookPredicate) {
        List<Book> result = new ArrayList<>();
        for (Book book : bookshelf) {
            if (bookPredicate.test(book)) {
                result.add(book);
               }
        }
        return result;
    }
}

 

3.2 Consumer

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

Consumer는 이름 그대로 소비자의 역할을 하며 하나의 매개변수를 받아서 로직을 수행하고 반환값을 가지지 않는 accept 메서드를 구현해야 합니다. 그리고 andThen 메서드를 이용해서 두 개의 Consumer 동작을 결합할 수도 있습니다.

 

예제 코드

Consumer<Book> bookPagePrinter = (Book book) -> { System.out.println(book.getPage()); };
Consumer<Book> bookTitlePrinter = (Book book) -> { System.out.println(book.getTitle()); };
Consumer<Book> bookTitleAndPagePrinter = bookTitlePrinter.andThen(bookPagePrinter);

Book book = new Book(500, "book title");
bookTitleAndPagePrinter.accept(book);

 

3.3 Function

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Funtion의 경우에는 매개변수와 반환값의 타입을 모두 고려해야 해서 Consumer나 Predicate에 비해 조금 복잡한 편입니다.함수를 결합해 주는 compose 메서드와 순차적으로 함수를 실행해 주는 andThen 메서드가 정의되어 있습니다.

 

예제코드

Function<Book, Integer> getBookPageFunction = (Book book) -> book.getPage();
Function<String, Book> getNewTitleBookFucntion = (String title) -> new Book(500, title);
Function<Integer, Book> getNewPageBookFunction = (Integer pageCount) -> new Book(pageCount, "None");

// (String) -> Integer
Integer page = getBookPageFunction.compose(getNewTitleBookFunction).apply("Hello World"); 
// (Book) -> Book
Book newPageBook = getBookPageFunction.andThen(getNewPageBookFunction).apply(new Book(500, "Title"));