본문 바로가기

WhiteShip Java Study : 자바 처음부터 멀리까지

람다식(feat. 익명 구현 클래스 vs 람다식)

선장님과 함께하는 마지막 자바 스터디입니다. (ㅜ)

자바 스터디 Github

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

나의 Github

github.com/cmg1411/whiteShip_live_study

 

cmg1411/whiteShip_live_study

✍ 자바 스터디할래. Contribute to cmg1411/whiteShip_live_study development by creating an account on GitHub.

github.com

  • 람다식 사용법
  • 함수형 인터페이스
  • Variable Capture
  • 메소드, 생성자 레퍼런스

 

 

 

 

 

 

함수형 프로그래밍

람다식 글인데 갑자기 함수형 프로그래밍부터 시작한다. 뜬금없다고 느낄수도 있겠지만, 이걸 알고 람다식을 보는 것과 모르고 보는 것은 정말로 다르다.

 

함수형 프로그래밍은 프로그래밍의 패러다임이다. 마치 절차지향 프로그래밍, 객체지향 프로그래밍처럼.

함수형 프로그래밍은 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나이다.
- 위키백과

 

함수형 프로그래밍은 선언적 프로그래밍이다. 이와 대조적으로 람다를 지원하기 전의 자바는 완전한 명령형 프로그래밍이었다.

  • 명령형 프로그래밍 : 클래스에서 메서드를 정의하고, 필요할 때 그 메서드를 호출하는 명령하여 동작.
  • 선언적 프로그래밍 : 데이터가 입력으로 주어지고, 데이터가 처리되는 과정(흐름)을 정의하는 것으로 동작.

함수형 프로그래밍을 간단히 알아보자.

함수형 프로그래밍의 조건에는 다음과 같은 것이 있다.

  • 순수 함수.
  • 고차 함수.
  • 익명 함수.

함수형 프로그래밍의 특징에는 다음과 같은 것이 있다.

  • 불변성
  • 참조 투명성
  • 일급 함수 (일급 객체)
  • 게으른 평가

 

함수형 프로그래밍의 조건

1. 순수 함수

같은 입력 시 같은 출력을 보장한다. 부수 효과(Side Effect)가 없다.

멀티쓰레드에서도 안전하다.

 

Side effect는 반환 값 이외에, 호출 된 함수 밖에서 관찰할 수 있는 어플리케이션의 상태 변경이다.

 

2. 고차 함수

일급 함수의 특징을 만족해야 한다.

  • 함수의 인자로 함수를 전달할 수 있다.
  • 함수의 리턴값으로 함수를 사용할 수 있다

3. 익명 함수

이름이 없는 함수이다. 자바에서 람다식을 말한다.

 

4. 합성 함수

새로운 함수를 생성하거나 어떤 계산을 수행하기 위해 둘 이상의 함수를 결합하는 것이다.

자바에서는 메서드 체이닝을 통해 구현된다.

 

함수형 프로그래밍의 특징

1. 불변성

상태를 변경하지 않는 것.

상태를 변경하게 되면, 부수 효과가 생기게 되어 순수함수의 조건을 만족하지 못한다.

순수함수를 사용하는 함수형 프로그래밍은 불변성을 가진다.

 

2. 참조 투명성

프로그램의 변경 없이도 어떤 표현식을 값으로 대체할 수 있다.

 

3. 일급 함수

일급 함수는 다음과 같다.

  • 함수를 함수의 매개변수로 넘길 수 있다.
  • 함수를 함수의 반환값으로 돌려줄 수 있다.
  • 함수를 변수나 자료구조에 담을 수 있다.

일급 함수를 포함하는 일급 시민 (First-class Citizen)

  • 대상을 함수의 매개변수로 넘길 수 있다.
  • 대상을 함수의 반환값으로 돌려줄 수 있다.
  • 대상을 변수나 자료구조에 담을 수 있다.

 

추가적으로 Java 8 이후에는 객체가 위의 조건들을 만족하는 일급 객체를 사용한다. 라고 할 수 있다.

 

 

 

4. 게으른 평가

함수형 언어가 아닌 언어의 코드는 실행 즉시 값을 평가한다.

하지만 함수형 언어에서는 값이 필요한 시점에 평가한다. 

