일급 객체
Kotlin의 함수는 Java의 함수와 다르게 일급 객체(First-Class Object)이다. 일급 객체는 일반적으로 다른 객체들에 적용 가능한 모든 연산을 적용할 수 있는 객체를 의미하는데, Java는 여타 객체들이 함수의 파라미터, 리턴 값으로 선언될 수 있는 반면 함수는 그러지 못한다. 또한 함수 자체에는 타입을 가지지 못하고 SAM(Single Abstracy Method)인터페이스를 통해 표현되기 대문에 일급 객체라고 할 수 없다.
반면 Kotlin의 경우에는 여타 다른 객체들에 적용 가능한 연산들을 Kotlin 함수에도 적용할 수 있다. 따라서 함수를 변수로써 선언할 수 있고 , 함수 자체를 다른 함수의 파라미터 혹은 리턴 값으로 선언할 수 있다. 한번 비교해보자.
public class FunctionTestJava {
public static void main(String[] args) {
Supplier returnTenAnonymous = new Supplier() {
@Override
public Object get() {
return 10;
}
};
Supplier returnTenLambda = () -> {
return 10;
};
System.out.println("ANONYMOUS:" + returnTenAnonymous); //ANONYMOUS: function.FunctionTestJava$1@30f39991
System.out.println("LAMBDA: " + returnTenLambda); //LAMBDA: function.FunctionTestJava$$Lambda$14/0x0000000801003400@452b3a41
System.out.println("ANONYMOUS GET: " + returnTenAnonymous.get()); //10
System.out.println("LAMBDA GET: " + returnTenLambda.get()); //10
}
}
Java는 위와 같이 함수 자체는 타입을 가지지 못한다. 단지, `Supplier` 라는 Java에서 제공하는 SAM 인터페이스를 통해 표현할 수 있을 뿐이다. 따라서 Java에서 함수를 파라미터로 넘기거나 리턴하는 것은 사실 SAM 인터페이스의 구현체를 파라미터로 넘기거나 리턴하는 것을 의미한다.
fun main() {
val returnTenLambda: () -> Int = { 10 }
val returnTenAnonymous: () -> Int = fun() = 10
val returnTenAnonymous2: () -> Int = fun(): Int {
return 10
}
println(returnTenLambda) // Function0<java.lang.Integer>
println(returnTenAnonymous) // Function0<java.lang.Integer>
println(returnTenLambda()) // 10
println(returnTenAnonymous()) // 10
}
반면 Kotlin은 함수 자체에도 타입이 있고, 함수를 변수로 선언할 수 있다. `returnToLambda` 변수를 보면, 타입이 `() -> Int` 이다. 이는 이 변수가 파라미터로 아무것도 받지 않고 `Int` 타입을 리턴하는 함수 타입이라는 소리다. 따라서 함수를 함수 자체로 파라미터로 넘기거나 리턴할 수 있게 된다.
고차 함수
함수의 파라미터나 리턴값으로 함수 타입이 선언되어 있는 함수를 고차함수라고 부른다. Kotlin은 함수를 함수 자체로 다른 함수의 파라미터나 리턴 값으로 선언할 수 있다고 했다. 한번 고차함수를 만들어보자.
fun getSumValueOfTwoFunction(returnInt1: () -> Int, returnInt2: () -> Int): Int {
return returnInt1() + returnInt2()
}
fun main() {
val returnTenLambda: () -> Int = { 10 }
val returnTenAnonymous: () -> Int = fun() = 20
println(getSumValueOfTwoFunction(returnTenLambda, returnTenAnonymous))
}
`getSumValueOfTwoFunction` 함수는 `() -> Int` 타입의 함수를 두 개 받고 `Int` 타입을 반환한다. 함수 타입이 파라미터로 선언되어 있기에 이 함수는 고차함수이다.
fun getSumFunctionOfTwoFunction(returnInt1: () -> Int, returnInt2: () -> Int): () -> Int {
return { returnInt1() + returnInt2() }
}
fun getFunction(): (() -> Int, () -> Int) -> () -> Int {
return fun(returnInt1: () -> Int, returnInt2: () -> Int): () -> Int {
return { returnInt1() + returnInt2() }
}
}
fun main() {
val returnTenLambda: () -> Int = { 10 }
val returnTenAnonymous: () -> Int = fun() = 20
println(getSumFunctionOfTwoFunction(returnTenLambda, returnTenAnonymous)())
println(getFunction()(returnTenLambda, returnTenAnonymous)())
}
위 예시에서는 함수를 파라미터로 받는 함수와, 함수를 리턴하는 함수를 복잡하게 만들어 봤다. 사실 위와 같이 작성할 일이 정말 없겠지만, Kotlin에서는 위와 같은 코드가 가능하다.
함수 리터럴
프로그래밍에서 리터럴이라는 단어는 소스코드의 고정된 값을 나타내는 표기법을 의미한다. 예를 들어, Java나 Kotlin에서 10L 이라는 문자는 "`long` 자료형 숫자 10"이라는 고정된 값을 의미하고, 'a' 라는 문자는 `char`형 문자 a 라는 고정된 값을 의미하는데, 이렇게 고정된 값을 표기한 것 자체를 리터럴이라고 한다. 만약 int형 숫자 10 이라는 고정된 값을 표기하기 위해서는 "10" 으로 표기하면 되는데 이것 역시 리터럴이다. 리터럴은 리터럴 자체로는 의미가 없기 때문에, 변수 초기화 또는 인자 값으로 줄 때 같이 쓰이는 일이 많다. (소스 코드 상 10L만 작성해놓는 일은 거의 없을 것이다. 대부분 `val num: Long = 10L` 처럼 변수를 초기화할 때 리터럴을 사용한다.)
번외로 Javascript에는 Java나 Kotlin에는 없는 객체 리터럴도 존재한다.
let stooge = {
first_name : "Jerome",
last_name : "Howard"
}
위 코드는 위키백과에 있는 코드인데, Javascript에서 객체를 표기하는 방법이라고 한다. JSON 형식에도 영감을 주었다고 한다.
각설하고 Kotlin의 함수 역시 고정된 값을 표기하는 리터럴이 존재한다. 리터럴의 종류로 익명함수, 람다식 등이 있는데 자세히 살펴보자.
익명함수
익명함수란 말 그대로 이름이 없는 함수를 의미한다. 함수이기 때문에 `fun` 키워드를 통해 선언할 수 있다. 예시를 작성해보자.
val sumAnonymous: (a: Int, b: Int) -> Int = fun(a, b) = a + b
val sumAnonymous = fun(a, b) = a + b // 함수 타입을 추론할 수 없기에 불가능
val sumAnonymousDetail: (a: Int, b: Int) -> Int = fun(a: Int, b: Int): Int { return a + b }
val sumAnonymousDetail = fun(a: Int, b: Int): Int { return a + b } // 함수 파라미터에 형을 명시했기에 가능
`fun` 키워드로 함수를 선언하되, 이름은 선언하지 않는다. Kotlin의 일반 함수처럼 본문에는 식 혹은 문(블록)이 들어갈 수 있다. 문이 본문인 함수는 일반 함수와 동일하게 리턴 값이 있다면 리턴 타입을 선언해야 하며, 문에도 `return` 키워드를 통해 반환값을 명시해줘야 한다. 이외 파라미터는 일반적인 Kotlin 문법처럼 추론 가능한 경우 생략할 수도 있다. (변수, 파라미터, 리턴 타입에 명시된 경우 등)
람다식
람다식은 익명함수보다 간결하게 함수를 선언할 수 있는 방법이다. `fun` 키워드 없이, { } 안에 함수를 작성하면 된다.
val sumLambda: (a: Int, b: Int) -> Int = { a, b -> a + b }
val sumLambda = { a: Int, b: Int -> a + b }
람다식은 파라미터 -> 본문 형식으로 이루어져 있다. 이 역시 타입이 추론 가능한 경우 생략할 수 있다.
익명함수는 문이 본문인 함수로 선언을 했다면 리턴 타입을 명시할 수 있지만, 람다식은 함수 선언부에 파라미터만 명시가 가능하고, 리턴 타입은 명시할 수 없다. 그리고 람다식 내에 `return` 키워드를 사용할 수 없다.
fun main() {
val sumLambda: (a: Int, b: Int) -> Int = { a, b ->
return a + b // 불가능!
}
}
`return` 키워드는 가장 가까운 `fun` 블록을 종료하거나 반환하는 역할을 한다. 람다식은 `fun` 키워드가 없기 때문에, 람다식에 `return`을 허용한다면 람다식이 종료되는 게 아니라, 가장 가까운 `main` 함수를 종료시키게 된다. 이를 "비지역적 반환(non-local return)"이라고 하는데, 람다식 블록 안에 있는 `return`은 `main` 블록 안에 있는 지역적인 `return`이 아니기 때문이다. Kotlin에서는 기본적으로 이런 비지역적 반환을 금지해놓았다. 람다식을 리턴할 것을 예상했는데, 정작 람다식을 호출하는 부모 함수가 종료되는 예기치 못한 문제가 발생할 수 있기 때문이다.
수신 객체가 있는 함수 리터럴
Kotlin 에는 확장함수라는 개념이 있다. 특정 클래스의 함수를 외부에서 확장할 수 있도록 만들어진 문법이다. 기본적으로 아래와 같이 선언한다.
fun LocalDateTime.isBeforeOrEqual(from: LocalDateTime): Boolean {
return this.isBefore(from) || this.isEqual(from)
}
fun LocalDateTime.isAfterOrEqual(from: LocalDateTime): Boolean {
return this.isAfter(from) || this.isEqual(from)
}
fun LocalDateTime.isBetweenOrEqual(from: LocalDateTime, to: LocalDateTime): Boolean {
return this.isAfterOrEqual(from) && this.isBeforeOrEqual(to)
}
확장 함수는 함수명 앞에 확장하고자 하는 클래스의 객체를 참조하도록 명시해준다. 그러면 함수 본문에서 this를 통해 객체의 프로퍼티나 함수를 사용할 수 있게 된다. 이 객체를 "수신 객체"라고 부른다. 이러한 확장함수 역시 리터럴로 값을 표현할 수 있는데, 이를 "수신 객체가 있는 함수 리터럴"이라고 한다.
val isBeforeOrEqual = fun LocalDateTime.(from: LocalDateTime): Boolean {
return this.isBefore(from) || this.isEqual(from)
}
val isAfterOrEqual: LocalDateTime.(LocalDateTime) -> Boolean = { from ->
this.isAfter(from) || this.isEqual(from)
}
val isBetweenOrEqual: LocalDateTime.(LocalDateTime, LocalDateTime) -> Boolean =
{ from: LocalDateTime, to: LocalDateTime ->
this.isAfterOrEqual(from) && this.isBeforeOrEqual(to) }
수신 객체가 있는 함수 리터럴은 위와 같이 익명 함수를 사용할 수도 있고, 람다식을 사용할 수도 있다.
리터럴로 표기한 경우 해당 함수는 고정된 값이므로 변수에 초기화하거나 파라미터, 리턴 값으로 줄 수 있게 되는데, 기본적으로 람다식은 람다식에서 리턴하는 값의 타입을 표시할 수 없기 때문에, 리턴 타입이 있는 경우에는 초기화 되는 변수, 다른 함수의 파라미터, 리턴 타입으로 함수가 사용되는 경우 해당 함수의 리턴 타입이 명시되어 있어야 사용 가능하다.
fun main () {
someFunction { from: LocalDateTime, to: LocalDateTime ->
this.isAfterOrEqual(from) && this.isBeforeOrEqual(to)
}
}
fun someFunction(func: LocalDateTime.(LocalDateTime, LocalDateTime) -> Boolean) {}
위와 같이 다른 함수의 파라미터로 리터럴을 통해 넣어주었다면, 함수 파라미터에는 리턴 타입을 명시되어 있기 때문에 람다식으로 제공이 가능하다.
val isBetweenOrEqual: LocalDateTime.(LocalDateTime, LocalDateTime) -> Boolean =
{ from: LocalDateTime, to: LocalDateTime ->
this.isAfterOrEqual(from) && this.isBeforeOrEqual(to) }
isBetweenOrEqual(now, from, to)
now.isBetweenOrEqual(from, to)
수신 객체가 있는 함수 리터럴은 위와 같이 첫번째 파라미터로 수신 객체를 넣어주거나, 수신 객체로부터 함수를 호출하는 방법으로 사용 가능하다.
그리고 수신 객체가 있는 함수 리터럴은 당연하지만 일반적인 확장 함수와 다르게 지역적이다. 아래의 예시를 보자.
fun main() {
...
val isBetweenOrEqual: LocalDateTime.(LocalDateTime, LocalDateTime) -> Boolean =
{ from: LocalDateTime, to: LocalDateTime ->
this.isAfterOrEqual(from) && this.isBeforeOrEqual(to) }
now.isBetweenOrEqual(from, to)
}
fun someFunction() {
val now = LocalDateTime.now()
now.isBetweenOrEqual() // 접근 불가!
}
`someFunction`에서 `main`에 선언된 `isBetweenOrEqual` 함수에 접근하려고 하면 당연하지만 접근이 안된다. 해당 함수 리터럴은 `main` 함수의 지역변수에 초기화가 되어 있고, 지역변수는 선언된 함수 내에서만 사용 가능하기 때문에 `main`에서만 사용할 수 있게 된다.
보통 확장함수를 kt 파일 안에 전역 함수로 선언해두고 유틸성으로 사용하기 때문에 위와 같이 리터럴로 표기하여 지역 변수에 초기화 한 경우에도 사용 가능하다고 오해할 수 있는데 그렇지 않다는 점을 알아두자.
클로저(Closure)
클로저란 일급 객체 함수(first-class functions)의 개념을 이용하여 스코프에 묶인 변수를 바인딩 하기 위한 일종의 기술이라고 위키피디아에서 설명하고 있다. 쉽게 설명하자면 스코프 내에 선언된 변수를 캡처(바인딩, 잡아두기)하는 기술 자체를 클로저라고 생각하면 될 것이다. 이렇게 클로저를 통해 잡아둔 변수는, 외부 스코프에서도 사용할 수 있게 된다.
Java의 함수형 프로그래밍을 생각해보면, 외부 스코프에 선언된 변수를 함수에서 사용하기 위해서는 해당 변수가 final 변수거나, effectively final 변수(선언 이후에도 참조가 변하지 않는 변수)여야 한다. 아래 예시를 살펴보자.
public static void main(String[] args) {
int num = 1;
Supplier fun = () -> {
num += 1;
return num;
};
num++
}
`num` 변수는 final 변수도 아니고, 이후 값이 변하기 때문에 다른 스코프의 함수 내에서 사용할 수 없는 변수가 된다.
fun main() {
var num = 1
val func = {
num += 5
}
num++
}
반면 Kotlin의 경우에는 `num` 변수를 사용할 수 있다. Kotlin은 클로저를 통해 이를 지원하기 때문이다.
public static final void main() {
final Ref.IntRef num = new Ref.IntRef();
num.element = 1;
Function0 func = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
Ref.IntRef var10000 = num;
var10000.element += 5;
}
});
int var10001 = num.element++;
}
자바 코드로 디컴파일 해보면, 위와 같이 `Ref` 라는 객체 하나를 만들어서 외부에 선언된 변수를 캡처하는 것을 볼 수 있다. 자바 코드도 위와 같이 작성한다면, 당연히 사용 가능할 것이다.
위와 같은 기능을 사용할 수 있다는 점 자체는 좋지만, 당연히 Ref 객체를 생성하기 때문에 성능 오버헤드가 따르기 마련이다. 게다가, 함수 자체는 `FunctionN` 클래스의 인스턴스를 만드는 방식으로 동작하기 때문에 함수를 반복문에서 사용하는 경우에는 인스턴스가 많아질 수 있다. 이런 성능 문제를 해결하기 위해 `inline` 함수를 사용하는 것을 고려해볼 수 있다.
inline 함수
`Inline` 함수란 단어 그대로 함수 호출부에 함수가 인라이닝 되는 함수를 의미한다. 함수를 선언할 때, `inline` 키워드를 통해 `inline` 함수를 만들 수 있다.
fun main() {
var num = 1
val sum = sumFive(num)
}
inline fun sumFive(num: Int): Int {
return num + 5
}
위와 같이 함수를 작성하고 디컴파일 해보면 아래와 같다.
public static final void main() {
int num = 1;
int $i$f$sumFive = false;
int sum = num + 5;
}
함수의 바디 자체가 그냥 함수 호출부에 들어간 것을 볼 수 있다.
위에서 함수 리터럴을 통해 함수를 선언할 때마다 `FunctionN` 인스턴스를 만들었었는데, `inline` 함수를 사용한다면 인스턴스 초기화에 따른 오버헤드를 줄일 수 있을 것이다.
고차 함수를 inline으로 선언할 경우
fun main() {
repeat(5) {
println("repeat")
}
}
inline fun repeat(count: Int, func: () -> Unit) {
for (i in 1 .. count) {
func()
}
}
만약 위와 같이 고차 함수를 `inline` 함수로 선언한다면, 고차 함수 내에서 사용되는 함수 역시 인라이닝 된다.
public static final void main() {
int count$iv = 5;
int $i$f$repeat = false;
int i$iv = 1;
byte var3 = count$iv;
while(true) {
int var4 = false;
String var5 = "repeat";
System.out.println(var5);
if (i$iv == var3) {
return;
}
++i$iv;
}
}
컴파일된 코드를 보면, 프린트문이 함수 내에 그대로 인라이닝 된 것을 볼 수 있다.
하지만 이는 고차함수의 인자로 들어간 함수를 알고 있기 때문에 가능한 일이고, 만약 인자로 들어가는 함수를 알 수 없는 경우에는 인라이닝 되지 않는다. 아래 예시를 살펴보자.
fun main(func: () -> Unit) {
repeat(5, func)
}
`repeat` 함수를 호출 할 때, 외부에서 넘어오는 함수 인자를 그대로 넣어주고 있다. 따라서 `main` 함수 자체에서는 해당 함수가 어떻게 구현이 되어있는 지 알 수 없다.
public static final void main(@NotNull Function0 func) {
Intrinsics.checkNotNullParameter(func, "func");
int count$iv = 5;
int $i$f$repeat = false;
int i$iv = 1;
byte var4 = count$iv;
while(true) {
func.invoke();
if (i$iv == var4) {
return;
}
++i$iv;
}
}
이런 경우에는 당연히 구현을 알 수 없기 때문에 인라이닝되지 않는다. `func.invoke()`를 통해 함수를 실행시키는 것을 볼 수 있다.
noinline
inline fun repeat(count: Int, noinline func: () -> Unit) {
for (i in 1 .. count) {
func()
}
}
만약 고차함수 인자를 인라이닝 하고 싶지 않다면, 위와 같이 파라미터 선언부에 `noinline` 키워드를 통해 인라이닝을 강제로 하지 않을 수 있다.
non-local return의 허용
위에서 고차함수에 대해 다룰 때, non-local return에 대해서 설명했었고, 고차함수에서는 불가능 하다고 말했었다. 하지만 인라인 고차함수에 넘겨주는 함수 인자는 비지역적반환이 가능하다.
fun main() {
repeat(5) {
println("repeat")
return
}
}
inline fun repeat(count: Int, func: () -> Unit) {
for (i in 1 .. count) {
func()
}
}
`main` 함수에서 `repeat` 함수를 호출하는 부분을 보면, `return`을 사용하더라도 컴파일 오류가 나지 않는다. 이는 `repeat` 함수가 `inline` 함수이기 때문이다.
다만 명심할 점은, `return` 키워드는 가장 가까운 함수 블록을 종료시키는 키워드이다. 따라서 위와 같은 경우, `repeat` 함수가 인라이닝 되기 때문에 고차함수에서 `return`이 호출되면 해당 함수가 종료되는게 아니라 `main` 함수를 종료시킨다. 이 점을 유의해야 한다.
Kotlin의 SAM 인터페이스
위에서 Java의 경우에는 함수가 일급 객체가 아니기 때문에, SAM(Single Abstract Method) 인터페이스를 통해 함수를 표현한다고 했다. Kotlin은 함수가 일급 객체이긴 하지만, Kotlin에서도 SAM 인터페이스를 선언할 수 있고 Java처럼 간단하게 표현할 수도 있다.
우선 간단하게 추상 메서드 하나만 가지고 있는 인터페이스를 만들어보고, 해당 인터페이스를 초기화 해보자.
interface SAMInterface {
fun doSomething(int: Int)
}
fun main() {
val sam = object : SAMInterface {
override fun doSomething(int: Int) {
TODO("Not yet implemented")
}
}
}
추상 메서드 하나만 가지고 있는 인터페이스는 위와 같이 초기화 할 수 있다. Java로 따지자면, 익명클래스를 통해 초기화 한 것과 비슷할 것이다.
그러나 Java는 람다식으로도 이를 초기화 할 수 있었다. Kotlin에서도 더 간단하게 초기화할 수 있을까? 바로 `fun` 키위드를 인터페이스 선언부에 작성하면 가능하다.
fun interface SAMInterface {
fun doSomething(int: Int)
}
fun main() {
val sam = SAMInterface { TODO("Not yet implemented") }
val sam: SAMInterface = { int: Int -> TODO("Not yet implemented") } // Compile Error
}
위와 같이 `fun` 키위드를 인터페이스 선언부에 작성하면, 마치 생성자를 호출하듣이 간단하게 호출할 수 있게 된다. 물론 Java는 단순 람다식으로도 초기화가 가능하지만, 코틀린에서 이는 불가능하다.
왜냐하면 일반적으로 코틀린의 함수는 일급 객체이기 때문에, 함수마다 타입이 존재하기 때문이다. 위 예시에서 람다로 표기한 함수는 `(Int) -> Unit` 타입이 된다. 하지만 실제 `sam` 이라는 변수는 `SAMInterface` 타입만이 가능하기에 위와 같이 람다로 초기화하는 게 불가능 한 것이다. 단, 함수 파라미터의 인자로 제공할 때에는 람다로도 표현이 가능하다.
fun someFunction(sam: SAMInterface) {
TODO()
}
fun main() {
val sam = SAMInterface { TODO() }
someFunction(sam)
someFunction { TODO() }
}
`someFuction`은 `SAMInterface` 객체가 파라미터로 선언되어 있다. 이럴 경우에는 `main`함수의 맨 아랫줄 처럼 람다식으로도 `SAMInterface`의 객체를 표현할 수 있게 된다.
하지만 SAM 인터페이스를 함수 파라미터에는 람다로 제공할 수 있다는 점 때문에 문제가 생길수도 있다. 다음과 같은 경우를 살펴보자.
fun interface SAMInterface {
fun doSomething(int: Int)
}
fun someFunction(sam: SAMInterface) {
println("SAM 파라미터 적용")
}
fun someFunction(func: (Int) -> Unit) {
println("람다 파라미터 적용")
}
위와 같이 함수를 오버로딩 했는데, SAM 인터페이스에 선언한 함수의 타입과 동일한 타입의 함수를 받도록 오버로딩 되어있자고 해보자. 당연히 첫번째 함수는 `SAMInterface` 타입을 받고, 두번째 함수는 `(Int) -> Unit` 타입을 받기 때문에 사실상 둘은 다른 타입이라 오버로딩이 가능하다.
위와 같은 상황에서, 과연 아래와 같은 코드는 어떤 함수가 호출이 될까?
someFunction { TODO() } // "람다 파라미터 적용"
보면 인자로 주어지는 람다식 자체는 두 함수 모두 적용이 가능한 상태이다. 하지만 이 함수는 호출하면 람다 파라미터가 선언된 함수가 호출된다. 만약, `SAMInterface`가 선언된 함수를 호출하고 싶다면, 파라미터를 람다가 아니라 `SAMInterface` 타입이라는 것을 명시해주어야 한다.
someFunction(SAMInterface { TODO() })
하지만 위와 같이 코드를 작성하는 것은 좋지 않다. 의도와 다르게 작동할 수 있기 때문이다. 다음 예시를 보면 생각보다 복잡한 문제라는 것을 확인할 수 있을 것이다.
fun someFunction(sam: SAMInterface) {
println("SAM 파라미터 적용")
}
fun <T> someFunction(func: (T) -> Unit) {
println("람다 파라미터 적용")
}
이번에는 람다 파라미터를 받는 함수를 제네릭 함수로 만들어보았다. 이 타입 파라미터가 `Int`라고 가정을 하고 위 예시와 똑같이 한번 호출해보자.
someFunction{ TODO() } // "SAM 파라미터 적용"
이전 예시에서는 람다 파라미터를 받는 함수가 호출되었다. 이번에도 똑같을까? 아니다. 이번에는 SAM 인터페이스를 받는 함수가 호출된다. 이는 Koltin 에서 타입이 더 구체적인 함수를 우선하여 호출하는 매커니즘이 있기 때문이다.
따라서 위와 같이 함수를 오버로딩하게 된다면, 어떤 경우에는 SAM 인터페이스를 파라미터로 받는 함수가, 어떤 경우에는 람다를 파라미터로 받는 함수가 호출될지 직접 확인하는 과정이 필요해진다. 따라서 지양하는 것이 좋을 것 같다. 아니면 컨벤션을 SAM 인터페이스를 파라미터로 넘길 때는 무조건 SAM 인터페이스를 표기하는 방식으로 파라미터를 넘겨주도록 정해야 할 것이다.