티스토리 뷰

Chapter. 3 - 람다표현식 (3.5 ~ 3.7)

3.5 형식 검사, 형식 추론, 제약

람다가 사용되는 코드의 문맥(콘텍스트)를 이용해서 람다의 형식(Type)을 추론할 수 있다.
어떤 콘텍스에서 기대되는 람다 표현식의 형식대상 형식(target type)이라고 한다.

3.5.1 형식검사

예시

private val inventory = listOf(Apple(RED, 100), Apple(GREEN, 90), Apple(RED, 150))

***val heavierThan150g: List<Apple> = filter(inventory) { apple: Apple -> apple.weight > 150 } // == filter(inventory, { apple: Apple -> apple.weight > 150 })***

private fun filter(inventory: List<Apple>, ***p: Predicate<Apple>***): List<Apple> {
    return inventory.filter { p.***test***(it) }
}
  1. 람다가 사용된 콘텍스트는 무엇인가?
    • filter 메소드의 두번째 인자로 { apple: Apple -> apple.weight > 150 } 가 전달되었다. 해당 람다가 사용된 콘텍스트인 filter의 정의를 확인한다.
  2. 사용된 콘텍스트의 대상 형식이 무엇인가?
    • filter 함수의 대상 형식(target type)은 Predicate<Apple>임을 확인 할 수 있다.
  3. Predicate 인터페이스의 추상메서드는 무엇인가?
    • fun test(apple: Apple): Boolean 임을 확인 할 수 있다.
  4. 추상 메서드의 함수 디스크립터 파악
    • 해당 추상 메서드는 Apple을 인수로 받아서 Boolean을 반환하는 test 메서드임을 파악할 수 있다.
    • 람다식으로 보면 Apple → Boolean 이다.
  5. 기존에 전달한 람다의 시그니처와 비교
    • { apple: Apple → apple.weight > 150 } 의 람다 시그니처 Apple → Boolean과 동일함이 검증된다.

3.5.2 같은 람다, 다른 함수형 인터페이스

위에서 대상 형식을 알아봤다. 이러한 대상 형식의 특징으로 인해 같은 람다 표현식으로 다른 함수형 인터페이스의 호환되는 추상 메서드에 사용될 수 있다.

    val intLambda = { -> 42 }
    val c: Callable<Int> = Callable { intLambda() }
    val p: PrivilegedAction<Int> = PrivilegedAction { intLambda() }

3.5.3 형식 추론

자바 컴파일러는 타입 추론이 가능하다. 즉 대상 형식을 이용해서 함수 디스크립터를 알 수 있고 컴파일러는 이를 통해 람다의 시그니처도 추론이 가능하다.

val heavierThan150g1 = filter(inventory) { **apple: Apple** -> apple.weight > 150 }
val heavierThan150g2 = filter(inventory) { **apple** -> apple.weight > 150 }

private fun filter(inventory: List<Apple>, p: Predicate<Apple>): List<Apple> {
    return inventory.filter { p.test(it) }
}
  • 위 처럼 Apple 타입을 제거해도 람다 파라미터 타입을 추론하므로 사용이 가능하다.

3.5.4 지역 변수 사용

람다 표현식은 해당 람다가 선언된 메소드의 지역변수를 사용할 수는 있다. 근데 제약 조건이 존재한다. → 상수이거나 상수 처럼 취급되는 변수여야 한다.
중요!! JAVA 한정 제약 조건이다. Kotlin에서는 그렇지 않다.

int portNumber = 8080;
Runnable r = () -> sout(portNUmber);
portNumber = 9090; // <-- 수정하는 행위가 들어가면 해당 지역변수는 사용할 수 없다.

Java에서 상수 취급되는 지역변수만 사용 가능한 이유는?

  • 일단 지역변수와 인스턴스 변수는 서로 태생이 다름을 알아야한다.
    인스턴스 변수는 클래스가 생성되어 메모리에 올라가는 시점에 함께 생성된다. 즉 클래스의 메소드의 실행 여부와 상관없이 생성되며 이때 생성된 인스턴수 변수는 Heap 메모리에 적재된다.
    반면에, 지역변수는 메서드가 콜되면서 지역변수가 호출되면서 스택에 쌓이면서 적재된다.

    이때 람다가 지역변수를 참조하고 있을 때, 람다가 스레드에서 실행되었을 때 변수를 할당한 스레드가 사라지는 경우 변수 할당이 해제될 텐데 그럼에도 람다는 해당 변수에 접근하려 할 것이다.

    따라서 자바에서는 해당 변수에 접근을 허용하지 않고 캡쳐링을 통한 복사본에 접근을 허용한다. 따라서 복사본의 값이 바뀌면 안되므로 상수 취급되는 지역변수만 사용이 가능한 것이다.

    또한, 외부 지역변수의 변경도 불가능하다. 
    람다식 바디의 생명주기가 종료되면, 내부의 지역변수 할당을 해제해야 하므로 외부 지역변수의 변경이 가능할 수가 없는 구조이다.

    따라서 자바는 외부 지역변수를 복제한 데이터를 통해 읽기만 가능한 캡쳐링만을 허용한다.

 