참고로, 람다식을 사용한 스트림에서, 종단연산이 있지 않으면 없는 연산과 같은 이유가 된다.

 

 

 

 

람다식

람다식은 메서드를 하나의 식으로 표현하는 선언적 프로그래밍의 방법이다.

원래의 자바는 익명 클래스를 이용하여 익명 구현 객체를 사용할 수 있었다.

 

이를 함수형 프로그래밍을 도입하면서 간단하게 표현할 수 있는 방법이 람다식이다.

(람다와 익명 내부 클래스가 같다는 것이 아니다.)

 

 

특징

  • 익명
    • 보통의 메서드와 달리 이름이 없다.
  • 함수
    • 보통의 메서드와 달리 메서드가 아닌 함수이다.
    • 메서드는 클래스에 종속적인 것을 메서드라 하지만, 함수는 어느곳에 종속적이지 않다.
  • 일급 시민
    • 매개변수의 인자가 될 수 있고, 반환값이 될 수 있고, 자료구조에 담길 수 있다.

 

람다식 사용법

기본                    : (int num) -> {System.out.println(num);} 

단일 실행문은 중괄호 제거    : (int num) -> System.out.println(num);

단일 인자는 타입 생략       : (num) -> System.out.println(num);

단일 인자는 소괄호 제거      : num -> System.out.println(num);

인자가 없으면 소괄호 필수     : () -> System.out.println("매개변수 없음"); 

인자가 여러개면 소괄호 필수   : (x, y) -> System.out.println(x, y);

인자가 없고 반환값이 있으면   : () -> {return value;};

실행코드가 return문 뿐이면 return 키워드 생략 가능 : () -> value;

매개변수, 리턴타입 둘다 있으면 : (x, y) -> x+y;

 

  • 위에서 볼 수 있듯, 람다는 좌항, 우항, -> 로 이루어져있다.
  • 좌항을 파라미터로, 우항의 람다 몸체를 실행한다.

 

 

사용예제

public static void main(String[] args) {
        Comparator<Integer> comp = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1.compareTo(o2); // o1 은 Integer 이고, Integer 은 Comparable 을 구현하고,
            }				 // Comparable 에 compareTo 메서드가 있다.
        };

        System.out.println(comp.compare(2, 1));
    }

 

먼저 익명 클래스를 보자. 람다가 있기 전에는 이렇게 사용할 수 있었다.

Comparator 은 인터페이스이며, 그 구현체를 익명 클래스 구현을 통해 만들고 메서드를 사용한다.

 

하지만 람다를 사용하면 아래와 같이 쓸 수 있다.

(요즘 IDE 는 너무 똑똑해서, 저렇게만 써도 람다로 바꾸라고 난리를 친다..)

public static void main(String[] args) {
    Comparator<Integer> comp = (o1, o2) -> (o1.compareTo(o2));

    System.out.println(comp.compare(2, 1));
}

 

  1. Comparator 에는 메서드가 하나이기 때문에 메서드명은 추론할 수 있다.
  2. 유일한 메서드의 매개변수는 유일하므로 매개변수들의 타입도 추론할 수 있다.

이렇기 때문에 코드는 극적으로 짧아진다. 가독성 또한 좋아졌다.

 

 

 

함수형 인터페이스

함수형 프로그래밍과 람다를 알아보았다.

함수형 프로그래밍의 람다는 일급 함수로 매개변수, 리턴값, 자료구조의 일부가 될 수 있다 하였다.

 

그럼 이 때, 람다의 타입은 무엇일까 ?

다시말해, 람다라는 함수객체는 어떤 타입의 구현체일까 ?

람다가 있어야 할 곳이 어딘가 ?

 

이것에 대한 대답이 함수형 인터페이스이다.

 

 

함수형 인터페이스는, 제네릭 <T> 를 타입 파라미터로 가진다.

인터페이스에 정의된 하나의 추상 메서드는, 이후 함수형 인터페이스의 구현체인 람다함수를 실행시킬 메서드이다.

 

 

 

 

