람다 식과 멤버 참조#
람다 소개: 코드 블록을 함수 인자로 넘기기#
- ‘이벤트가 발생하면 이 핸들러를 실행하자’나 ‘데이터 구조의 모든 원소에 이 연산을 적용하자’와 같은 생각을 코드로 표현하기 위해서 예전에는 무명 내부 클래스를 사용했다.
- 하지만 이는 코드가 너무 번잡스러워졌고, 자바 8부터 람다를 사용해 간결하게 작성할 수 있었다.
- 자바 컬렉션에 대해 수행하는 대부분 작업은 람다나 멤버 참조를 인자로 받는 함수를 통해 더 짧고 이해하기 쉽게 만들 수 있다.
람다 식의 문법#
val sum = {x: Int, y: Int -> x + y}
- 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
people.maxBy() {p: Person -> p.age}
- 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다.
people.maxBy {p: Person -> p.age}
- 로컬 변수처럼 컴파일러가 람다 파라미터의 타입도 추론할 수 있다.
people.maxBy {p -> p.age}
- 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우
it
으로 바로 쓸 수 있다.it
을 남용하면 안된다. 람다 안에 람다가 중촙되는 경우 각 람다의 파라미터를 명시하는 편이 낫다.
- 람다의 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 된다.
println("Computing the sum of $x and $y...")
현재 영역에 있는 변수에 접근#
- 람다는 람다 밖에 있는 변수에 접근할 수 있다.
- 코틀린은 자바와 다르게 람다 안에서 파이널 변수가 아닌 변수에도 접근하고 변경할 수 있다.
- 변경 가능한 변수를 필드로 하는 클래스를 선언하는 기법을 사용한다.

- 람다가 포획(capture)한 변수: 람다 안에서 사용하는 외부 변수
멤버 참조#
- 멤버 참조: 이중 콜론(
::
)을 사용하여 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어주는 것
- 최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다.
fun salute() = print("Salute!")
run(::salute)
val action = { person: Person, message: String ->
sendEmail(person, message)
}
val nextAction = ::sendEmail
- 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.
data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
- 확장 함수도 멤버 함수와 똑같은 방식으로 참조할 수 있다.
- 바운드 멤버 참조: 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출해준다.

컬렉션 함수형 API#
필수적인 함수: filter와 map#
filter
함수는 컬렉션을 이터레이션하면서 주어진 람다에 각 원서를 넘겨 람다가 true
를 반환하는 원소만 모은다.
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 })
map
함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다.
val list = listOf(1, 2, 3, 4)
println(list.map { it * it })
- 필터와 변환 함수를 맵에 적용할 수도 있다.
- 맵의 경우 키와 값을 처리하는 함수가 따로 존재한다.
filterKeys
와 mapKeys
는 키를 걸러내거나 변환한다.filterValues
와 mapValues
는 값을 걸러 내거나 변환한다.
all, any, count, find: 컬렉션에 술어 적용#
all
함수는 모든 원소가 주어진 람다(Predicate
)를 만족하는지 반환한다.any
함수는 주어진 람다(Predicate
)를 만족하는 원소가 하나라도 있는지 반환한다.!all
과 any
가 같고, !any
와 all
이 같지만 가독성을 높이려면 any
와 all
앞에 !
를 붙이지 않는 편이 낫다.
count
함수는 람다(Predicate
)를 만족하는 원소의 개수를 반환한다.count
가 있다는 사실을 잊어버리고, 컬렉션을 필터링한 결과의 크기를 가져오는 경우가 있다. 하지만 이렇게 처리하면 조건을 만족하는 모든 원소가 중간 컬렉션이 생겨서 추가적인 연산이 발생한다.
println(people.filter(canBeInClub27).size)
find
함수는 람다(Predicate
)를 만족하는 원소가 하나라도 있는 경우 가장 먼저 조건을 만족한다고 확인된 원소를 반환하며, 원소가 없는 경우 null
을 반환한다.- 조건을 만족하는 원소가 없으면
null
이 나온다는 사실을 더 명확히 하고 싶다면 firstOfNull
을 쓸 수 있다.
groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경#
groupBy
함수는 컬렉션을 주어진 람다에 따라 여러 그룹으로 구분 해준다.
val people = listOf(Person("Alice", 31), Person("Bob", 29), Person("Carol", 31))
println(people.groupBy { it.age })

flatMap과 flatten: 중첩된 컬렉션 안의 원소 처리#
flatMap
함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 mapping하고, 람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 한데 모은다(flatten)
val strings = listOf("abc", "def")
println(strings.flatMap { it.toList() })