Kotlin에서는 어떻게 외부 변수의 수정이나 가변 지역변수의 사용이 가능할까?

이부분은 코틀린 코드를 자바 코드로 디컴파일 해보면서 비교가 가능하다.

Case1. val 불변변수를 람다식 내부에서 사용하는 경우

kotlin

class LocalVariable {
    fun execute() {
        **val immutableNumber = 5 // <<--- 수정 불가능한 변수**
        executeLambda { -> println(immutableNumber * 10) }
    }

    private fun executeLambda(
        multipleLambda: () -> Unit
    ) {
        multipleLambda()
    }
}

fun main() {
    LocalVariable().execute()
}

Java(decompile)

// LocalVariable.java
public final class LocalVariable {
   public final void execute() {
      **final int immutableNumber = 5; // <<--- 앞서 말한것과 마찬가지로 final 상수로 만들어서 취급하고 있다.**
      this.executeLambda((Function0)(new Function0() {
         public final void invoke() {
            int var1 = immutableNumber * 10;
            System.out.println(var1);
         }

         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }
      }));
   }

   private final void executeLambda(Function0 multipleLambda) {
      multipleLambda.invoke();
   }
}

// LocalVariableKt.java
public final class LocalVariableKt {
   public static final void main() {
      (new LocalVariable()).execute();
   }

   public static void main(String[] args) {
      main();
   }
}

Case2. var 가변변수를 람다식 내부에서 수정 경우

Kotlin

class LocalVariable {
    fun execute() {
        **var mutableNumber = 5 // <<--- 수정 가능한 변수**
        executeLambda { -> mutableNumber++ }
    }

    private fun executeLambda(
        multipleLambda: () -> Unit
    ) {
        multipleLambda()
    }
}

fun main() {
    LocalVariable().execute()
}

Java(decompile)

// LocalVariable.java
public final class LocalVariable {
   public final void execute() {
      **final Ref.IntRef mutableNumber = new Ref.IntRef(); // <<-- 수정 접근이 가능하도록 Heap 메모리에 적재할 수 있는 인스턴스 변수로 만든다.**
      mutableNumber.element = 5;
      this.executeLambda((Function0)(new Function0() {
         public final void invoke() {
            int var1 = mutableNumber.element++;
         }

         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }
      }));
   }

   private final void executeLambda(Function0 multipleLambda) {
      multipleLambda.invoke();
   }
}

// LocalVariableKt.java
public final class LocalVariableKt {
   public static final void main() {
      (new LocalVariable()).execute();
   }

   public static void main(String[] args) {
      main();
   }
}
  • 람다 외부에 위치한 가변 지역변수 mutableNumber를 ++하는 연산을 람다에서 수행하고 있다.

기존 Java의 경우 Stack에 적재된 지역변수이므로 이를 수정하는 것이 불가능했다.

하지만 Kotlin의 경우 Java로 디컴파일된 코드를 보면 IntRef라는 final 클래스에 의해 래핑되고, 내부적으로 변경이 가능한 변수로 포획한다는 것을 확인할 수 있다.
IntRef로 생성된 클래스는 Heap 메모리에 적재 될 것이기 때문에 람다 블록에서 변수를 변경하고 생명주기가 끝나도 영향을 받지 않을 수 있다.

3.6 메서드 참조

val inventory = listOf(Apple(RED, 100), Apple(GREEN, 90), Apple(RED, 150))

// 람다식 전달을 통한 정렬
inventory.sortedWith { a1, a2 -> a1.weight - a2.weight }

// java.util.Comparator.comparing을 이용해 method reference를 통한 정렬
inventory.sortedWith(comparing(Apple::weight))

// kotlin에서 method reference를 사용하는 방법, compareBy를 이용
inventory.sortedWith(compareBy(Apple::weight))

// selector function을 사용하는 경우
inventory.sortedBy { it.weight }
반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday