티스토리 뷰

Live Study

Live Study_Week 15. Lambda Expression

Junior Lob! 2021. 3. 1. 01:44

 

 

함수형 프로그래밍이란?

순수 함수들을 조합하여 사이드 이펙트(부작용)를 제거하고, 모듈화를 높여 유지보수와 생산성을 올리는데 초점을 둔 패러다임이다. Non Blocking과 Asynchronous , Parallel Programming을 구현, 지원하는데 적합하다고 한다.

 

함수형 프로그래밍의 사고방식은 문제 해결에 대해 선언적인 행위(함수)들을 조합(구성)하여 해결하는 것이다.

자바도 스칼라, 자바스크립트와 같은 함수형 패러다임 언어 혹은 지원하는 언어, 기술들의 대두로 인하여 JDK 8부터 해당 기능을 도입하게 되었다.

 

함수형 인터페이스, 람다, 메서드 레퍼런스, 디폴트 메서드, Future, Fork-Join, 리액티브 등 추가

 

 

1급 객체

함수형 프로그래밍의 중요한 조건 중 하나를 의미한다. 이는 변수나 데이터 구조 안에 넣을 수 있고, 인자로 전달 가능하고, 동적으로 속성 값을 할당 가능하며, 리턴 값으로도 사용될 수 있는 메서드를 말한다.

 

 

순수 함수

같은 입력에 대해서 항상 같은 출력을 반환하는 형태의 메서드를 의미한다. 이는 인자로 주어지는 것들만 사용하고, 상태를 유지하지 않으며, 함수 외부 변수를 사용하지 않는 형태이다. 멀티스레드 환경에서 안전한 메서드.

public int minus(int a, int b) {
    return a - b;
}

 

 

고차 함수

1급 객체의 서브셋으로 메서드의 인자로 전달할 수 있고, 리턴 값으로 사용할 수 있는 메서드를 의미한다.

 

자바에서의 고차 함수는 하나 이상의 인자로 Lambda 식을 가지고 있거나, Lambda 식을 반환하는 메서드를 말한다.

default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}

 

 

익명 함수

이름이 없는 함수를 의미하며, 이는 Lambda expression으로 구현되는 메서드를 의미한다.

 

 

합성 함수

원하는 값을 도출하기 위해, 둘 이상의 메서드를 조합하는 것을 말한다. Stream API 처럼 데이터가 흐르는 파이프라인을 구성하고, 필요한 메서드를 연속적으로 호출하여 구현한다.

 

 

 

람다식 사용법

Example

람다는 익명 클래스를 단순화하고 표현식을 메서드의 인수로 넘기거나, 객체를 생성하는 데 사용한다.

 

익명 객체 생성하기

Thread thread = new Thread(new Runnable() {

        @Override
        public void run() {
                System.out.println("Hello Lambda!");
        }

});

thread.start();

------------------------

// 익명 객체 내부에 한줄 코드만 존재하는 경우, { }와 return을 생략하고 작성할 수 있다.

Thread thread = new Thread( () -> System.out.println("Hello Lambda!") ); 

------------------------

// 2줄인 경우

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello Lambda!");
            System.out.println("Line");
        }
});

------------------------

Thread thread = new Thread(() -> {
        System.out.println("Hello Lambda!");
        System.out.println("Line");
});

 

 

메서드의 인수로 넘기기

// JDK Dynamic Proxy code
TargetObject getRealObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            TargetObject targetObject = new TargetObjectImpl();

            System.out.println("Before");
            Object invoke = method.invoke(targetObject, args);
            System.out.println("After");

            return invoke;
        }});

------------------------

// InvocationHandler 인터페이스의 익명 객체를 Lambda Expression으로 대체하였다.

TargetObject realObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class},
            (proxy, method, args) -> {
                TargetObject targetObject = new TargetObjectImpl();

                System.out.println("Before");
                Object invoke = method.invoke(targetObject, args);
                System.out.println("After");

                return invoke;
            });

 

 

인자 값 사용(소비) 하기

인자로 전달된 값을 사용하여 데이터를 처리하고 로직을 완료한다. 리턴 타입은 void이다.

String value = "val";
someMethod(value, (value) -> System.out.println(value));

 

 

