본문 바로가기
JAVA/[JAVA] Stream

Functional Interface (2)

by oncerun 2022. 2. 8.
반응형

Functional Interface에서 자주 사용되는 몇 가지 함수형 인터페이스를 알아보자.

 

  • Supplier
  • Consumer
  • Predicate
  • Comparator

 

 

1. Supplier

 

공급자라는 뜻을 가지고 있는 Supplier는 input값을 받지 않고 리턴 값만 존재하는 함수형 인터페이스이다.

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

 

이 함수형 인터페이스에는 추상 메서드인 get()이 존재하고 인자는 없고 리턴 값만 존재한다.

 함수형 프로그래밍에서 함수는 1급 시민이기 때문에 인자로 사용이 가능하다.

이 Supplier를 사용해 인자를 주지않고 새로운 값을 생성하는 Supplier를 인자로 넘겨 고유 아이디를 생성해보는 연습을 진행해보자.

 

public static void printRandomUUID(Supplier<String> supplier, int count) {
    for (int i = 0; i < count; i++) {
        log.info("Random UUID {}", supplier.get());
    }
}

printRandomUUID는 supplier와 count를 받아 count만큼 supplier를 실행시키는 메서드이다. 

 

이후 다음 단계는 랜덤한 UUID를 생성하는 Supplier를 람다식으로 생성한 후 해당 함수를 변수에 할당해 

printRandomUUID의 인자로 넘겨주면 된다.

Supplier<String> createRandomUUID = () -> UUID.randomUUID().toString();
printRandomUUID(createRandomUUID, 10);

 

2. Consumer

   Consumer는 인자를 받아 리턴 값을 주지 않는 모양을 띄고 있다. 추상 메서드로 accept(T t)를 가지고 있다.

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
}

 

이번에는 좀더 범용적으로 사용하기 위해 제네릭 타입으로 특정 리스트를 넘겨주면 해당 리스트의 값을 사용하는 Consumer를 연습해보자.

 

public static void main(String[] args) {

    List<Integer> integerInputs = Arrays.asList(1,2,3);
    List<String> stringInputs = Arrays.asList(UUID.randomUUID().toString(),UUID.randomUUID().toString(),UUID.randomUUID().toString());
    List<Double> doubleInputs = Arrays.asList(Math.random(), Math.random(),Math.random());

    Consumer<Integer> integerConsumer = integer -> log.info("특정 로직처리 {}", integer);
    Consumer<String> stringConsumer = str -> log.info("특정 로직처리 {}", str);
    Consumer<Double> doubleConsumer = dou -> log.info("특정 로직처리 {}", dou);

    genericProcess(integerInputs, integerConsumer);
    genericProcess(stringInputs, stringConsumer);
    genericProcess(doubleInputs, doubleConsumer);

}

public static <T> void genericProcess(List<T> inputs, Consumer<T> processor) {
    for (T input : inputs) {
        log.info("인력값을 Consumer가 소비하면서 특정 로직을 처리한다.");
        processor.accept(input);
    }
}

 

이는 여러 장점을 가지고 있는데 함수를 인자로 넘기기 때문에 요구사항의 변경이 발생하면 특정 Consumer만 생성하여  인자로 넘겨주면 기존 genericProcess를 변경하지 않고 로직의 변경이 가능하다. 

 

 

 

3. Predicate

 Predicate는 T라는 타입을 인자로 받으며 Boolean을 리턴을 하는 추상 메서드인 test(T t)를 가지고 있습니다.

 

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

}

 

어떠한 input을 받아서 true/false를 반환해야 할 때 Predicate 인터페이스를 통해 표현할 수 있습니다.

 

Predicate<Integer> isPositive = x -> x > 0;
log.info("test {}", isPositive.test(10));

 

 

Predicate 인터페이스에는 여러 default 메서드를 가지고 있어 이 default 메서드를 활용하면 and 연산, or연산, 부정 연산이 가능하다. 

 

filtering을 하는 헬퍼 함수를 만들어 and 연산, or연산, 부정 연산을 해보자.

public static <T> List<T> filter(List<T> inputs, Predicate<T> condition) {
    //Predicate는 List를 돌면서 각 요소의 test를 진행하여 test를 통과한 이들만 반환한다.
    List<T> output = new ArrayList<>();
    for (T input : inputs) {
        if (condition.test(input)){
            output.add(input);
        }
    }
    return output;
}

Predicate 인터페이스의 default 메서드인 and()는 인자로 Predicate <? super T>를 받아 && 연산을 진행하며,

or()는 Predicate<? super T>을 받아 ||연산을 negate()는! 연산을 진행한다.

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는 0보다 크면 true를 반환한다.  

List<Integer> result1 = filter(integerInputs, isPositive);

 

Predicate 인터페이스의 default 메서드인 negate()를 활 요하면 역연산이 가능하다. 

List<Integer> result2 = filter(integerInputs, isPositive.negate());

 

이번에는 default 메서드인 and(), or()를 활용해 진행하자. 

List<Integer> result3 = filter(integerInputs, isPositive.or(x -> x == 0));
List<Integer> result4 = filter(integerInputs, isPositive.and(x -> x % 2 == 0));

 

 

