- 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 의미
📖 5.1 람다식과 멤버 참조
🔖 5.1.1 람다 소개: 코드 블록을 값으로 다루기
button.setOnClickListener(object: OnClickListener {
override fun onClick(v: View) {
println("I was clicked!")
}
})
button.setOnClickListener {
println("I was clicked!")
}
- 람다를 메서드가 하나뿐인 익명 객체 대신 사용할 수 있다.
🔖 5.1.2 람다와 컬렉션
fun findTheOldest(people: List<Person>) {
var maxAge = 0
var theOldest: Person? = null
for (person in people) {
if (person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
people.maxByOrNull { it.age }
maxByOrNull함수를 이용하면 편한다.- 람다가 인자를 하나만 받고 그 인자에 구체적인 이름을 붙이고 싶지 않기 때문에 it이라는 암시적 이름을 사용한다.
- 멤버 참조 또한 사용할 수 있다.
🔖 5.1.3 람다식의 문법
fun main() {
val sum = {x: Int, y: Int -> x + y}
println(sum)
}
- 코틀린 람다식은 중괄호로 둘러싸여 있다.
- 화살표가 인자 목록과 람다 본문을 구분해준다.
fun main() {
{ println(42) }
}
- 람다식을 직접 호출해도 된다.
- 별로 쓸모가 없어 직접 실행하는 편이 낫다.
fun main() {
run{ println(42) }
}
- run은 인자로 받은 람다를 실행해 주는 라이브러리 함수
people.maxByOrNull({p: Person -> p.age })
- 구분자가 너무 많이 쓰여서 가독성이 덜어진다.
- 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입을 굳이 적을 필요가 없다.
- 인자가 하나뿐인 경우 이름을 붙이지 않아도 된다.
people.maxByOrNull() {p: Person -> p.age }
- 코틀린에는 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
- 문법적 관습
people.maxByOrNull {p: Person -> p.age }
- 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출시 빈 괄호를 없애도 된다.
fun main() {
val people = listOf(Person("Dmitry", 4), Person("Eve", 4))
val names = people.joinToString(
separator = " ",
transform = { p: Person -> p.name }
)
println(names)
}
people.joinToString(" ") { p: Person -> p.name }
- 굉장히 간결하게 변한다.
people.maxByOrNull { p: Person -> p.age }
people.maxByOrNull { p -> p.age } // 파라미터 타입을 컴파일러가 추론
- 로컬 변수처럼 컴파일러는 람다 파라미터의 타입도 추론할 수 있다.
- it을 너무 남용하는 것보다는 파라미터를 명시하는 편이 낫다.
val getAge = { p: Person -> p.age }
people.maxByOrNull { getAge}
- 람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문백이 존재하지 않는다.
fun main() {
val sum = { x: Int, y: Int ->
println("Computing $x and $y")
x + y
}
}
- 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결괏값이 된다.
- 명시적인 return이 필요하지 않다.
🔖 5.1.4 현재 영역에 있는 변수 접근
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach {
println("$prefix $it")
}
}
forEach람다는 자신을 둘러싼 영역에 정의된 prefix 변수와 다른 변수에 접근할 수 있다.- 파일 영역에 이를 때까지 자신을 둘러싼 영역의 변수를 참조할 수 있다.
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {
if (it.startsWith("4")) {
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
}
- 코틀린과 자바 람다의 다른 점 중 한가지는 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이다.
- 람다 안에서 접근할 수 있는 외부 변수를 람다가 캡처한 변수라고 부른다.
- 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝난다.
- 파이널 변수 캡처 -> 람다 코드를 변수 값과 함께 저장
- 파이널이 아닌 변수를 캡처 -> 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장
🔖 5.1.5 멤버 참조
val getAge = Person::age
- ::을 사용하는 식을 멤버 참조라고 부른다.
- 한 메서드를 호출하거나 한 프로퍼티에 접근하는 함수 값을 만들어준다.
- 참조 대상이 함수인지 프로퍼티인지와는 관계없이 멤버 참조 뒤에는 괄호를 넣으면 안 된다.
fun salute() = println("Salute")
fun main() {
run { ::salute }
}
- 최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다.
fun main() {
val createPerson = ::Person
}
- 생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.
- 확장 함수도 똑같은 방식으로 참조할 수 있다.
🔖 5.1.6 값과 엮인 호출 가능 참조
fun main() {
val seb = Person("Sebastian", 26)
val personsAgeFunction = Person::age // 사람이 주어지면 나이를 돌려주는 멤버 참조
println(personsAgeFunction(seb)) // 사람을 인자로 받음
val sebsAgeFunction = seb::age // 특정 사람의 나이를 돌려주는, 값과 엮인 호출 가능 참조
println(sebsAgeFunction) // 파라미터 지정하지 않아도 됨
}
- 특정 객체 인스턴스에 대한 메서드 호출에 대한 참조를 만들 수 있다.
📖 5.2 자바의 함수형 인터페이스 사용: 단일 추상 메서드
함수형 인터페이스 or 단일 추상 메서드 인터페이스
- 인터페이스 안에 추상 메서드가 단 하나
🔖 5.2.1 람다를 자바 메서드의 파라미터로 전달
void postponeComputation(int delay, Runnable computation) {}
postponeComputation(1000) { println(42) }
- 컴파일러는 자동으로 Runnable의 인스턴스로 변환해준다.
- 익명 클래스의 인스턴스를 만들어줌
postponeComputation(1000, object : Runnable) {
override fun run() {
println(42)
}
}
- 명시적으로 선언하면 호출할 때마다 새 인스턴스가 생기지만 람다를 사용하면 람다에 해당하는 익명 객체가 재사용된다.
- 람다를 inline 표시가 돼 있는 코틀린 함수에 전달하면 익명 클래스가 생성되지 않는다.
🔖 5.2.2 SAM 변환: 람다를 함수형 인터페이스로 명시적 변환
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
fun main() {
createAllDoneRunnable().run()
}
- SAM 생성자는 컴파일러가 생성한 함수로 람다를 단일 추상 메서드 인터페이스의 인스턴스로 명시적으로 변환
- SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다.
- SAM 생성자는 하나의 인자만을 받아 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환
val listener = OnClickListener {view -> // 람다를 사용해 SAM 생성자 호출
val text = when (view.id) {
button1.id -> "First button"
button2.id -> "Second button"
else -> "Unknown button"
}
toast(text)
}
- 값을 반환할 때 외에 람다로 생성한 함수형 인터페이스 인스턴스를 변수에 저장해야 하는 경우에도 SAM 생성자 사용
- 람다에는 익명 객체와 달리 인스턴스 자신을 가리키는
this가 없다.- 람다 안에서
this는 그 람다를 둘러싼 클래스의 인스턴스를 가리킨다.
- 람다 안에서
- SAM 변환을 컴파일로가 자동으로 수행할 수 있지만 가끔 오버로드한 메서드 중에서 어떤 타입의 메서드를 선택해 람다를 변환해 넘겨줘야 할지 모호한 때에는 명시적으로 SAM 생성자 적용하면 된다.
📖 5.3 코틀린에서 SAM 인터페이스 정의: fun interface
fun interface IntCondition {
fun check(i: Int): Boolean
fun checkString(s: String) = check(s.toInt())
fun checkChar(c: Char) = check(c.digitToInt())
}
fun interface라고 정의된 타입의 파라미터를 받는 함수가 있을 때 람다 구현이나 람다에 대한 참조를 직접 넘길 수 있다.
fun checkCondition(i: Int, condition: IntCondition): Boolean {
return condition.check(i)
}
fun main() {
checkCondition(1) { it % 2 != 0} // 람다 직접 사용
val isOdd: (Int) -> Boolean = { it % 2 != 0 }
checkCondition(1, isOdd) // 시그니처가 일치하는 람다에 대한 참조 사용
}
- 함수형 타입 시그니처로 표현할 수 없는 연산이나 더 복잡한 계약을 표현하려면 함수형 인터페이스가 좋은 선택
📖 5.4 수신 객체 지정 람다: with, apply, also
- 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출할 수 있게 하는 것
🔖 5.4.1 with 함수
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\nNow I know the alphabet!")
return result.toString()
}
- 매번 result라는 이름을 반복 사용
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) { // 메서드를 호출하려는 수신 객체를 지정
for (letter in 'A'..'Z') {
this.append(letter)
}
this.append("\nNow I know the alphabet!")
this.toString()
}
}
- 위 예시의 파라미터는 두개다.
- stringBuilder와 람다
- with 함수는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다.
fun alphabet(): String {
val stringBuilder = StringBuilder()
return with(stringBuilder) { // 메서드를 호출하려는 수신 객체를 지정
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
}
- 람다 안에서는 명시적인 this 참조를 사용해 그 수신 객체에 접근할 수 있다.
- this를 없애고 메서드나 프로퍼티 이름만 사용해 접근 가능
fun alphabet() = with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
toString()
}
- 식을 바로 반환
🔖 5.4.2 apply 함수
fun alphabet() = StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}.toString()
- apply는 항상 자신에 전달된 객체(수신 객체)를 반환한다.
fun createViewWithCustomAttributes(context: Context) =
TextView(context).apply {
text = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}
- apply를 객체 초기화에 활용할 수 있다.
- java의 builder와 비슷하다.
fun alphabet() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\nNow I know the alphabet!")
}
buildString함수는StringBuilder를 활용해String을 만드는 경우 사용할 수 있는 우아한 해법buildList,buildSet,buildMap
🔖 5.4.3 객체에 추가 작업 수행: also
fun main() {
val fruits = listOf("Apple", "Banana", "Cherry")
val uppercaseFruits = mutableListOf<String>()
val reversedLongFruits = fruits
.map { it.uppercase() }
.also { uppercaseFruits.addAll(it) }
.filter { it.length > 5 }
.also { println(it) }
.reversed()
}
- 수신 객체에 대해 어떤 동작을 수행한 후 수신 객체 반환
- also의 람다 안에서는 수신 객체를 인자로 참조
- 디폴트로 it
- 원래의 수신 객체를 인자로 받는 동작을 실행할 때 also가 유용하다.