불 값 리턴하기

인자로 전달된 값을 기반으로 불 값을 리턴한다. 주로 값의 유효성 검증 및 비교 작업을 담당한다.

String value = "val";
someMethod(value, (value) -> "val".equals(value));

 

 

두 객체 비교하기

각각의 타입으로 전달된 두 객체를 비교하여 결과 값을 리턴한다.

String value1 = "val";
String value2 = "val";

Boolean result = someMethod(value1, value2, (value1, value2) -> value1.equals(value2));

 

 

객체 생성하기

인자로 전달되는 것 없이 객체를 생성한다. 리턴 타입은 void이다.

Lob lob = someMethod(() -> new Lob());

 

 

객체를 변경하기

인자로 전달된 값을 변경해서 다른 객체로 리턴한다.

String value = "hello";
String subString = someMethod(value, (value) -> value.substring(0, 3))

 

 

값을 조합, 병합하기

인자로 전달된 값을 조합해서 새로운 값을 리턴한다.

String prefix = "hello ";
String suffix = "world";
String fullText = someMethod(prefix, suffix, (prefix, suffix) -> prefix + suffix);

 

 

 

함수형 인터페이스 ( java.util.function )

하나의 추상 메서드를 가지고 있는 인터페이스나 @FunctionaInterface Annotation이 작성된 인터페이스를 말한다.

 

@FunctionaInterface는 함수형 인터페이스 형식을 제약 사항으로 지정한다.

 

Function <T, R>

T라는 타입의 한 인자를 받아서 R 타입으로 반환하는 함수형 인터페이스이다.

 

받은 인자를 다른 값으로 변환해서 리턴할 때, 값을 변경하거나 매핑할 때 사용한다.

 

R apply (T value)

compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

public static Long parseLong(String value, Function<String, Long> function) {
    return function.apply(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(parseLong("100", Long::valueOf));
}

 

유사한 함수형 인터페이스

  • BiFunction <T, U, R>

    각각의 타입을 가지는 두 인자를 받아서 다른 타입으로 반환하는 함수형 인터페이스이다.

     

    R apply(T t, U u);

    compute(), merge(), replaceAll() 등의 메서드를 구현하는 데 사용된다.

     

    String, Integer  -> Long

      public static Long parseLong(String value1, Integer value2 , BiFunction<String, Integer, Long> function) {
          return function.apply(value1, value2);
      }
    
      ----------------------
    
      public static void main(String[] args) {
    
              System.out.println(parseLong("100", 100, (value1, value2) -> Long.parseLong(value1)+value2));
      }
  • DoubleFunction

    입력되는 인자가 double인 함수형 인터페이스이다.

    R apply(double value);

     

  • DoubleToIntFunction

    입력되는 인자가 double, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(double value);

     

  • DoubleToLongFunction

    입력되는 인자가 double, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(double value);

     

  • IntFunction

    입력되는 인자가 int인 함수형 인터페이스이다.

    R apply(int value);

     

  • IntToDoubleFunction

    입력되는 인자가 int, 반환 타입은 int인 함수형 인터페이스이다.

    double applyAsDouble(int value);

     

  • IntToLongFunction

    입력되는 인자가 int, 반환 타입은 long인 함수형 인터페이스이다.

    long applyAsLong(int value);

     

  • LongFunction

    입력되는 인자가 long인 함수형 인터페이스이다.

    R apply(long value);

     

  • LongToDoubleFunction

    입력되는 인자가 long, 반환 타입은 double인 함수형 인터페이스이다.

    double applyAsDouble(long value);

     

  • LongToIntFunction

    입력되는 인자가 long, 반환 타입은 int인 함수형 인터페이스이다.

    int applyAsInt(long value);

     

  • ToDoubleBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 double를 반환하는 함수형 인터페이스이다.

    double applyAsDouble(T t, U u);

     

  • ToIntBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToIntFunction

    T라는 타입의 한 인자를 받아서 int를 반환하는 함수형 인터페이스이다.

    int applyAsInt(T value);

     

  • ToLongBiFunction <T, U>

    각각의 타입을 가지는 두 인자를 받아서 long을 반환하는 함수형 인터페이스이다.

    int applyAsInt(T t, U u);

     

  • ToLongFunction

    T라는 타입의 한 인자를 받아서 long를 반환하는 함수형 인터페이스이다.

    long applyAsLong(T value);

     

Consumer

T라는 타입의 한 인자를 받아서 소모하고 아무것도 반환하지 않는 함수형 인터페이스이다.

 

인자를 전달하여 처리한 뒤 결과를 리턴 받을 필요가 없을 때 사용한다.

 

void Aceept(T t)

forEach() 등의 메서드를 구현하는 데 사용된다.

public static void printList(List<String> list, Consumer<String> consumer) {
    for (String item : list) {
        consumer.accept(item);
        }
}

----------------------

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        printList(list, System.out::println);
}

 

