ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Lambda Expression 2. Function 패키지
    Java 2020. 2. 8. 17:49
    반응형

    java.util.function 패키지

    일반적으로 자주 쓰이는 형식의 메서드를 함수형 인터페이스로 미리 정의해둠

    • 대부분의 메서드는 타입이 비슷함 또는 제네릭 메서드로 정의하면 타입 관계 없음

    • 매개변수도 보통 없거나 한 개 또는 두 개

    • 반환 값은 없거나 한 개

    장점

    함수형 인터페이스에 정의된 메서드 이름도 통일되고, 재사용성이나 유지보수 측면에서도 좋음

    자주 쓰이는 기본적인 함수형 인터페이스

    매개변수와 반환값의 유무에 따라 4개의 함수형 인터페이스가 정의

    Function의 변형으로 Predicate가 있는데, 반환값이 boolean인 것만 차이가 있음 - 조건식을 함수로 표현하는데 사용

    타입 문자 T는 Type을 R은 Return Type을 의미
    // 함수형 인터페이스, 메서드
    
    // 매개변수도 없고 반환값도 없음
    java.lang.Runnable void run()
    
    // 매개 변수는 없고, 반환값만 있음 - 반환 : T
    Supplier<T> T get()
    
    // Supplier의 반대로 매개변수만 있고, 반환값이 없음 - 매개변수 : T
    Consumer<T> void accept(T t)
    
    // 일반적인 함수. 하나의 매개변수를 받아서 결과를 반환 - 매개변수 : T, 반환 : R
    Function<T, R> R apply(T t)
    
    // 조건식을 표현하는데 사용됨. 매개변수는 하나, 반환 타입은 boolean - 매개변수 : T, 반환 : boolean
    Predicate<T> boolean test(T t)

    조건식의 표현에 사용되는 Predicate

    Predicate는 조건식을 람다식으로 표현하는데 사용됨

    Function의 변형으로, 반환타입이 boolean이라는 것만 다름

    수학에서는 결과로 true 또는 false로 반환하는 함수를 '프레디케이트(predicate)'라고 함'

    매개변수가 두 개인 함수형 인터페이스

    매개변수의 개수가 2개인 함수형 인터페이스는 이름 앞에 접두사 'Bi'가 붙음

    매개변수의 타입으로 보통 'T'를 사용하므로, 알파벳에서 'T'의 다음 문자인 'U', 'V', 'W'를 매개변수의 타입으로 사용할 뿐 다른 의미는 없음
    // Supplier의 경우 매개변수가 없고 반환값만 존재하여 해당되지 않음
    
    // 두 개의 매개변수만 있고, 반환 값이 없음 - 매개변수 : T, U
    BiConsumer<T, U> void accept(T t, U u)
    
    // 조건식을 표현하는데 사용됨, 매개변수는 둘, 반환값은 boolean - 매개변수 : T, U / 반환값 : boolean
    BiPredicate<T, U> boolean test(T t, U u)
    
    // 두 개의 매개변수를 받아서 하나의 결과를 반환 - 매개변수 : T, U / 반환값 : R
    BiFunction<T, U, R>

    두 개 이상의 매개변수를 갖는 함수형 인터페이스

    필요시 직접 만들어서 써야 함

    만일, 3개의 매개변수를 갖는 함수형 인터페이스를 선언해야 한다면?

    @FunctionalInterface
    interface TriFunction<T,U,V,R> {
        R apply(T t, U u, V v);
    }

    UnaryOperator와 BinaryOperator

    UnaryOperator의 조상은 Function, BinaryOperator의 조상은 BiFunction

    매개변수의 타입과 반환타입의 타입이 모두 일치한다는 점만 제외하고는 Function과 같음

    /* Function의 자손
     * Function과 달리 매개변수와 결과의 타입이 같음
     */
    UnaryOperator<T> T apply(T t)
    
    /* BiFunction의 자손
     * BiFunction과 달리 매개변수와 결과의 타입이 같음
     */
    BinaryOperator<T> T apply(T t)

    컬렉션 프레임웍과 함수형 인터페이스

    컬렉션 프레임웍의 인터페이스에 다수의 디폴트 메서드가 추가

    그 중의 일부는 함수형 인터페이스를 사용함

    와일드 카드는 생략하였음
    Collection Interface
        // 조건에 맞는 요소를 삭제
        boolean removeIf(Predicate<E> filter)
        
    List Interface
        // 모든 요소를 변환하여 대체
        void replaceAll(UnaryOperator<E> operator)
    
    Iterable Interface
        // 모든 요소에 작업 action을 수행
        void forEach(Consumer<T> action)
        
    Map Interface
        // 지정된 키의 값에 작업 f를 수행
        V compute(K key, BiFunction<K, V, V> f)
        // 키가 없으면, 작업 f 수행 후 추가
        V computeIfAbsent(K key, Function<K, V> f)
        // 지정된 키가 있을 때, 작업 f 수행
        V computeIfPresent(K key, BiFunction<K, V, V> f)
        // 모든 요소에 병합작업 f를 수행
        V merge(K key, V value, BiFunction<V, V, V> f)
        // 모든 요소에 작업 action을 수행
        void forEach(BiConsumer<K, V> action)
        // 모든 요소에 치환작업 f를 수행
        void replaceAll(BiFunction<K, V, V> f)

    Map 인터페이스의 compute메서드들은 Map의 value를 변환하는 작업, merge()는 Map을 병합하는 작업

    기본형을 사용하는 함수형 인터페이스

    위의 함수형 인터페이스들은 모두 제네릭 - 래퍼클래스 사용

    기본형 대신 래퍼클래스(wrapper)를 사용하는 것은 당연히 비효율적

    보다 효율적으로 처리할 수 있도록 기본형을 사용하는 함수형 인터페이스들이 제공됨

    // AToBFunction은 입력이 A타입 출력이 B타입
    DoubleToIntFunction int applyAsInt(double d)
    
    // ToBFunction은 출력이 B타입, 입력은 제네릭타입
    ToIntFunction<T> int applyAsInt(T value)
    
    // AFunction은 입력이 A타입, 출력은 제네릭타입
    IntFunction R apply(T t, U u)
    
    // ObjAFunction은 입력이 T, A타입이고 출력은 없음
    ObjIntConsumer<T> void accept(T t, U u)

    매개변수의 타입과 반환 타입이 일치할 때는 Function대신 UnaryOperator를 사용할 것

    // 오토박싱 & 언박싱의 횟수가 줄어들기 때문에 성능이 더 좋음
    IntUnaryOperator > Function & IntFunction
    
    IntFunction, ToIntFunction, IntToLongFunction
    // IntToIntFunction > 존재하지 않음 - IntUnaryOperator가 그 역할을 대신 하기 떄문

    Function의 합성 & Predicate의 결합

    java.util.function Package의 함수형 인터페이스는 추상 메서드 외에도 default 메서드와 static 메서드가 정의되어 있음

    그 중, Function과 Predicate에 정의된 메서드를 알아보자

    (다른 함수형 인터페이스의 메서드는 이와 유사함)

    Function인터페이스의 경우 반드시 두 개의 타입을 지정해주어야 하기 때문에, 두 타입이 같아도 Function<T>로 쓸 수 없고, Function<T, T>로 써야 함
    Function
    default <V> Function<T, V> andThen(Function<? super R, ? extends V) after)
    default <V> Function<V, R> compose(Function<? super V, ? extends T) before)
    static  <T> Function<T, T> identity()
    
    Predicate
    default Predicate<T>    and(Predicate<? super T> other)
    default Predicate<T>    or(Predicate<? super T> other)
    default Predicate<T>    negate()
    static <T> Predicate<T> isEqual(Object targetRef)

    Function의 합성

    수학 = 두 함수를 합성하여 하나의 새로운 함수를 만들어낼 수 있음

    람다식 = 두 람다식을 합성해서 새로운 람다식을 만들 수 있음

    두 함수의 합성은 어느 함수를 먼저 적용하느냐에 따라 달라짐

    함수 f, g가 있을 때

    • f.andThen(g)는 함수 f를 먼저 적용한 후 함수 g를 적용

    • f.compose(g)는 함수 g를 먼저 적용한 후 함수 f를 적용

    andThen()과 compose()의 비교

    // -> T -> Function<T, V> ( Function<T, R>.apply -> Function<R, V>.apply) -> V ->
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
    
    // -> V -> Function<V, R> ( Function<V, T>.apply -> Function<T, R>.apply) -> R ->
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)

    예시) 문자열을 숫자로 변환하는 함수 f와 숫자를 2진 문자열로 변환하는 함수 g를 andThen()으로 합성하여 새로운 함수 h를 만들어보자

    Function<String, Integer> f = (s) -> Integer.parseInt(s, 16);
    Function<Integer, String> g = (i) -> Integer.toBinaryString(i);
    Function<String, String>  h = f.andThen(g);
    
    /* 함수 f의 제네릭타입은 String, String이므로 String을 입력받아서 String을 결과로 반환
     * 예) 문자열 "FF"를 입력하면, 결과로 "11111111"를 얻음
     */
    System.out.println(h.apply("FF")); // "FF" -> 255 -> "11111111";
    
    /* String FF가 Input으로 들어온다.
     * Function<String, Integer> f의 내용인 Integer.parseInt(s, 16)이 apply
     * Integer 255가 다음으로 넘어간다
     * Function<Integer, String> g의 내용인 Integer.toBinaryString(i)가 apply
     * String "11111111"가 리턴된다.
     */

    예시) compose()를 통해 위의 두 함수를 반대의 순서로 합성해보자.

    Function<Integer, String>  g = (i) -> Integer.toBinaryString(i);
    Function<String, Integer>  f = (s) -> Integer.parseInt(s, 16);
    Function<Integer, Integer> h = f.compose(g);
    
    /* 제네릭 타입이 <Integer, Integer>
     * 함수 h에 숫자 2를 입력하면, 결과로 16을 얻음
     * 함수 f는 "10"을 16진수로 인식하기 때문에, 16을 결과로 얻는 것
     */
    System.out.println(h.apply(2)); // 2 -> "10" -> 16
    
    /* Integer 2를 넣으면
     * Function<Integer, String> g = (i) -> Integer.toBinaryString(i)가 apply
     * String "10"이 넘어가고
     * Function<String, Integer> f = (s) -> Integer.parseInt(s, 16)이 apply
     * Integer 16이 나옴
     */

    항등 함수 : 함수에 x를 대입하면 결과가 x인 함수를 말함(f(x) = x)

    identity()는 함수를 적용하기 이전과 이후가 동일한 '항등 함수'가 필요할 때 사용

    이 함수를 람다식으로 표현하면 'x -> x'

    잘 사용되진 않는 편이며, map()으로 변환작업시, 변 없이 그대로 처리하고자 할 때 사용

    Function<String, String> f = x -> x;
    // Function<String, String> f = Function.identity(); // 위의 문장과 동일함
    
    System.out.println(f.apply("AAA")); // AAA가 그대로 출력됨

    Predicate의 결합

    조건식 = 여러 조건식을 논리 연산자인 &&(and), ||(or), !(not)으로 연결해서 하나의 식으로 구성할 수 있음

    Predicate = 여러 Predicate를 and(), or(), negate()로 연결해서 하나의 샤ㅐ로운 Predicate로 결합할 수 있음

    Predicate<Integer> p = i -> i < 100;
    Predicate<Integer> q = i -> i < 200;
    Predicate<Integer> r = i -> i % 2 == 0;
    Predicate<Integer> notP = p.negate();    // i >= 100 - 조건식 부정
    
    // 100 <= i && (i < 200 || i % 2 == 0)
    Predicate<Integer> all = notP.and(q.or(r));
    System.out.println(all.test(150));       // true
    
    // 람다식을 직접 넣어도 무관함
    Predicate<Integer> all2 = notP.and(i -> i < 200).or(i -> i%2 == 0);

    static 메서드 isEqual() : 두 대상을 비교하는 Predicate를 만들 때 사용

    // isEqual()의 매개변수로 비교대상을 하나 지정하고, 또 다른 비교대상은 test()의 매개변수로 지정
    Predicate<String> p = Predicate.isEqual(str1);
    // str1과 str2가 같은지 비교하여 결과 반환
    boolean result = p.test(str2);
    
    /* 위의 두 문장을 합치면 - 오히려 이해하기 더 편함
     * str1과 str2가 같은지 비교
     */
    boolean result = Predicate.isEqual(str1).test(str2);

    메서드 참조

    람다식으로 메서드를 간결하게 표현할 수 있지만, 람다식을 더욱 간결하게 표현할 수 있는 방법이 있음

    단) 항상 사용할 수 있는 것은 아님, 람다식이 하나의 메서드만 호출하는 경우 "메서드 참조(method reference)"라는 방법으로 람다식을 간략히 할 수 있음

    메서드 참조는 람다식을 마치 static 변수처럼 다룰 수 있게 해줌, 코드를 간략히 하는데 유용해서 많이 사용됨

    // 예시 문자열을 정수로 변환하는 람다식
    Function<String, Integer> f = (String s) -> Integer.parseInt(s);
    
    // 위 람다식을 메서드로 표현한다면(익명 클래스의 객체지만, 간단히 메서드만 적어보자면)
    Integer wrapper(String s) {
        return Integer.parseInt(s);
    }
    
    /* 위 wrapper 메서드는 값을 받아서 Integer.parseInt()에게 넘겨주는 일만 함
     * 거추장스러운 메서드를 벗겨내고 Integer.parseInt()를 직접 호출하는 것이 낫지 않을까?
     */
    Function<String, Integer> f = (String s) -> Integer.parseInt(s);
    
    Function<String, Integer> f = Integer::parseInt; // 메서드 참조
    /* 위의 메서드 참조에서 람다식의 일부가 생략되었지만, 
     * 컴파일러는 생략된부분을 우변의 parseInt메서드의 선언부로부터,
     * 또는 좌변의 Function 인터페이스에 지정된 제네릭 타입으로부터 쉽게 알아낼 수 있음
     */
    // 타입만 봐도 람다식이 두 개의 String 타입의 매개변수를 받는 다는 것을 알고 있으므로, 생략 가능
    BiFunction<String, String, Boolean> f = (s1, s2) -> s1.equals(s2);
    
    /* 메서드 참조로 바꾸면
     * equals의 경우, 다른 클래스도 동일 이름의 메서드가 존재할 수 있기 때문에 equals 앞에 클래스명이 필요
     */
    BiFunction<String, String, Boolean> f = String::equals;

    이미 생성된 객체의 메서드를 람다식에서 사용할 경우엔, 클래스 이름 대신 그 객체의 참조변수를 적어줘야 함

    MyClass obj = new MyClass();
    Function<String, Boolean> f  = (x) -> obj.equals(x); // 람다식
    Function<String, Boolean> f2 = obj::equals;          // 메서드 참조

    3가지 메서드 참조 정리

    즉, 하나의 메서드만 호출하는 람다식은 '클래스이름::메서드이름' 또는 '참조변수::메서드이름'으로 바꿀 수 있음

    // static메서드 참조
    (x) -> ClassName.method(x)
    ClassName::method
    
    // 인스턴스메서드 참조
    (obj, x) -> obj.method(x)
    ClassName::method
    
    // 특정 객체 인스턴스 메서드 참조
    (x) -> obj.method(x)
    obj::method

    생성자의 메서드 참조

    생성자를 호출하는 람다식도 메서드 참조로 변환할 수 있음

    // 람다식
    Supplier<MyClass> s = () -> new MyClass();
    
    // 메서드 참조
    Supplier<MyClass> s = MyClass::new;

    매개 변수가 있는 생성자는, 매개변수의 개수에 따라 알맞은 함수형 인터페이스를 사용하면 됨

    필요하다면 함수형 인터페이스를 새로 정의할 것

    // 람다식
    Function<Integer, MyClass> f = (i) -> new MyClass(i);
    // 메서드 참조
    Function<Integer, MyClass> f2 = MyClass::new;
    
    // 람다식
    BiFunction<Integer, String, MyClass> bf = (i, s) -> new MyClass(i, s);
    // 메서드 참조
    BiFunction<Integer, String, MyClass> bf2 = MyClass::new;

    배열을 생성할 때

    // 람다식
    Function<Integer, int[]> f = x -> new int[x];
    // 메서드 참조
    Function<Integer, int[]> f2 = int[]::new;
    반응형

    'Java' 카테고리의 다른 글

    Stream(스트림) - 2. 스트림 생성하기  (0) 2020.02.13
    Stream(스트림) - 1. 스트림이란  (0) 2020.02.13
    Lambda Expression 1. 람다식  (0) 2020.02.08
    Enum(열거형)  (0) 2020.02.08
    서버 통신 - Socket / UDP  (0) 2020.02.07

    댓글

Designed by Tistory.