5.1 람다 식과 멤버 참조
5.1.2 람다와 컬랙션
람다가 없을 때
data class Person(val name: String, val age: Int)
fun findTheOldest(peoples: List<Person>) {
var maxAge = 0
var theOldest: Person? = null
for(person in peoples) {
if(person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
println(theOldest)
}
fun main(args: Array<String>) {
val peoples = listOf(Person("soungho", 14), Person("sejin", 50))
findTheOldest(peoples)
}
람다를 사용
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val peoples = listOf(Person("soungho", 14), Person("sejin", 50))
println(peoples.maxByOrNull {it.age})
}
- 람다를 적극적으로 활용한 코드는 일반적으로 더 짧고 이해하기 쉬우며, 실수할 여지도 낮다.
- 코틀린은 간결한 코드 작성을 위한 람다 함수를 많이 지원하니 적극적으로 사용해 보자
5.1.3 람다식의 문법
peoples.maxByOrNull {it.age}의 변천사
fun main(args: Array<String>) {
val peoples = listOf(Person("soungho", 14), Person("sejin", 50))
println(peoples.maxByOrNull({p:Person -> p.age})) // <- 가장 넓게 풀어진 형태
println(peoples.maxByOrNull() {p:Person -> p.age}) // <- 가장 마지막에 람다 인자가 올 경우 메소드 밖으로 이동 가능
println(peoples.maxByOrNull{p:Person -> p.age}) // <- 람다가 유일한 인자인 메소드는 소괄호 생략 가능
println(peoples.maxByOrNull{p -> p.age}) // <- 람다의 인자가 추론 가능하면 생략 가능
println(peoples.maxByOrNull{it.age}) // <- it 으로 대치 가능
}
- it 사용 시 주의사항
- it을 사용하는 관습은 코드를 간결하게 한다. 하지만 이를 남용하면 안 된다.
- 특히, 람다 안에 람다가 중첩되는 경우에는 각 람다 파라미터를 명시하는 것이 좋다.
- 중첩된 경우 it을 사용하면 이 it 이 어떤 것을 가리키는지 명시적이지 않기 때문이다.
- 문맥상 람다 파라미터의 의미나 타입을 쉽게 알 수 있는 경우에만 it을 사용하자
5.1.4 현재 영역에 있는 변수에 접근(클로저)
- 자바의 경우 무명 내부 클래스를 정의할 때, 메서드의 로컬 변수를 무명 내부 클래스 내부에서 사용할 수 있었다.
- 단, 람다 밖의 변수는 final 이여야 한다는 제약이 있었다.
- 즉, 람다 밖에 선언된 변수를 람다 안에서 참조는 가능하지만 변경은 허용하지 않았다.
- 코틀린에 람다에서는, final 이 아니어도 람다 내부에서 람다 밖의 지역변수를 참조하고 심지어 값을 바꿀 수도 있게 되었다. -> 클로저 개념 도입
- 클로저: 함수를 쓸모 있는 1급 시민으로 만드는데 필수 요소
- 이로 인해 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있다.
fun lamba1(): () -> Unit {
var x = 3
return {
x = 7 // <- 람다 밖에 있는 x 변수를 포획
println("x is $x") // => 이로인해 lamba1 메소드 밖에서도 람다 호출이 가능!
// => lamba1 밖에서도 x 변수는 살아있을 수 있음에 주의해야 한다
}
}
fun main(args: Array<String>) {
val lam = lamba1()
lam()
}
- 어떻게 이걸 코틀린에서 가능하게 했을까?
- 기본적으로 final 지역변수는 람다 내부에서 접근 가능하다고 했다.
- final 이 아닌 변수를 내부적으로 wrapper class를 만들어서 감싸고 이를 final로 만들어 람다 내부에서 접근할 수 있도록 트릭을 사용했다.
- final wrapper class는 변경이 불가하지만 그 속에 실제 value는 final 이 아니라 변경이 가능하고 코틀린 내부적으로 이걸 사용해 값을 변경시키는 것을 통해 클로저를 구현했다.
5.2 컬렉션 함수형 API
- 코틀린만의 특징적인 함수형 API는 없다.
- 모두 C#, 그루비, 스칼라 등의 함수형 패러다임이 있는 언어에서 일반적으로 사용되는 함수형 API와 거의 동일한 내용이다.
- 함수형 API는 간결하지만 그만큼 축약된 형태이기 때문에 간혹 의도와 다른 비효율적인 코드가 나올 수 있어서 주의가 필요하다.
- 필요할 때 찾아서 쓰자..
5.3 지연 계산(lazy) 컬렉션 연산
- 배경
- 지금까지 위에서 언급한 map이나 filter와 같은 API는 결과 컬렉션을 즉시(eagerly) 생성한다.
- 이는 컬렉션 관련 함수를 연쇄하면 매 단계마다 임시 컬렉션이 생성됨을 의미한다.
- 컬렉션의 원소가 적을 때는 별 차이가 없지만, 백만 개 정도로 원소가 많을 때는 매우 비효율적일 수 있다.
- 시퀀스를 사용하면 이러한 비효율을 줄일 수 있다.
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val peoples = listOf(Person("soungho", 14), Person("sejin", 50))
peoples.map(Person::name).filter{ it.startsWith("A") }
peoples.asSequence() // <- 시퀸스 생성
.map(Person::name) // <- 시퀀스도 컬렉션 API 와 동일한 API 제공
.filter { it.startsWith("A") }
.toList()
}
- 시퀀스를 사용하면 중간 결과를 저장하는 컬렉션이 생성되지 않아서 원소가 많은 경우 성능이 눈에 띄게 좋아진다.
- 시퀀스가 내부적으로 어떻게 동작하기에, 이런 최적화가 가능한 걸까??? -> 지연계산 방법 사용
- 코틀린 지연 계산 시퀀스는 Sequence 인터페이스로부터 시작한다.
- 이 인터페이스는 단지 한 번에 하나씩 열거될 수 있는 원소의 시퀀스를 표현한다.(-> 내부에 iterator() 메서드 하나뿐임)
- Sequence의
- 아래에서 더 자세히 다룬다.
5.3.1 시퀀스 연산: 중간 연산과 최종 연산
- 시퀀스의 연산은 중간 연산과 최종 연산으로 나뉜다.
- 중간 연산
- 또 다른 시퀀스를 반환하는 연산
- 그 시퀀스는 원래의 시퀀스의 원소를 변환하지는 않고 변환하는 방법만 알 고 있다.
- 중간 연산은 항상 최종 연산까지 지연된다.
- 최종 연산
- 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터, 일련의 계산을 수행해 얻은 컬렉션이나 원소, 숫자 또는 객체를 반환
- 즉시 계산과 지연 계산의 개념적 차이
- 코틀린의 시퀀스는 자바의 스트림 API와 동작 방식이 같다.
5.4 자바 함수형 인터페이스 활용
- 코틀린은 람다를 잘 지원하지만, 코틀린으로 개발하면서 자바 API를 다뤄야 하는 경우도 많다.
- 자바는 엄밀하게 말하면 함수 타입이 존재하지 않는다. 함수형 인터페이스만 존재할 뿐이다.
- 함수형 인터페이스란, 하나의 인터페이스에 하나의 메서드만 정의된 형태를 말한다.(단일 추상 메서드)
- 함수형 인터페이스를 인자로 받는 메서드를 호출할 때, 자바 8 이전까지는 익명 내부 클래스를, 자바 8부터는 람다를 넘길 수 있으나 람다는 내부적으로 함수형 인터페이스를 구현한 익명 클래스로 변환되어 동작했다.
- 코틀린은 이와 달리, 명확하게 함수 타입이 존재한다.
- 그리고 함수형 인터페이스를 인자로 받는 자바 API를 호출할 때 함수타입(람다)을 넘겨줘도 코틀린 컴파일러에 함수형 인터페이스에 대한 구현체로 변환되어 호환성이 유지된다.
- 단, 코틀린 코드에서 만들어진 함수형 인터페이스를 인자로 받는 메서드는 람다로 호출할 수 없다.
- 코틀린 함수를 작성할 때, 함수형 인터페이스가 아니라 명확하게 함수 타입으로 인자를 선언해야 한다.
interface TestRunnable{
fun run()
}
fun test1(runnable: TestRunnable) {
runnable.run()
}
fun test2(runnable: Runnable) {
runnable.run()
}
fun main() {
test1 { println("hello") } // <- Type mismatch. Required: TestRunnable Found: () → Unit
test2 { println("hello") }
}
- 아래에서 함수형 인터페이스를 인자로 받는 자바 메서드에 코틀린 람다를 넘기면 어떻게 동작하는지 알아보자
5.4.1 자바 메서드에 람다를 인자로 전달
public class Test {
public static void postPhoneComputation(int delay, Runnable runnable){
runnable.run();
}
}
fun main() {
/***
* case 1:
*/
Test.postPhoneComputation(1000) { println("hello")} // <- 포획 변수가 없음(외부 조건에 상관없이 항상 동일하게 동작
/***
* case 2:
*/
Test.postPhoneComputation(1000, object : Runnable {
override fun run() {
println("hello")
}
})
/***
* case 3:
*/
val someVal = 3
Test.postPhoneComputation(1000) { println(someVal)} // <- 포획 변수가 있음(외부 조건에 따라 달라져야함)
}
case1: 자바 함수형 인터페이스 인자에 포획 변수 없이 람다를 넘기는 경우
- 컴파일러에 의해 내부적으로 람다를 본문으로 하는 인터페이스의 구현체를 넘겨줌
- 외부 환경에 상관없이 람다의 동작이 항상 동일
- 구현체를 함수 호출 때마다 만들지 않고 하나 만들어두고 재사용하는 방식(컴파일러 수준 최적화)
case2: 자바 함수형 인터페이스 인자에 포획 변수 없이 함수형 인터페이스를 구현한 객체 식을 넘기는 경우
- 위에서 언급한 컴파일러 최적화 없이 항상 구현체를 생성해서 인자로 전달
- 문법적인 간결함이나, 성능 두 가지 다 객체식보단 람다를 넘기는 게 효과적이다
case3: 자바 함수형 인터페이스 인자에 포획 변수가 있는 람다를 넘기는 경우
- 외부 환경에 의해 람다 동작이 달라질 수 있음
- 따라서, 함수 호출 시 마다 구현체 생성해서 전달됨
지금까지 코틀린 람다 <-> (함수형 인터페이스의 구현체) <-> 자바 함수형 인터페이스 사이의 자동 변환되는 경우에 대해 알아봤다.
이제 코틀린에서 이 변환을 명시적으로 해 줘야 하는 경우에 대해 알아보자
5.4.2 SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경
함수형 인터페이스를 리턴하는 경우 람다를 직접 반환할 수 없고 반환하고 싶은 람다를 SAM 생성자로 감싸줘야 한다.
fun createAllDoneRunnable1(): Runnable {
return { println("All done")} // Type mismatch. Required: Runnable Found: () → Unit
// 일반 람다는 코틀린 컴파일러가 해석 불가
}
fun createAllDoneRunnable2(): Runnable {
return Runnable{ println("All done")} // <- SAM 생성자 호출: 마치 익명 내부 클래스를 생성하는것과 유사
// <- 함수형 인터페이스의 유일한 추상 메소드의 본문에 사용할 람다만을 인자로 받아서
// <- 그 함수형 인터페이스를 구현한 인스턴스를 반환
}
fun createAllDoneRunnable3(): Runnable {
return object : Runnable{
override fun run() {
println("All done")
}
} // <- SAM 생성자 대신 객체 식을 사용해 함수형 인터페이스의 구현체를 반환할 수도 있다.
// <- 요 방법보단 SAM 생성자 사용을 추천한다(문법적으로 더 간결하기 때문)
}
5.5 수신 객체 지정 람다: with, apply
수신 객체 지정 람다
- 람다 안에서 미리 정해둔 수신 객체의 메서드를 사용가능
- 일반 람다와 수신 객체 지정 람다의 관계는 일만 함수화 확장함수의 관계와 유사
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
- 둘 다 T 타입으로 수신 객체의 타입을 제네릭하게 받고 있음
- 메서드는 T 타입으로 되어있지만 메서드 호출부에 있는 수신 객체와 컴파일 타임에 바인딩되어 수신객체 타입에 해당하는 모든 프로퍼티와 메서드를 참조 가능
- with 은 전달된 람다의 결과를 반환, apply 는 전달된 수신 객체를 반환
'kotlin' 카테고리의 다른 글
kotlin data class 와 jackson 역직렬화 & spring과의 통합 (0) | 2023.03.26 |
---|---|
[Kotlin in Action] 6장: 코틀린 타입 시스템 > 6.1 널과 관련한 이야기 (0) | 2023.03.18 |
[Kotlin in Action] 4장: 클래스, 객체, 인터페이스 (0) | 2023.02.11 |
[Kotlin in Action] 3장: 함수의 정의와 호출 (0) | 2023.02.08 |
[kotlin-tip] 코프링 환경에서 로깅하기 (0) | 2023.01.14 |
댓글