유사한 함수형 인터페이스

  • BiConsumer <T, U>

    입력되는 인자가 2개인 함수형 인터페이스이다.

    void accept(T t, U u);

     

  • DoubleConsumer

    기본형 타입인 double의 인자를 사용하는 함수형 인터페이스이다.

    void accept(double value);

     

  • IntConsumer

    기본형 타입인 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(int value);

     

  • LongConsumer

    기본형 타입인 long 인자를 사용하는 함수형 인터페이스이다.

    void accept(long value);

     

  • ObjDoubleConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 double 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, double value);

     

  • ObjIntConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 int 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, int value);

     

  • ObjLongConsumer

    입력되는 인자가 2개이며 1번째 인자로는 Object, 2번째로는 long 타입의 인자를 사용하는 함수형 인터페이스이다.

    void accept(T t, long value);

     

Supplier

T라는 타입의 한 인자를 반환하는 함수형 인터페이스이다.

 

여기서 T는 받는 인자가 아닌 반환 타입을 지정한다.

 

T get();

orElseThrow(), orElseGet(), requireNonNull() 등의 메서드를 구현하는 데 사용된다.

public static String executeValue(Supplier<String> supplier) {
    return supplier.get();
}

----------------------

public static void main(String[] args) {

        String val = "value";
    System.out.println(executeValue(() -> val) );
}

 

유사한 함수형 인터페이스

  • BooleanSupplier

    boolean 값을 반환하는 함수형 인터페이스이다.

    boolean getAsBoolean();

     

  • DoubleSupplier

    double 값을 반환하는 함수형 인터페이스이다.

    double getAsDouble();

     

  • IntSupplier

    int 값을 반환하는 함수형 인터페이스이다.

    int getAsInt();

     

  • LongSupplier

    long 값을 반환하는 함수형 인터페이스이다.

    long getAsLong();

 

Predicate

T라는 타입의 한 인자를 받아서 그에 대한 Boolean 값을 제공하는 함수형 인터페이스이다.

 

주로 데이터를 필터링하거나, 조건에 맞는지 여부를 확인하는 데 사용한다.

 

boolean test(T t);

removeIf(), filter() 등의 메서드를 구현하는 데 사용된다.

public static boolean check(String value, Predicate<String> predicate) {
    return predicate.test(value);
}

----------------------

public static void main(String[] args) {

        System.out.println(check("hello", (value) -> value.length() > 4) );
}

 

유사한 함수형 인터페이스

  • BiPredicate <T, U>

    각각의 타입을 가지는 두 인자를 받고 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(T t, U u);

     

  • DoublePredicate

    double 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(double value);

     

  • IntPredicate

    int 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(int value);

     

  • LongPredicate

    long 타입인 인자를 받아 boolean을 반환하는 함수형 인터페이스이다.

    boolean test(long value);

     

Operator

특정한 정수, 실수형 데이터를 처리하는 데 사용되는 함수형 인터페이스이다.

 

