스터디/LiveStudy

15주차 과제: 람다식

TheWing 2021. 3. 1. 00:21

15주차 과제: 람다식

람다 표현식(Lambda Expressions)

람다식이란?

식별자 없이 실행가능한 함수

  • 메소드를 하나의 식으로 표현하는 것이라고 볼 수 있다.
  • 람다식으로 표현하면 return 이 없어지므로 람다식을 anonymous function(익명 함수) 이라고도 한다.

람다식의 장단점

장점

  • 코드를 간결하게 만들 수 있다
  • 가독성이 향상된다
  • 멀티쓰레드환경에서 용이하다
  • 함수를 만드는 과정 없이 한번에 처리하기에 생산성이 높아진다

단점

  • 람다로 인한 무명함수는 재사용이 불가능하다
  • 디버깅이 많이 까다롭다
  • 람다를 무분별하게 사용하면 코드가 클린하지 못하다
  • 재귀로 만들 경우 부적합하다

람다식 사용법

(매개변수) -> 표현 바디
(매개변수) -> { 표현 바디 }
() -> { 표현 바디 }
() -> 표현 바디

...생략...

기본 예제

LeagueOfLegend , Setting

@FunctionalInterface
interface Setting {
    void setup();
}

public class LeagueOfLegend {
    public void running(Setting setting) {
        setting.setup();
        System.out.println("LeagueOfLegend running");
    }
}

lambdaSample

public class lambdaSample {
    public static void main(String[] args) {
        LeagueOfLegend leagueOfLegend = new LeagueOfLegend();
        leagueOfLegend.running(new Setting() {
            @Override
            public void setup() {
                System.out.println("leagueOfLegend is setup");
            }
        });
    }
}

결과

leagueOfLegend is setup
LeagueOfLegend running

람다 예제

lambdaSample

public class lambdaSample {
    public static void main(String[] args) {
        LeagueOfLegend leagueOfLegend = new LeagueOfLegend();
        leagueOfLegend.running(() -> System.out.println("leagueOfLegend is setup"));
    }
}

결과

leagueOfLegend is setup
LeagueOfLegend running

@FunctionalInterface

  • Java 8이전에서는 자바에서 값이나 객체가 아닌 하나의 함수를 변수에 담아두는 것은 허용되지 않았다.
  • Java8에서 람다식이 추가되고 하나의 변수에 하나의 함수를 매핑할 수 있다.
  • 이해하기 쉽게 새로운 예제를 살펴보자

예제

@FunctionalInterface
public interface Functional {
    public int calc(int a, int b);
}
  • 1개의 메소드를 가진 것이 Functional interface 이다 Single Abstract Method(SAM)이라고도 불리기도한다
  • @FunctionalInterface 를 지정하게되면 이 인터페이스가 함수형 인터페이스라고 명시를 해주고 컴파일러가 SAM 여부를 체크할 수 있도록한다

사용 예제2

Various 사용

Functional add = (int a, int b) -> {return a + b; };
Functional add1 = (int a, int b) -> a + b;
Functional add2 = Integer::sum;
  • 이 예제는 모두 결과는 같다.

그럼 이제 함수를 사용해보자

int result = add.calc(1, 1) + add1.calc(2, 2) + add2.calc(3, 3);

결과

12

정상적으로 동작한다

바이트코드

  • 기본 예제와 람다예제로 작성하였을시 결과는 동일한데 어떻게 실행되고 바이트코드가 동일한지 궁금해서 찾아보았다.

기본 예제 바이트코드

  • 기본 예제에서 익명클래스인 lambdaSample$1 새로운 클래스를 생성하여 초기화를 해주고 Setting 인터페이스를 실행하는것 같다

  • 익명클래스는 INVOKESPECIAL 이란 OPCODE로 생성자를 호출하고, INVOKEVIRTUAL 로 Setting을 호출한다.

