람다
다른 함수에게 넘길 수 있는 코드 조각
- 자바 8 버전에서 람다가 도입되기 전에는 무명클래스를 통해 필요한 동작코드를 전달
- 하지만 람다를 통해서 훨씬 간단하게 코드조각을 전달해줄 수 있다.
{ x: Int, y: Int -> x + y }
- 위처럼 중괄호와 -> 로 람다 식을 표현
- 람다가 인자의 마지막인 경우는 () 밖으로 뺄 수 있다.
- 람다 파라미터가 하나인 경우에는 it 디폴트 이름으로 사용 가능
변수 포획 (capture)
- 람다는 람다 외부에 있는 변수를 접근해 변경할 수 있다.
class Ref<T>(var value: T)
>> val counter = Ref(0)
>> val inc = {counter.value++}
var count = 0
val inc = { counter++ }
- 다음과 같이 변경할 수 있는 변수를 가진 final한 클래스를 포획해서 나중에 변경할 수 있게 된다.
수신객체 지정 람다 (lambda with receiver)
- 람다는 확장함수처럼 본문에서 다른 객체를 명시하지 않고 this로 참조할 수 있는 람다다.
- 객체를 반복해서 호출하지 않아도 되기 때문에 코드가 간결해진다.
- with, apply, run, let, also 등등이 있는데 아래 scope function에 자세히 적어놓았다.
함수형 인터페이스
- 자바의 함수형 인터페이스를 제공해야 하는 API를 사용하는 경우 람다를 대신 넘길 수 있다.
SAM (Single Abstract Method)
- 추상 메서드가 하나만 존재하는 인터페이스이다.
- OnClickListener가 대표적
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v){
...
}
}
- 자바에서는 이렇게 무명객체를 만들어서 전달해줘야했지만
button.setOnClickListener { view -> ... }
processTheAnswer(
new Function1<Integer,Integer>() {
@Override
public Integer invoke(Integer number) {
System.out.println(number);
return number + 1;
}
}
);
- 이런식으로 코틀린에서 제공하는 FunctionX 인터페이스를 통해서 구현할 수 있다.
무명객체 vs 람다
- 무명객체는 메서드를 호출할 때마다 새로운 객체를 생성한다.
- 람다는 하나의 인스턴스를 반복 사용한다.
- 그러나 외부 변수를 포획한 경우에는 외부 변수 값이 변경되기 때문에 호출할 때마다 새로운 인스턴스를 생성한다.
- 그리고 람다를 그냥 함수타입으로 사용하는 경우는 SAM 방식으로 호출하지 않는다.
고차함수
함수를 인자로 전달하거나 반환할 수 있는 함수
함수 타입
(p1: Int, p2: String) -> R: Int
이런 식으로 표현한다.
inline
- 람다가 외부변수를 포획하면 매번 새로운 무명 클래스를 생성한다.
- 그럼 람다를 자주 사용하면 성능이 떨어지는 거 아닌가?
- 이 때 inline을 통해서 함수에 전달되는 람다를 같이 호출된 함수의 본문에 inline 시킨다.
- 그러나 inline 함수가 너무 크면 좋지 않다.
- 대부분 컬렉션 API 함수들은 inline이다.
넌로컬 리턴
- inline으로 람다가 인라이닝되면 람다가 전달되는 함수를 호출하는 함수를 알 수 있게 되므로 외부 함수를 람다에서 리턴할 수 있게 된다.
fun lookForAlice(people: List<Person>) {
people.forEach {
if( it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found!")
}
함수형 컬렉션 API
- 컬렉션에서 for 문 대신에 함수형 컬렉션 API를 통해서 컬렉션 요소들을 편하게 iterate할 수 있다.
- 람다 대신에 :: 멤버 참조를 통해서 이미 만들어진 메서드, 프로퍼티 등의 참조를 전달해줄 수 있다.
- filter, map, all, any, count, find, groupBy, flatMap 등등 다양하게 존재
scope function
- let
public inline fun <T,R> T.let(block: (T) -> R): R
- null이 될 수 있는 식을 다룰 때 사용
- taskId?.let{}으로 ?뒤에 사용하면 let안에서는 taskId가 null이 아님을 보장할 수 있다.
- 만약 taskId가 null이 될 수 있으면 let{} ?: 으로 elvis 연산자를 통해 null처리도 해줄 수 있다.
- if(taskId==null) {} else {}와 동일
- run
- with
public inline fun <T,R> with(receiver: T, block: T.() -> R): R
- 거의 사용하지 않음
- with에 들어오는 값의 null check를 해주지 않기 때문
- nullable한 값이라면 내부에서 ?.let을 또 해줘야 하는데 그럴바에 그냥 처음부터 let을 쓰는 게 더 이득
- this로 받는 게 let과의 차이점인데 null이 절대 아니면 사용할 만 하지만 null이 아닌 경우에 run이랑 동일 기능을 제공하기 때문에 run을 대신 써도 됨
- apply
public inline fun <T> T.apply(block: T.() -> Unit): T
- this로 값이 전달 됨
- 전달받은 수신객체를 리턴
- 주로 객체 생성 시 초기화을 동시에 해줄 때 사용
- also
public inline fun <T> T.also(block: (T) -> Unit): T
- apply와 비슷하지만 it으로 전달되기 때문에 이미 생성한 객체의 세부 속성을 변경하고자 할 때 사용
- 마찬가지로 수신객체를 리턴하기에 내부 속성 변경용으로 사용
T.() vs (T)
- scope function 보니 T.() 도 있고 (T)도 있는데 차이점은 무엇일까
확장함수
어떤 클래스의 메소드인 것처럼 호출할 수 있는 함수
- 즉, 진짜 그 클래스 내 메소드가 아니고 외부에서 선언되었지만 그 클래스 메소드인것 처럼 클래스.메소드이름() 이렇게 부를 수 있는 것
fun String.lastChar(): Char = this[this.length-1]
- 위 식이 확장함수이며
- 앞의 String. 수신 객체의 타입 (receiver type)
- 뒤의 this가 수신 객체 (receiver object)
- 확장 함수가 호출되는 대상이 되는 객체
- 만약 “hello,world!”.lastChar()를 호출했다면 this는 “hello, world!” 라는 String 객체 인스턴스를 가리킴
- 확장함수는 정적메소드이므로 오버라이딩 할 수 없다.
(T)
fun String.lastChar(block: (String) -> Unit): Char {
block(this)
return this[this.length-1]
}
- 그럼 (T) 로 전달을 하게 되면 수신객체의 값이 it으로 전달이 된다!
T.()
- T.()은 확장함수처럼 보인다.
- block이라는 람다식의 파라미터 ()가 T의 확장함수라는 의미이다.
- 그럼 파라미터라는 새로운 확장함수 내부에서 T를 this로 사용할 수 있다
- lastChar를 생각해라! (String.lastChar() = this 로 접근가능했다)
- 람다이름()으로 호출만 해도 T의 확장함수로 호출되는 효과가 생겨서 내부적으로 this를 사용할 수 있게 된다.
fun String.lastChar(block: String.() -> Unit): Char {
block()
return this[this.length-1]
}
- this로 변경된 것 빼고는 값은 동일하게 나온다!
시퀀스 (Sequence)
- 함수형 컬레션은 함수결과 리스트를 바로바로 생성
- 중간결과를 담는 임시 컬렉션이 생성된다.
- 컬레션 원소가 많아지면 메모리를 많이 사용
- 시퀀스는 중간리스트가 없기 때문에 원소가 많은 경우 유용
중간연산, 최종연산
- 중간연산 (intermediate)
- 시퀀스를 반환
- 최종연산이 호출되면 그제서야 각 원소에 대해 필요한 중간연산과정들을 계산한다.
- 중간에 원하는 결과값을 찾으면 그 이후 원소는 계산안할 수도 있다.
- 최종연산을 호출하지 않으면 아무것도 출력되지 않는다.
- 최종연산 (terminal)
- 결과를 얻을 수 있는 연산
- toList() 가 대표적
- 꼭 컬렉션으로 변경해야 하는가?
- Sequence로 남겨둬서 iterator를 사용할 거라면 변경안해도 됨
- 그러나 인덱스를 통해 컬렉션 API 쓰고 싶으면 컬렉션으로 변경해야
스트림
- 시퀀스를 자바의 스트림과 동일한 개념
- 그럼 왜 또 만들었냐?
- 코틀린 액션에서는 "안드로이드 등에서 예전 버전 자바를 사용하는 경우 자바 8에 있는 스트림이 없기 때문이다." 라고 적혀있다.
- 내 생각으로는 자바 8 이전버전으로 구현한 안드로이드에서 스트림을 사용하고 싶을 때 코틀린의 시퀀스를 사용할 수 있도록 만든게 아닌가? 싶다.
출처
Kotlin in Action 5장, 8장