Operator 인터페이스

  • UnaryOperator extends Function <T, T>

    T라는 타입의 한 인자를 받아서 해당 타입으로 반환하는 함수형 인터페이스이다.

     

    내부적으로 Function을 상속받았다.

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

 

  • BinaryOperator extends BiFunction <T, T, T>

      public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
      }
    
      public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
          Objects.requireNonNull(comparator);
          return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
      }

 

  • DoubleBinaryOperator

    double 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    double applyAsDouble(double left, double right);

     

  • DoubleUnaryOperator

    인자와 반환 타입이 double인 메서드를 제공하는 함수형 인터페이스이다.

    double applyAsDouble(double operand);

     

  • IntBinaryOperator

    int 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    int applyAsInt(int left, int right);

     

  • IntUnaryOperator

    인자와 반환 타입이 int인 메서드를 제공하는 함수형 인터페이스이다.

    int applyAsInt(int operand);

     

  • LongBinaryOperator

    long 타입의 두 인자를 받고 해당 타입으로 값을 반환하는 함수형 인터페이스이다.

    long applyAsLong(long left, long right);

     

  • LongUnaryOperator

    인자와 반환 타입이 long인 메서드를 제공하는 함수형 인터페이스이다.

    long applyAsLong(long operand);

     

 

번외 : Runnable

해당 인터페이스는 JDK 1.0 부터 존재해왔던 오래된 인터페이스이지만, 자바의 함수형 인터페이스 제약을 지키고 있기에 추가하였다.

 

인자 타입과 반환 타입이 존재하지 않는 메서드를 제공하는 (논리적인) 함수형 인터페이스이다.

 

public abstract void run();

Runnable runnable = () -> System.out.println("class.run");
runnable.run();

 

 

번외 : Comparator<T>

해당 인터페이스는 JDK 1.2 부터 존재해왔던 인터페이스로 역시 자바의 함수형 인터페이스 제약을 지키고 있다. 

@FunctionalInterface도 타입 래벨에 정의되어 있음을 알 수 있다.

 

 

T라는 타입을 가지는 두 인자를 받아 int 값을 반환하는 메서드를 제공하는 함수형 인터페이스이다. 

객체, 값 객체 간의 우선 순위 즉 정렬을 위한 값을 얻어내는데 주로 사용된다.

 

int compare(T o1, T o2);

 Arrays.sort(split, (o1, o2) -> o2.compareTo(o1));

 

 

 

Variable Capture (Lambda Capturing)

Lambda의 body에서 인자로 넘어온 것 이외의 변수를 접근하는 것을 Variable Capture라고 한다.

 

 

Lambda는 인스턴스, 정적 변수final로 선언된 혹은 final처럼 사용되고 있는 지역 변수를 참조할 수 있다.

 

지역변수를 사용할 때에는 해당 변수에게 값의 재할당이 일어나서는 안된다.

// final int value = 100;
// 값을 통한 초기화 이후에 value의 값은 변경되어서는 안된다.
int value = 100;

// 순수 함수 형태가 아닌 사용 방식.
Lob lob = () -> new Lob(value);

--------------------
예제

@FunctionalInterface
interface Lob {
    public abstract void print();
}

//private final int a = 10; 도 동일하게 재정의가 가능하다.
private int a = 10;

public void hello() {

    final int b = 20;
    int c = 30;
    int d = 40;

    final Lob lobA = () -> System.out.println(a);
    lobA.print();

    // a 재정의
    a = 20;
    final Lob lobB = () -> System.out.println(a);
    lobB.print();

    final Lob lobC = () -> System.out.println(b);
    lobC.print();

    final Lob lobD = () -> System.out.println(c);
    lobD.print();

    final Lob lobE = () -> System.out.println(d);
    lobE.print();
}

public static void main(String[] args) {
        Lambda lambda = new Lambda();
        lambda.hello();
}

 

왜 지역 변수를 재정의해서 사용할 수 없는가?

이는 지역 변수가 스택 영역에 존재하기에 발생하는 문제점인데, 해당 변수를 초기화하는 스레드가 사라져 변수 할당이 해제된 경우에, Lambda를 실행하고 있는 별도의 스레드가 해당 변수 정보에 접근하려는 경우가 발생할 수 있다.

 

지역변수는 스레드 간에 공유가 되지 않는다.

 

자바에서는 람다를 실행하는 스레드의 스택에 지역 변수 할당 시 생성한 변수의 복사본을 저장하여 동작시키게 되는데, 이 값이 변경되었을 경우를 예측할 수 없기에 final 혹은 재할당 방지 제약조건을 걸어둔 것이다.

 

 