익명 클래스 & Function Type

  • 자바에서는 왜 람다를 내부적으로 익명클래스로 컴파일하지 않을까?
  • JAVA 8 이전 버전에서 람다를 쓰기위한 retrolambda 같은 라이브러리나, kotlin 같은 언어에서는 컴파일 시점에 람다를 단순히 익명클래스로 치환이 된다.
  • 다만, 익명클래스로 사용할 경우 아래와 같은 문제가 발생할 수 있다.
    • 항상 새 인스턴스로 할당한다
    • 람다식 마다 클래스가 하나씩 생기게된다

람다 예제 바이트코드

  • 람다 예제에서 좀 신기한점이 있었다. 새로운 메서드를 static으로 생성해서 메서드를 실행시키는것 같다
  • 중간쯤에 INVOKEDYNAMIC 부분을 잘 몰라서 한번 찾아봤다.

INVOKEDYNAMIC CALL

  • indy가 호출되게되면 bootstrap영역의 lambdafactory.metafactory() 를 수행하게된다
    • lambdafactory.metafactory(): java runtime library의 표준화 method
    • 어떤 방법으로 객체를 생성할지 dynamically 를 결정한다
      • 클래스를 새로 생성, 재사용, 프록시, 래퍼클래스 등등 성능 향상을 위한 최적화된 방법을 사용하게된다
  • java.lang.invoke.CallSite 객체를 return한다
    • LambdaMetafactory ~ 이렇게 되어있는 곳의 끝에 CallSite객체를 리턴하게된다.
    • 해당 lambda의 lambda factory, MethodHandle을 멤버변수로 가지게된다.
    • 람다가 변환되는 함수 인터페이스의 인스턴스를 반환한다.
    • 한번만 생성되고 재 호출시 재사용이 가능하다

찾아보니 람다 동작이 매우 신기하다..

함수형 인터페이스

함수형 인터페이스(Functional Interface)란?

  • 위에서 한번 언급했듯이 함수형 인터페이스는 1개의 추상 메소드를 가지고 있는 인터페이스이다.

함수형 인터페이스를 왜 사용하지?

  • 자바의 람다식은 함수형 인터페이스로만 접근이 가능하기 때문이다.
  • 위에 예제를 인용하면
public interface Functional {
    public int calc(int a, int b);
}

Functional func = (a, b) -> { return  a + b;};
System.out.println("func.calc(1,2) = " + func.calc(1,2));
// 결과
// func.calc(1,2) = 3

이렇게 사용이 가능하다

기본 함수형 인터페이스

  • Runnable
  • Supplier
  • Consumer
  • Function<T, R>
  • Predicate
  • 등등

추가적으로 찾아보려면 docs 를 참고해보자

Runnable

  • 인자를 받지 않고 리턴값도 없는 인터페이스다 우리가 앞전 쓰레드를 공부할 때 Runnable 인터페이스로 실행 했던 것이라고 보면된다.

예제

Runnable runnable = () -> System.out.println("runnable run");
runnable.run();
//결과
// runnable run
  • Runnable은 run()을 호출해야한다. 함수형 인터페이스마다 run() 과 같은 실행 메소드 이름이 다르다. 인터페이스 종류마다 만들어진 목적이 다르고, 인터페이스 별 목적에 맞는 실행 메소드 이름을 정하기 때문이다.

Suppliers

  • Supplier<T> 은 인자를 받지 않고 T 타입의 객체를 리턴한다
public interface Supplier<T> {
    T get();
}

예제

public class SupplierSample {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "Supplier Sample";
        String getSupplier = supplier.get();
        System.out.println("getSupplier = " + getSupplier);
    }
}
// 결과
// getSupplier = Supplier

Consumer

  • Cunsumer<T> 는 T타입의 객체를 인자로 받고 리턴 값은 없다
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); };
    }
}

예제

public class ConsumerSample {
    public static void main(String[] args) {
        Consumer<String> print = str -> System.out.println("this is " + str + " Interface");
        print.accept("Consumer");
    }
}
// 결과
// this is Consumer Interface