함수형 인터페이스의 특징

  • 추상 메서드가 단 하나여야 한다.
    • 추상 메서드가 하나여야, 람다식에서 정보를 추론할 수 있다.
    • 추상 메서드는 하나지만, 도움이되는 static 메서드나, default 메서드들은 가질 수 있다.
  • @FunctionalInterface 로 검증할 수 있다.
    • 에너테이션 챕터에서 살펴보았었따.

 

 

 

 

함수형 인터페이스의 종류

java.util.function 에는 자바의 빌트인 함수형 인터페이스가 40여개가 있다.

우리는 앵간하면 제공하는 함수형 인터페이스를 이용하여 프로그래밍할 수 있으며, 필요한 경우 구현해서 쓸 수 있다.

 

인터페이스명 추상 메소드 설명
Runnable void run() 기본적인 인터페이스, 매개변수와 반환값 없음
Supplier<T> T get() 매개변수 없음, 제네릭타입 반환값 가짐
Consumer<T> void accept() 제네릭 매개변수 하나, 반환값 없음(void)
Predicate<T> boolean test() 제네릭 매개변수 하나, Boolean 반환값 하나
Function<T,R> R apply(T t) 제네릭 매개변수 하나와 다른 제네릭 반환값하나
Comparator<T> int compare(T o1, T o2) 같은 제네릭 타입 매개변수 두개를 받고, Integer 반환값 하나 가짐, 객체간 비교를 위핸 compare를 위한 인터페이스
BiConsumer<T,U> void accept(T t, U u) 서로다른 제네릭 매개변수 두개를 받고 반환값 없음
BiFunction<T,U,R> R apply(T t, U u) 서로 다른 제네릭 매개변수 두개를 받고 다른 제네릭 타입의 반환값 하나
BiPridicate<T,U> boolean test(T t, U u) 서로 다른 제네릭 타입의 매개변수 2개를 받고
Boolean 타입의 반환값 하나 가짐



사실 인터페이스의 추상메서드가 하나이고, 반환타입과 파라미터만 맞다면, 어떤 함수형 인터페이스를 쓰든 람다식을 수용할 수 있다. 이와 같은 설계는, 만약 자바에서 새로운 라이브러리와 기능을 제공했을때, 기존 기능을 제약없이 사용가능한 상위호환성을 제공한다.
java.util.function 에는 각 Supplier, Consumer, Predicate, Function 기본 함수형 인터페이스에 대해 파생적인 인터페이스들로 구성되어 있다.

예를 들면, IntConsumer 는 원시타입인 int 를 받도록 되어 있는데, 이를 사용하면 불필요한 박싱으로 인한 객체생성을 하지 않아서 훨씬 효율적이다.

 

 

 

 

 

 

 

 

 

함수형 인터페이스들의 디폴트, 정적 메서드

default 메서드, static 메서드는 추상메서드가 아니므로 추상메서드가 하나여야 한다는 함수형 인터페이스의 제약과는 상관없이 만들 수 있다. 

한마디로 default, static 메서드가 있어도 추상메서드만 하나면 함수형 인터페이스란 것이다.

 

 

1. Consumer, Function, Operator 종류 인터페이스의 디폴트메서드

 

  • andThen()
인터페이스AB = 인터페이스A.andThen(인터페이스B);
최종결과 = 인터페이스AB.method();

인터페이스 A 를 실행후, 반환값을 인터페이스 B의 매개값으로 준다.

Consumer 계열, Operator 계열에 있다.

Consumer 계열은 반환값이 없기 때문에, 단순히 순서를 정해주는 역할을 하게 된다.

 

 

  • compose()
인터페이스AB = 인터페이스A.compose(인터페이스B);
최종결과 = 인터페이스AB.method();

위와 반대다. 인터페이스 B 를 먼저 실행 후 반환값을 인터페이스 A의 매개값으로 준다.

Function, Operator 를 뺀 Operator 계열에 있다.

 

 

 

 

 

 

 

 

 

 

2. Predicate 계열의 default 메서드

 

  • and()
predicateAB = predicateA.and(predicateB);
boolean result = predicateAB.method();

&& 와 대응된다.

결과값 둘을 and 연산한 결과를 반환하는 인터페이스를 만든다.

 

 

  • or()
predicateAB = predicateA.or(predicateB);
boolean result = predicateAB.method();

|| 와 대응된다.

결과값 둘을 or 연산한 결과를 반환하는 인터페이스를 만든다.

 

 

  • negate()