- 리스트의 리스트가 있을 때 특별한 변환 없이 평평하게 펼치기만 하면 되는 경우
flatten
함수를 사용할 수 있다.
지연 계산(lazy) 컬렉션 연산#
map
이나 filter
같은 컬렉션 함수는 결과 컬렉션을 즉시 생성한다. 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다.Sequence
를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
people.asSequence()
.map(Person::name)
.filter{it.startsWith("A")}
.toList()
Sequence
안에는 iterator
라는 단 하나의 메소드가 있다. 이 메소드를 통해 시퀀스로부터 원소 값을 얻을 수 있다.Sequence
인터페이스의 강점은 시퀀스의 원소가 필요해질 때 비로소 계산된다.- 어떤 컬렉션이든
asSequence
확장 함수를 호출하면 시퀀스로 바꿀 수 있다. - 시퀀스의 원소를 차례로 이터레이션해야 한다면 시퀀스를 직접 써도된다. 하지만 시퀀스의 원소를 인덱스를 사용해 접근하는 등의 다른 API 메소드가 필요하다면 시퀀스를 리스트로 변환해야 한다.
시퀀스 연산 실행: 중간 연산과 최종 연산#
- 중간 연산: 다른 시퀀스를 반환한다.
- 최종 연산: 결과를 반환한다.
- 시퀀스의 경우 모든 연산은 각 원소에 대해 순차적으로 적용한다. 연산이 차례대로 적용하다가 결과가 얻어지면 그 이후 원소에 대해서는 연산이 이뤄지지 않을 수도 있다.

- 컬렉션에 대해 수행하는 연산의 순서도 성능에 영향을 끼친다.

- 코틀린에서 스트림과 같은 개념을 따로 구현해 제공하는 이유는 안드로이드 등에서 예전 버전 자바를 사용하는 자바 8에 있는 스틀미이 없기 때문이다.
- CPU에서 병렬적으로 실행하는 기능 등을 수행하려면 스트림 연산을 사용하면 된다.
시퀀스 만들기#
asSequence()
뿐만 아니라 generateSequence
함수를 이용해 시퀀스를 만들 수 있다.
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
자바 함수형 인터페이스 활용#
- 자바 8 이전의 자바에서는 메소드에 함수형 인터페이스를 인자로 넘기기 위해 무명 클래스의 인스턴스를 만들어야만 했다.
- 함수형 인터페이스: 단하나의 추상 메소드를 가지고 있는 인터페이스. SAM(single abstract method)라고도 불린다.
- 코틀린에서는 무명 클래스 인스턴스 대신 람다를 넘길 수 있다.
자바 메소드에 람다를 인자로 전달#
- 람다와 무명 객체의 차이점
- 무명 객체: 메소드를 호출할 때 마다 새로운 객체가 생성된다.
- 람다: 기본적으로 람다에 대응하는 무명 객체를 생성하여, 메소드를 호출할 때마다 반복 사용한다.
- 하지만 람다가 주변 영역의 변수를 포획한다면 매 호출마다 새로운 인스턴스를 생성해준다.
- 코틀린
inline
으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다.- 대부분의 코틀린 확장 함수들은
inline
표시가 붙어있다. - 코틀린의 콜렉션 확장 함수 등
SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경#
- SAM 생성자: 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수
- 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다.
- 함수의 반환 값을 람다로 직접 반환할 수 었어서, SAM 생성자로 감싸줘야 된다.
- 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 사용할 수 있다.
- 가끔 오버로드한 메소드 중에서 어떤 타입의 메소드를 선택해 람다를 변환해 넘겨줘야 할지 모호한 때, 명시적으로 SAM 생성자를 사용할 수 있다.
- 람다에서는 무명 객체와 달리 인스턴스 자신을 가리키는
this
가 없다. 람다 안에서 this
는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.- 자신을 가리키는
this
가 필요하면 무명 객체를 사용해야 된다.
수신 객체 지정 람다: with와 apply#
- 코틀린 표준 라이브러리의
with
와 apply
는 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있게할 수 있다.
with 함수#
- 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있다.
with
는 두 개의 파라미터를 받는 함수다.- 첫 번째 인자로 받은 객체로 두 번째 인자로 받은 람다의 수신 객체로 만든다.
- 인자로 받은 람다 본문에서는
this
를 통해 그 수신 객체에 접근할 수 있다. this
와 점(.
)을 사용하지 않고 프로퍼티나 메소드 이름만 사용해도 수신 객체의 멤버에 접근할 수 있다.
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet")
toString()
}
}
with
에게 인자로 넘긴 객체의 클래스와 with
를 사용하는 코드가 들어있는 클래스 안에 이름이 같은 메소드가 있으면 this
참조 앞에; 레이블을 붙이면 호출하고 싶은 메소드를 명확하게 정할 수 있다.
this@OuterClass.toString()
apply 함수#
apply
함수는 거의 with
와 같다.- 대신
apply
는 항상 자신에게 전달된 객체를 반환한다. apply
는 확장함수로 정의돼 있다.
fun alphabet2() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
- 표준 라이브러리의
buildString
함수를 사용하면 더 단순화할 수 있다.buildString
의 인자는 수신 객체 지정 람다며, 수신 객체는 항상 StringBuilder
가 된다.
fun alphabet3() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}