andThen() 을 사용하면 두개 이상의 Consumer 를 사용할 수 있다

public class ConsumerSample {
    public static void main(String[] args) {
        Consumer<String> print = str -> System.out.println("this is " + str + " Interface");
        Consumer<String> print1 = str -> System.out.println("ok");
        print.andThen(print1).accept("Consumer");
    }
}
// 결과
// this is Consumer Interface
// ok

Function

Function<T, R> 는 T 타입의 인자를 받아, R 타입의 객체로 리턴한다

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;
    }
}

예제1

public class FunctionSample {
    public static void main(String[] args) {
        Function<Integer, Integer> add = (value) -> value + value;
        Integer result = add.apply(5);
        System.out.println("result = " + result);
    }
}
// 결과
// result = 10

예제2

public class FunctionSample2 {
    public static void main(String[] args) {
        Function<Integer, Integer> add = (value) -> value + 2;
        Function<Integer, Integer> sub = (value) -> value - 2;

        Function<Integer, Integer> addAndSub = add.compose(sub);
        Integer result = addAndSub.apply(10);
        System.out.println("result = " + result);
    }
}
// 결과
// result = 10

Predicate

  • Predicate<T> 는 T타입 인자를 받고 결과로 boolean으로 리턴한다
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);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

예제

public class PredicateSample {

    public static void main(String[] args) {
        Predicate<Integer> isSmallerThan = num -> num < 10;
        System.out.println("5는 10보다 작은지? -> "+ isSmallerThan.test(5));
    }
}
// 결과
// 5는 10보다 작은지? -> true

예제2

and()or() 는 다른 Predicate와 함께 사용이 가능하다.

public class PredicateSample2 {
    public static void main(String[] args) {
        Predicate<Integer> isBiggerThan = num -> num > 20;
        Predicate<Integer> isSmallerThan = num -> num < 10;
        System.out.println(isBiggerThan.and(isSmallerThan).test(15));
        System.out.println(isBiggerThan.or(isSmallerThan).test(5));
    }
}

// 결과
// false
// true

예제3

isEqual() 는 static 메소드이고 인자로 전달되는 객체와 같은지 체크하여 객체를 만들어준다

public class PredicateSample3 {

    public static void main(String[] args) {
        Predicate<String> isEqual = Predicate.isEqual("TheWing");
        System.out.println("isEqual.test(\"TheWing\") = " + isEqual.test("TheWing"));
    }

}
// 결과
// isEqual.test("TheWing") = true

요약정리

 

Variable Capture

  • 람다식은 특정 상황에서 람다 함수 본문 외부에 선언된 변수에 접근이 가능하다.
  • Java8 버전 이전에서는 익명의 내부 클래스가 이를 둘러싼 메서드에 대한 로컬 변수를 캡처할 때 이 문제가 발생했다. 컴파일러가 만족할 수 있도록 로컬 변수 앞에 final 키워드를 추가해야만 했었다.
  • 우리가 변수를 final 로 선언하면 컴파일러가 변수가 사실상 final로 인식할 수 있다.

Local variable Capture

  • Local variable 은 조건이 final 또는 effectively final 이여야한다
  • effectively final은 무엇일까?
    • Java 8에 추가된 syntactic sugar 의 일종으로, 초기화된 이후 값이 한번도 변경되지 않았다는 것
    • effectively final 변수는 final 키워드가 붙어있지 않지만 fianl 키워드를 붙인 것과 동일하게 컴파일에서 처리한다.
    • 결론적으로 초기화하고 값이 변경되지 않은 것을 의미한다.
  • 왜 이런 제약조건이 있는걸까?
    • JVM 메모리 구조를 되짚어보자.
      • 지역 변수는 쓰레드끼리 공유가 안 된다.
      • JVM에서 인스턴스 변수는 힙 영역에 생성된다.
      • 인스턴스 변수는 쓰레드끼리 공유가 가능하다
  • 결론적으로
    • 지역 변수가 스택에 저장되기 때문에 람다식에서 값을 바로 참조하는 것에 제약이 있어 복사된 값을 사용하는데 이때 멀티 쓰레드 환경에서 변경이 되면 동시성에 대한 이슈를 대응하기가 힘들다