predicateAB = predicateA.negate(predicateB);
boolean result = predicateAB.method();

! 와 대응된다.

결과값 둘을 negate 연산한 결과를 반환하는 인터페이스를 만든다.

 

 

 

 

 

 

 

 

 

 

 

2. Predicate 계열의 static 메서드

 

 

  • Predicate.isEqual()
Predicate<Object> predicate = Predicate.isEqual(targetObject);
boolean result = predicate.test(sourceObject);

두 객체의 동일성을 검사한다.

위 코드는 Objects.equals(targetObject, sourceObject); 와 같은 역할이다.

 

 

 

 

3. BinaryOperator 의 static 메서드

 

  • minBy() : Comparator 객체를 받아 더 작은 객체를 반환한다.
  • maxBy() : Comparator 객체를 받아 더 큰 객체를 반환한다.

 

 

 

 

 

 

 

 

 

 

함수형 인터페이스와 람다의 활용

많은 예가 있겠지만, 하나의 예를 들자면.

enum 형식에는 abstract 메서드를 통해 상수마다 메서드 몸체를 정의할 수 있었다.

 

abstract 메서드가 아닌, 함수형 인터페이스를 필드로 가지게 하고, 필드에 람다식을 정의하는 방법으로 같은 기능을 구현할 수 있다.

그리고 이 람다식을 실행시키는 메서드를 하나 만들어주면 된다.

 

alkhwa-113.tistory.com/entry/Enum-%ED%99%9C%EC%9A%A9%EB%9E%8C%EB%8B%A4%EC%8B%9D-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

Enum 활용(람다식 사용하기)

3주차 미션이 끝나고 마지막 최종 코딩 테스트를 기다리고 있다. 3주차 미션은 너무 어려웠어서, 코드 리펙토링이 충분히 진행되지 못했다. 그래서 조금 더 코드를 줄이는 방법을 고민한 결과 Enu

alkhwa-113.tistory.com

 

 

 

다른 간단한 예시로는 ,

public class FunctionalInterface {

    public static void main(String[] args) {
        BiConsumer<String, String> con = (s1, s2) -> System.out.println(s1 + s2);
        con.accept("Hi", "Java");
    }
}

의 출력값은 HiJava 이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

익명 구현 클래스 vs 람다식

익명 구현 클래스가 하던 일을 람다식을 이용하면 간단하게 코드가 바뀔 수 있을 알았다.

하지만 이 둘은 분명하게 다르다고 했는데, 어떤 점이 다른지 공부해보자.

 

간단한 연산을 정의한 IntBinaryOperator 인터페이스를 이용하였다.

이를 이용하여 더하기를 만들어 봤다.

따라나오는 바이트코드는 차이점을 보기 위해 적당히 삭제할건 삭제했다.

 

1. 익명 내부 클래스를 이용한 구현

public class AnonymousClass {

    public static void main(String[] args) {
        IntBinaryOperator plus = new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        };
    }
}

 

위 코드의 바이트코드.

public class com/whiteship/white_ship_study/week15/AnonymousClass {

  static INNERCLASS com/whiteship/white_ship_study/week15/AnonymousClass$1 null null

  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 8 L0
    NEW com/whiteship/white_ship_study/week15/AnonymousClass$1
    DUP
    INVOKESPECIAL com/whiteship/white_ship_study/week15/AnonymousClass$1.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 14 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

 

  1. 먼저 첫줄이다. 익명 클래스는 static 중첩 클래스로 새로운 AnonymousClass$1 를 생성하고 있다.
  2. INVOKESPECIAL 을 통해 AnonymousClass$1 의 클래스의 객체를 생성한다.

 

 

2. 람다식을 이용한 구현

public class LambdaEx {

    private IntBinaryOperator plus() {
        IntBinaryOperator plus = (x, y) -> {
            return x + y;
        };
        return plus;
    }
}

 

위 코드의 바이트코드.

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week15/NotAnonymous {

  // compiled from: NotAnonymous.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x2
  private plus()Ljava/util/function/IntBinaryOperator;
   L0
    LINENUMBER 12 L0
    INVOKEDYNAMIC applyAsInt()Ljava/util/function/IntBinaryOperator; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (II)I, 
      // handle kind 0x6 : INVOKESTATIC
      com/whiteship/white_ship_study/week15/NotAnonymous.lambda$plus$0(II)I, 
      (II)I
    ]
    ASTORE 1
   L1
    LINENUMBER 15 L1
    ALOAD 1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