java.util.function에는 수많은 함수형 인터페이스가 나열되어 있다. 하지만 기본적으로 Function, BiFunction, Consumer, Predicate, Supplier에서 primitive type을 지원하는 역할만 진행한다.

 

이는 wrapper로 감싼 객체를 연산을 하는 것보다 primitive type연산이 더 빠르고 리소스를 적게 사용하기 때문에 기본 타입에 대한 지원으로 볼 수 있다.

 

[UnaryOperator]

 UnaryOperator는 Funtion 함수형 인터페이스에서 인자와 리턴 타입이 동일하게 T타입인 것을 확인할 수 있다.

따라서 인자타입과 리턴 타입이 같은 경우 UnaryOperator를 사용할 수 있다. 

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    /**
     * Returns a unary operator that always returns its input argument.
     *
     * @param <T> the type of the input and output of the operator
     * @return a unary operator that always returns its input argument
     */
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}
UnaryOperator<Integer> unaryOperator = x -> x + 10;
Integer result = unaryOperator.apply(20);
log.info("result : {}", result);

Function<Integer, Integer> function = x -> x +10;
Integer result2 = function.apply(20);
log.info("result2 {}", result2);

 

이렇듯 수많은 인터페이스도 잘 보면 중복이 존재하고 최적화라는 관점에서 많아 보이는 것이지 막상 하나하나 뜯어보면 중심적으로 동작하는 몇 개의 함수형 인터페이스만 존재한다.

 

 

4. Comparator

 Comparator는 java.util패키지에 존재하는 Functional Interface입니다.

@FunctionalInterface
public interface Comparator<T> {
   
    int compare(T o1, T o2);

    boolean equals(Object obj);
    
}

 

4.1)  compare(T o1, T o2)

 

 T라는 제네릭 타입이 input 두 개를 받아 비교를 한 후 int를 반환합니다.

 

String의 compareTo()를 사용해 보신 분들은 아시겠지만 숫자 타입은 비교라는 것이 가능하지만 사용자 정의 객체의 비교, 문자열의 비교들은 간단하지 않습니다.  이러한 문제를 Comparator를 이용해 표현할 수 있습니다. 

 

첫 번째 인자 (o1)이 두 번째 인자(o2) 보다 작다면 음수를, 같으면 0을, o1이 o2보다 크다면 양수를 반환하도록 구현하면 됩니다.  

 

 

POJO로 User를 하나 생성한 후 List에 담아 예제를 만들어 볼 것입니다.

package com.example.functional.utill.comparator.model;

public class User {

    private Long id;

    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Collections.sort() 메서드는 2개의 인자를 받습니다. 첫 번째 인자는 <T> 타입의 List, 두 번째 인자는 <T> 타입의 Comparator입니다.

@SuppressWarnings({"unchecked", "rawtypes"})
public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

 

규약에 맞춰 첫 번째 인자가 크면 양수를 반환하고, 같으면 0을 반환, 첫 번째 인자가 두 번째 인자보다 작다면 음수를 반환하도록 하겠습니다. 

#1
Comparator<User> userComparator = (userA, userB) -> {
    if (userA.getId() > userB.getId()){
        return 1;
    }else if(userA.getId() == userB.getId()){
        return 0;
    }else{
        return -1;
    }
};

#2
Comparator<User> userComparator = (userA, userB) -> {
	return userA.getId() - userB.getId();
};

#3 
Comparator<User> userComparator = (userA, userB) -> userA.getId() - userB.getId();

 

이후 랜덤 하게 User를 추가한 후 Collections.sort() test를 진행하면 다음과 같습니다.

log.info(" before sort Users {}", users);
Collections.sort(users, userComparator);
log.info(" after sort Users {}", users);

이름 순으로 하고 싶다면 Comparator를 람다로 만들면 됩니다.

log.info(" before sort Users {}", users);
Collections.sort(users, (u1, u2) -> u1.getName().compareTo(u2.getName()));
log.info(" after sort Users {}", users);

 

 

 

[정리]

 

 지금 공부하는 것은 1급 시민으로 함수를 활용하기 위한 준비단계이다. 자바에서 함수를 어떻게 메서드의 인자로 넘기는지 친숙해지기 위한 연습이며, 또한 Functional Interface들을 몇 가지 사용해보면서 이런 식으로 활용하면 재활용성이 좋고, 가독성이 좋아진다라는 것을 배웠다. 

 

주의할 점은 가독성이라는 것은 편파적이어서 누구에게는 정말 가독성이 좋아 보이지만, 누구에게는 처음 보는 기호 혹은 단어일 수 있다. 따라서 적절한 커뮤니케이션, 스터디 등을 한 후 차후 기회가 있을 때 사용하는 것이 바람직해 보인다.

 

 

반응형

'JAVA > [JAVA] Stream' 카테고리의 다른 글

Stream (3)  (0) 2022.02.16
Stream (2)  (0) 2022.02.15
Stream  (0) 2022.02.12
Method Reference  (0) 2022.02.09
Functional Interface (1)  (0) 2022.02.07

댓글