예제

아래는 컴파일 되지 않는다

Supplier<Integer> incrementer(int start) {
  return () -> start++;
}

start 는 지역 변수이고 람다식 내에서 수정하려고 한다.

이것이 컴파일 되지 않는 이유는 람다가 start의 값을 캡쳐하기 때문이다. 즉 복사본을 만든다. 변수를 final 값으로 지정하면 람다 내에서 start를 incrementer로 증가 시키면 실제로 메소드내의 start 인자가 수정될 수 있다.

왜 복사되는데?

  • 우리가 메소드에서 람다를 반환하고 있다는 것을 확인할 수 있다. 따라서 람다는 start 메소드 매개변수가 가비지 컬렉터에 수집될 때까지 람다는 실행되지 않는다. 자바는 람다가 이 메서드를 벗어나기 위해서 복사본을 만들어야한다

메소드, 생성자 레퍼런스

Method Reference

  • 메소드를 간결하게 지칭할 수 있는 방법으로 람다가 쓰이는 곳에서 사용이 가능하다
  • 일반 함수를 람다 형태로 사용할 수 있도록 해준다.

예제

interface MethodReferenceInterface {
    void multiply(int value);
}

public class MethodReferenceSample {
    public static void main(String[] args) {
        MethodReferenceInterface methodReferenceInterface = MethodReferenceSample::multiplyPrint;
        methodReferenceInterface.multiply(13);
    }

    public static void multiplyPrint(int value) {
        System.out.println(value * 2);
    }
}
// 결과
// 26

메소드 참조 방법

  • Default Use
  • Constructor Reference
  • Static Method Reference
  • Instance Method Reference

Default Use 예제

@FunctionalInterface
interface ConverterInterface {
    String convert(Integer number);
}

public class MethodReferenceSample {
    public static void main(String[] args) {

        convert(100, (number) -> String.valueOf(number));

        convert(100, String::valueOf);
    }

    public static String convert(Integer number, ConverterInterface converterInterface) {
        return converterInterface.convert(number);
    }

}

Constructor Reference 예제

  • 실제로 생성자를 호출해서 인스턴스를 생성하는것이 아닌 생성자 메소드를 참조
클래스이름::new

String::new
() -> new String

Static Method Reference 예제

  • 메소드 참조는 static method를 직접적으로 가리킬 수 있다.
클래스이름::메소드이름
(매개변수) -> Class.staticMethod(매개변수)

String::valueOf
str -> String.valueOf(str)

Instance Method Reference 예제

  • 특정 인스턴스의 메소드를 참조할 수 있다. 클래스 이름이 아닌 인스턴스 명을 넣어야한다.
(매개변수) -> obj.instanceMethod(매개변수)
obj::instanceMethod

object::toString
() -> object.toString()

정리

// 생성자 참조
String::new // ClassName::new
() -> new String()

// static 메소드 참조
String::valueOf // ClassName::staticMethodName
(str) -> String.valueOf(str)

// Instance 메소드 참조 클로저
x::toString // instanceName::instanceMethodName
() -> "TheWing".toString()

// Instance 메소드 참조 람다
String::toString // ClassName::instanceMethodName
(str) -> str.toString()

Reference

'스터디 > LiveStudy' 카테고리의 다른 글

Java online LiveStudy 후기  (0) 2021.03.01
14주차 과제: 제네릭  (0) 2021.02.21
13주차 과제 : I/O  (0) 2021.02.08
12주차 과제 : 애노테이션  (0) 2021.01.31
11주차 과제: Enum  (0) 2021.01.25