  // access flags 0x100A
  private static synthetic lambda$plus$0(II)I
    // parameter synthetic  x
    // parameter synthetic  y
   L0
    LINENUMBER 13 L0
    ILOAD 0
    ILOAD 1
    IADD
    IRETURN
   L1
    LOCALVARIABLE x I L0 L1 0
    LOCALVARIABLE y I L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

                                                                                                                                                                          

  1. 이 클래스인 NotAnonymous 라는 클래스를 생성하는 코드는 없다. 
    1. 람다 구현은 따로 클래스를 생성하지 않는다.
    2. 대신 아래와 같이 새로운 메서드를 만든다.
  2. INVOKEDYNAMIC 키워드를 사용하며, LambdaMetafactory의 metafacotry 메서드에 MethodHandles$Lookup, MethodType, MethodHandle 등을 인자로 넣어 뭔가를 처리하고, Callsite 라는 것도 있다.
    1. 그 아래의 lambda$plus$0 을 잘 보자.
    2. 이 메서드는 아래에 private static 으로 자동 생성되었다.
  3. lambda$plus$0 prvate static 메서드
    1. 이 메서드의 ILOAD, IADD 를 통해 우리가 작성한 x + y 가 들어있음을 알 수 있다.

 

 

 

2. 람다식을 이용한 구현 + 외부 참조

public class LambdaEx {

    private IntBinaryOperator plus() {
        IntBinaryOperator plus = (x, y) -> {
            System.out.println(this);
            return x + y;
        };
        return plus;
    }
}

 

위 코드의 바이트코드.

// class version 52.0 (52)
// access flags 0x21
public class com/whiteship/white_ship_study/week15/NotAnonymous {

  // compiled from: NotAnonymous.java
  // access flags 0x19
  public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup

  // access flags 0x2
  private plus()Ljava/util/function/IntBinaryOperator;
   L0
    LINENUMBER 12 L0
    ALOAD 0
    INVOKEDYNAMIC applyAsInt(Lcom/whiteship/white_ship_study/week15/NotAnonymous;)Ljava/util/function/IntBinaryOperator; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (II)I, 
      // handle kind 0x7 : INVOKESPECIAL
      com/whiteship/white_ship_study/week15/NotAnonymous.lambda$plus$0(II)I, 
      (II)I
    ]
    ASTORE 1
   L1
    LINENUMBER 16 L1
    ALOAD 1
    ARETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE plus Ljava/util/function/IntBinaryOperator; L1 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

  // access flags 0x1002
  private synthetic lambda$plus$0(II)I
    // parameter synthetic  x
    // parameter synthetic  y
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L1
    LINENUMBER 14 L1
    ILOAD 1
    ILOAD 2
    IADD
    IRETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week15/NotAnonymous; L0 L2 0
    LOCALVARIABLE x I L0 L2 1
    LOCALVARIABLE y I L0 L2 2
    MAXSTACK = 2
    MAXLOCALS = 3
}

 

  1. 위와 다른 점은, this 라는 객체를 사용한다.
    1. 람다식은 위에서 본 것과 같이, 클래스를 따로 생성하는 것이 아니라 메서드를 생성한다.
    2. 따라서 람다식은 새로운 메서드로 볼 수 있으며, 메서드에서 this 는 메서드 를 가진 클래스를 의미한다.
    3. 따라서 람다식에서 this 는 람다를 포함하는 클래스의 인스턴스이다.
  2. 당연히 this 라는 인스턴스는 static 이 아니기 때문에, this 를 사용하는 람다는 static 이 아닌 lambda&plus$0 을 생성하는 것을 볼 수 있다.

 

 

그래서 차이가 뭔데?

위의 결과로 아래와 같은 차이를 알 수 있다.

 

1. 익명 내부 클래스는 새로운 클래스를 생성하지만, 람다는 새로운 메서드를 생성하여 포함한다.

  • 람다는 static 이든, 객체 사용을 위한 non-static 이든, 메서드로 생성된다.
  • 이에 반해 익명 내부 클래스는 새로운 클래스파일이 생성된다.

 