인스턴스 변수와 정적 변수는 왜 이런 제약조건을 걸지 않았는가?

이는 앞서 말했던 스레드 간의 가시성 문제의 연장선인데, 인스턴스 변수와 정적 변수는 모든 스레드에서 접근 가능한 값이기 때문에, 값의 변경이 이루어져도 직접적으로 접근할 수 있다.

 

 

 

메서드, 생성자 레퍼런스

메소드 참조?

JDK 8에 추가된 기능으로 함수를 메서드의 인자로 전달하는 것을 메서드 참조라고 한다.

 

해당 방식을 사용함으로써 해당 메서드 시그니처를 여러 곳에서 재사용할 수 있고, 기본적인 제공 메서드와 커스텀한 메서드 모두를 사용할 수 있다는 장점이 있다.

 

추가적으로 메서드 참조는 람다 표현식을 한번 더 축약적으로 표현할 수 있으며, 그를 통해 가독성을 향상할 수 있다.

 

람다 표현식을 대체하기보다는 상호 보완적인 관계를 형성한다.

// example

public static void printList(List<String> list, Consumer<String> consumer) {
        for (String item : list) {
            consumer.accept(item);
        }
}

public static void main(String[] args) {

        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");

        // 람다 표현식
        printList(list, (item) -> System.out.println(item));

        // 메서드 참조
        // 한 단계 더 축약된 것을 알 수 있다.
        printList(list, System.out::println);
}

 

메서드 참조를 사용하는 방법?

  • 정적 메서드의 참조 : ( Class::staticMethod )

    static으로 정의한 메서드를 참조할 때 사용하는 방식이다.

     

    Long.parseLong(value); → Long::parseLong

      // 람다 표현식
      someMethod(value, (value) -> Long.parseLong(value));
    
      // 메서드 참조
      someMethod(value, Long::parseLong);

 

  • 비한정적 메서드의 참조 (인스턴스) : ( Class::instanceMethod )

    public 혹은 protected로 정의된 메서드를 참조할 때 사용되는 방식이다.

     

    비한정적이라는 표현은 구문 자체가 특정한 객체를 참조하기 위한 변수를 지정하지 않는다는 것을 의미한다.

     

    string.length() → String::length

      // 람다 표현식
      someMethod(value, (value) -> value.length());
    
      // 메서드 참조
      someMethod(value, String::length);

 

  • 한정적 메서드 참조 (외부 인스턴스 변수) : ( Instance::instanceMethod )

    Lambda body 외부에서 선언된 객체의 메서드를 호출하거나, 객체를 생성해서 메서드 참조할 때 사용되는 방식이다.

     

    한정적이라는 표현은 참조하는 메서드가 특정 객체의 변수로 제한되는 것을 의미한다.

     

    lob.isLob() → lob::isLob

      Lob lob = new Lob(true);
    
      // 람다 표현식
      someMethod(() -> lob.isLob());
    
      // 메서드 참조
      someMethod(lob::isLob);

 

 

생성자 참조?

자바 언어에서는 메서드와 생성자를 구분하고 있다.

 

문법적인 구조상의 차이로는 리턴 타입이 없다는 것이 있지만, 메서드는 접근 권한만 있다면 호출 가능하나, 생성자는 객체가 생성될 때에만 호출할 수 있다.

 

생성자 참조는 ClassName::new 형식으로 작성된다.

 

생성자 참조는 새로운 객체를 생성하고 리턴하는 경우 등에서 사용된다.

// 람다 표현식
someMethod(String name -> new Lob(name)).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자 참조
someMethod(Lob::new).forEach((Lob lob) -> System.out.println(lob.name))

// 생성자, 메서드 참조
someMethod(Lob::new).forEach(System.out::println);

 

 

 

참고 자료

  • Practical 모던 자바

 

 

 

'Live Study' 카테고리의 다른 글

Live Study_Week 10. Multithreading programming  (3) 2021.03.02
Live Study_Week 13. I/O  (0) 2021.02.20
Live Study_Week 12. Annotation  (0) 2021.02.02
Live Study_Week 11. Enum  (0) 2021.01.28
Live Study_Week 09. 예외 처리  (0) 2021.01.11
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함