2. 익명 내부 클래스의 this : 새로 생성된 클래스, 람다의 this : 람다식을 포함하는 클래스

  • 위와 완벽하게 일맥상통하는 말이다.
  • 새로운 클래스를 만들기 때문에, 익명 내부클래스에서의 this 는 해당 클래스를 가리킨다.
  • 새로운 클래스를 만드는 게 아니고 그냥 메서드를 만드는 람다는, 메서드 즉 람다가 있는 클래스를 가리킨다.

 

 

 

 

INVOKEDYNAMIC ? INDY ? 람다 내부동작 ?

위에서 바이트코드에 어려운 말들이 등장했다.

람다식의 내부 동작 과정을 살펴보자.

 

 

1. 바이트코드의 INVOKE ~

기본적으로 INVOKEDYNAMIC 이 나오기전의 INVOKE~ 는 4가지가 있다.

바이트코드에 INVOKE ~ 는 기본적으로 아래의 의미이다.

  • invokevirtual - instance 메서드를 실행하기 위한 명령어 opcode.
  • invokestatic - static 메서드를 실행하기 위한 명령어 opcode.
  • invokeinterface - 인터페이스를 통해서 메서드를 실행하기 위한 명령어 opcode.
  • invokespecial - 생성자, 수퍼클래스, private method 등 invoke-virtual이 아닌 메서드들을 실행하기 위한 명령어 opcode.

 

 

그리고 바이트코드의 INVOKE~ opcode 뒷부분에는 아래와 같은 것들이 컴파일타임에 해석되어 명시되어 있으며, 런타임에는 이 정보들로 메서드들을 실행만하는 것이다.

  • 메소드의 이름
  • 메소드 시그니처 + 반환타입
  • 메소드가 정의되어 있는 클래스정보
  • 메소드를 실행할 수 있는 바이트코드

 

 

2. JAVA 7 의 INVOKEDYNAMIC

위 네가지의 INVOKE 들은 어떤 메서드를실행할지에 대한 정보가 컴파일타임에 모두 정해져서 바이트코드에 명시되어있다.

하지만, JAVA 7 에서는 컴파일타임이 아닌 런타임에 어떤 메서드를 실행할지 결정하기 위해 INVOKEDYNAMIC 이란 것을 만들었다.

 

람다에서 INVOKEDYNAMIC 은 람다식으로 반환될 인터페이스를 구현할 클래스를 동적으로 정의하고 인스턴스를 생성해서 반환한다.

 

함수형 인터페이스의 run(), accept() 등의 메서드를 실행한다는 것은, 람다 표현식으로 작성된 인터페이스 구현체를 실행한다는 것이고, 이것들이 INVOKEDYNAMIC 으로 치환된다.

 

 

3. INVOKEDYNAMIC 호출을 위해

INVOKEDYNAMIC 호출을 위해서는 세가지 정보가 필요하다.

  • Bootstrap method : 호출할 대상을 찾아서 연결, invokedynamic를 쓰는 메서드가 처음 실행될 때 한번 실행.
  • 정적 파라미터 목록 : 상수풀에 저장된 정보
  • 동적 파라미터 목록 : 메서드의 런타임에서 참주할 수 있는 변수

 

람다의 바이트코드는 아래와 같은 형태일 것이다.

public class SimpleLambda {  
    public static void main(String[] args) {
        Runnable lambda= invokedynamic(
            bootstrap=LambdaMetafactory,
            staticargs=[Runnable, lambda$0],
            dynargs=[]);
        lambda.run();
    }
    private static void lambda$0() {
        System.out.println(1);
    }
}

d2.naver.com/helloworld/4911107#fn:21

 

 

 

아래의 내용들은 모두 바이트코드에 명시되어 실행자체는 런타임에 이루어짐을 명심하자.

 

  1. bootstrap=LambdaMetafactory
    1. 아까 처음 봤던 LambdaMetafactory 이다.
    2. 이 LambdaMetafactory 의 metafactory() 를 자동으로 호출하는데, 이것이 bootstrap 메서드이다.
    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

 

여러가지 메서드 타입에 대한 정보를 가지고 Callsite 객체를 받는다.

 

Callsite 객체 : 실제 람다식 메서드 (위에서 본 private static, prvate 메서드) 의 정보를 포함하는 객체, 이 객체를 통해 어떤 람다식을 실행할 지 알 수 있다.

 

Callsite 클래스에는 아래와 같은 필드가 있다.

MethodHandle target;

 

그리고 MethodHandle 클래스에는 메서드를 실행시킬 수 있는 여러 메서드들이 정의되어 있다.

 

 

**따라서,

bootstrap 메서드로 반환받은 Callsite 객체의 필드인 MethodHandle 타입의 target 이라는 이름의 필드를 이용하여,

람다의 내부 내용을 변환시긴 private 메서드로 연결되는 것이다. **

 

 

 

 

2. staticargs=[Runnable, lambda$0] : 정적 파라미터 목록이다.

실제 람다식의 타입, 람다식이 변환된 메서드의 정보들이다.

 

 

3. dynargs=[] : 동적 파라미터 목록으로, 아까 봤던 람다 범위 이외의 다른 참조들을 포함한다. (this 등등)

 

 

 

 

정리

바이트코드에 정의된 InvokeDynamic 을 런타임에 실행할 때에는 아래와 같은 일들이 일어난다.

 

1. bootstrap 메서드를 호출합니다.
2. CallSite 객체를 반환받습니다.
3. Callsite 객체로 어떤 메서드를 실행할지 결정합니다.

 

www.slideshare.net/DanHeidinga/invokedynamic-evolution-of-a-language-feature

 

 

 

 

InvokeDynamic 방식의 장점

  1. 해당 코드 블륵이 처음 호출되기 전까지는 힙 메모리에 초기화되지 않기에 성능과 자원 면에서 효율적이다.
    1. 외부 변수를 사용하지 않는 (아까 static 으로 생성되었던) 람다식은 인스턴스 하나만 생성해 다시 반환한다.
    2. 무조건 클래스를 만드는 익명 클래스사용보다 인스턴스 생성비용이 1/67 이라고 한다.
  2. 람다식의 해석을 컴파일타임이 아닌 런타임으로 미뤘기 때문에, 향후 새로운 업그레이드된 JDK 가 나와도 더 유연하게 최적화할 수 있다.

 

 

 

Variable Capture

람다에서 접근가능한 변수 3개

  • 지역변수 (매개변수 포함)
  • 인스턴스 변수
  • static 변수

 

 

Variable Capture (feat. JVM Runtime Area)

다음과 같은 코드를 생각해보자.

 

public class VariableCapture {

    List<IntConsumer> intConsumerList = new ArrayList<>();

    public static void main(String[] args) {
        VariableCapture v = new VariableCapture();
        v.run();

        v.intConsumerList.get(0).accept(90);
    }

    public void run() {
        int sum = 10;
        IntConsumer consumer = a -> {
            System.out.println(sum);
            System.out.println(a);
        };
        intConsumerList.add(consumer);
        consumer.accept(4);
    }

    private void count(int i) {}
}

 

이 코드에서 중점적인 내용은 이렇다.

  • run 메서드에서 람다가 사용되고 있다.
  • 람다식에서 지역변수 sum 을 사용하고 있다.
  • 람다식을 IntConsumer 타입 리스트인 IntConsumerList 에 집어넣는다.

 

그리고 우리가 아는 내용은 이렇다.

  • 람다식은 함수객체로써, 힙 영역에 할당된다.
  • 따라서 리스트에 넣어두고 다른데서 꺼내쓰는 것이 가능하다.
  • 하지만 지역변수는 Stack 영역에 할당된다.
  • 따라서 run() 메서드의 스레드가 끝나면 할당이 해제될 것이다.

 

여기서 발생할 수 있는 문제는 아래와 같다.

  • main 메서드에서 run() 가 끝나면 sum = 10 값은 메모리에서 해제되어 사용할 수 없다.
  • 하지만 람다식은 리스트에서 꺼내 쓸 수 있다.
  • run() 가 끝나고 실행한  v.get(0).accept(90) 람다식 실행에서는 이미 메모리 해제된 sum 값을 어떻게 사용하는 것인가 ?

 

 

static 변수는 정적 영역에, 인스턴스 변수는 힙 영역에 있으니 상관없는데

지역변수를 사용할 떄 문제가 생기는 것이다.

이때 자바는 Variable Capture 를 이용하여 문제를 해결하고 있다.

 

 

그래서 Variable Capture 가 뭐 ?

람다를 포함하여 익명클래스, 메서드에 중첩된 클래스 같은 것 들에서 위의 문제를 해결하기 위해 내부에서 사용하는 지역변수를 미리 복사해서 가지고 있는 것을 말한다.

 

 

지역변수 제약 : final 이어야함

  • 지역변수를 람다에서 사용하기 위해 복사를 한다는 것을 알았다.
  • 그런데 그렇다고 아무 지역변수를 사용할 수 있는 것은 아니고, final 인 지역변수만 사용 가능하다.

 

왜 final 이어아햠 ?

  • final 이지 않은 함수는 변할 수 있다. 문제는 동시성 프로그래밍에서다.
  • 멀티스레드 환경에서 값이 변할 수 있는 변수는 각각의 스레드에서 다른 결과를 낼 수 있는 것이다.
  • 따라서 람다에서 final 지역변수만을 사용할 수 있는 제약이 생긴다.

 

final 안붙여도 되는데?

  • 아까 썼던 sum 변수는 final 을 붙이지 않았었다.
  • 하지만 아래와 같이 람다에서 값을 변경하려하면 에러를 뱉는다.
public class VariableCapture {

    List<IntConsumer> intConsumerList = new ArrayList<>();

    public static void main(String[] args) {
        VariableCapture v = new VariableCapture();
        v.run();

        v.intConsumerList.get(0).accept(90);
    }

    public void run() {
        int sum = 10;
        IntConsumer consumer = a -> {
            System.out.println(sum++); // 에러
            System.out.println(a);
        };
        intConsumerList.add(consumer);
        consumer.accept(4);
    }

    private void count(int i) {}
}

 

클로저 : 함수 범위 밖의 자유변수를 참조할 수 있는 함수.
람다도 일종의 클로저다.

 

  • 결국 사실상 final 로 취급되는 변수만 람다 (클로저) 안에서 접근할 수 있음을 알 수 있다.

 

 

쉐도잉

  • 위의 내용이 모두 이해됬다면, 쉐도잉이라는 개념도 쉽다.
  • this 키워드가 람다는 자신을 가진 클래스, 익명클래스는 익명클래스 자신을 가리키는 것을 말한다.
  • 다시말하면, 람다는 메서드로 생성되고, 익명클래스는 새로운 클래스가 생성되는 것을 말한다.
  • 다시 말하면, 아래와 같은 것을 말한다.

 

1. (익명 클래스) 아래 코드가 가능

public class Shadowing {

    int whiteShip = 1;

    Supplier<Integer> lambdaExpression() {
        return () -> {
            System.out.println(whiteShip);
            return whiteShip;
        };
    }

    public static void main(String[] args) {
        Shadowing s = new Shadowing();
        s.lambdaExpression().get();
    }
}

 

2. (람다식) 아래 코드는 컴파일 에러남

    Function<Integer, Integer> lambdaExpression() {
        int whiteShip = 1;
        
        return (whiteShip) -> { // Variable 'whiteShip' is already defined in the scope
            return whiteShip;
        };
    }

 

 

  • 에러 메세지를 보면, 해당 스코프에 이미 whiteShip 이 정의되었다고 한다.
  • 결론은, 람다와 람다를 포함하는 메서드는 같은 스코프(영역) 이다.
  • 익명 클래스는 포함하는 메서드와 다른 새로운 스코프(영역) 이다.
  • Shadowing 은 가려진다 라는 뜻으로 위의 개념을 설명하는 용어이다.

 

 

 

 

 

www.notion.so/15-757106032d85452cbc60cf1808d53978

d2.naver.com/helloworld/4911107#fn:21

stackoverflow.com/questions/30733557/what-is-a-bootstrap-method

'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글

제네릭  (0) 2021.02.24
NIO  (0) 2021.02.19
I/O  (0) 2021.02.17
ServiceLoader  (0) 2021.02.14
Annotation  (0) 2021.02.13