기록하는 습관

[Kotlin In Action]5장 - 람다로 프로그래밍 본문

스터디/Kotlin In Action

[Kotlin In Action]5장 - 람다로 프로그래밍

로그뉴 2023. 2. 27. 22:37

** 이 글은 Kotlin In Action을 읽고 정리한 글입니다. **

 

1. 람다 식과 멤버 참조

1-3. 람다 식의 문법

{ x:Int, y:Int -> x + y }
  • 코틀린 람다 식은 항상 중괄호로 둘러싸여 있다.
  • 화살표(->)가 인자 목록과 람다 본문을 구분한다.

실행 시점에 코틀린 람다 호출에는 부가 비용이 들지 않으며, 프로그램의 기본 구성 요소와 비슷한 성능을 낸다.

 

1) 함수 호출 시 맨 마지막 인자가 람다식이라면 이를 괄호 밖으로 빼낼 수 있다.

people.maxBy() { p -> p.age }

 

2) 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다.

people.maxBy { p -> p.age }

 

3) 람다의 파라미터가 하나뿐이고 컴파일러가 타입을 추론할 수 있다면 it을 바로 쓸 수 있다.

people.maxBy { it.age }
  • 람다를 변수에 저장할 때는 파라미터 타입을 추론할 문맥이 존재하지 않으므로, 파라미터 타입을 명시해야 한다.

 

1-4. 현재 영역에 있는 변수에 접근

람다가 포획한 변수

: 람다 안에서 사용하는 외부 변수를 람다가 포획(capture)한 변수라고 부른다.

 

closure

: 닫힌 객체 그래프와 람다 코드를 저장하는 데이터 구조. 람다를 실행 시점에 표현하는 데이터 구조는 람다에서 시작하는 모든 참조가 포함된 닫힌 객체 그래프를 람다 코드와 함께 저장해야 한다.

 

  • 람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용 가능
  • final 변수 포획한 경우: 람다 코드를 변수 값과 함께 저장
  • final이 아닌 변수를 포획한 경우: 특별한 래퍼로 감싸고 래퍼에 대한 참조를 람다 코드와 함께 저장한다.

 

자바와 다른 점

  • final 변수가 아닌 변수에 접근할 수 있다.
  • 람다 안에서 바깥의 변수를 변경해도 된다.

 

1-5. 멤버 참조

멤버 참조는 프로퍼티나 메서드를 단 하나만 호출하는 함수 값을 만들어준다.

::를 사용하는 식을 멤버 참조라고 부른다.

 

people.maxBy(Person::age)

 

최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다. 이 때, 클래스를 생략하고 ::로 참조를 바로 시작한다.

fun salute() = println("Salute!")  
  
fun main(args: Array<String>) {  
    run(::salute)  
}

 

생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.

:: 뒤에[ 클래스 이름을 넣으면 생성자 참조를 만들 수 있다.

val createPerson = ::Person  
val p = createPerson("Alice", 29)  
println(p)

 

바운드 멤버 참조를 사용하면 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출해준다. 따라서 호출 시 수신 대상 객체를 별도로 지정해 줄 필요가 없다.

val p = Person("name", 34)  
val personAgeFunction = Person::age
println(personAgeFunction(p))

val dmitrysAgeFunction = p::age  // 바운드 멤버 참조
println(dmitrysAgeFunction())

 

 

 

2. 컬렉션 함수형 API

2-1. 필수적인 함수: filter와 map

  • filter: 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
  • map: 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만든다.
val list = listOf(1, 2, 3, 4)  
println(list.filter { it % 2 == 0 })
 
val people = listOf(Person("Alice", 29), Person("Bob", 31))  
println(people.map { it.name })  // people.map(Person::name)

 

필요 없는 반복 계산 줄이기

// 매 번 최댓값 연산을 실행
people.filter { it.age == people.maxBy(Person::age)!!.age }

// 한 번만 최댓값 연산 실행
val maxAge = people.maxBy(Person::age)!!.age
people.filter{ it.age == maxAge }

 

2-2. all, any, count, find: 컬렉션에 술어 적용

  • all, any: 특정 조건이 모두 일치하는지 또는 일부 일치하는지 판단
  • count: 조건을 만족하는 원소의 개수를 반환
    • size를 사용하게 되면 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생기므로 count 사용 권장.
  • find: 조건을 만족하는 첫 번째 원소를 반환. 없으면 null 반환 (firstOrNull 함수와 동일)

 

2-3. groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경

  • 컬렉션의 원소를 구분하는 특성이 KEY이고, 키 value에 따른 각 그룹이 값인 map이다.
  • 각 그룹은 List이다.

 

2-4. flatMap과 flatten: 중첩된 컬렉션 안의 원소 처리

flatMap 함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고, 람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 모은다.

val strings = listOf("abc", "def")  
println(strings.flatMap { it.toList() })  // 1. toList 2. flatMap

 

 

3. 지연 계산(lazy) 컬렉션 연산

3-1. 시퀀스 연산 실행: 중간 연산과 최종 연산

시퀀스는 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산을 연쇄할 수 있다.

  • Sequence라는 인터페이스를 사용한다.
  • 중간 컬렉션을 생성하지 않으므로 원소가 많은 경우 성능이 좋다.
  • 지연 계산: 최종 연산이 호출될 때 계산이 수행된다.
  • asSequence 확장함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다.
listOf(1, 2, 3, 4).asSequence()  // 시퀀스로 변환
            .map { print("map($it) "); it * it }  
            .filter { print("filter($it) "); it % 2 == 0 }
            .toList()  // 다시 리스트로 변환

 

시퀀스는 모든 연산은 각 원소에 대해 순차적으로 적용된다. 즉시 계산은 전체 컬렉션에 연산을 적용하지만 지연 계산은 원소를 한 번에 하나씩 처리한다.  filter와 map 순서가 성능에 영향을 미친다.

 

참고) 자바 8에 stream과 개념이 같다. 병렬 기능을 사용하고 싶다면 스트림 연산을 사용해라.

 

 

3-2. 시퀀스 만들기

generateSequence 함수 사용

val naturalNumbers = generateSequence(0) { it + 1 } 
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }  
println(numbersTo100.sum())

sum() 최종 연산이 호출될 때 실행된다.

 

 

4. 자바 함수형 인터페이스 활용

코틀린에서 함수형 인터페이스를 인자로 받는 Java함수를 호출 할 경우 인터페이스 객체 대신 람다를 넘길 수 있다.

4-1. 자바 메서드에 람다를 인자로 전달

자바에서 메서드에 람다를 넘기기 위해 무명 클래스의 인스턴스를 만들어 넘기는 방식으로 사용했었지만 코틀린에서는 무명 클래스 인스턴스 대신에 람다를 넘기는 것이 가능하다. 

 

 

4-2. SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경

  • SAM이란? Single abstract method - 단일 추상 메소드 
  • SAM 인터페이스란, 추상 메소드가 단 1개 있는 인터페이스로 '함수형 인터페이스' 라고도 한다.
  • SAM 생성자란, 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수.
// 1. 자바에서 interface 정의 후 kotlin에서 sam 생성자 사용
public interface SAM{
    void a();
}
val test = SAM { println("Test") }

// 2. SAM interface에 b 함수 추가하면 컴파일 에러 발생
public interface SAM{
    void a();
    void b();
}
val test = SAM { println("Test") }

 

SAM 생성자 사용 예시

// 1. SAM 생성자 사용 X
val obejctInstance = object : View.OnClickListener{
    override fun onClick(v: View?) {
        println("onClick")
    }
}

// 2. SAM 생성자 사용 O
val lambdaInstance = View.OnClickListener { println("onClick") }

 

5. 수신 객체 지정 람다: with와 apply

수신 객체 지정 람다란 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출하는 것을 말한다.

5-1. with 함수

with 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 만든다.

 

fun <T, R> with(receiver: T, block: T.() -> R): R

T.()를 람다 리시버라고 하는데, 입력을 받으면 함수 내에서 this를 사용하지 않고도 입력받은 객체(receiver)의 속성을 변경할 수 있다.

 

 

val person = Person("Bob", 20)
with(person) {
    println(name)
    println(age)
}
  • with(T) 타입으로 Person을 받으면 {} 내의 블럭 안에서 곧바로 name 이나 age 프로퍼티에 접근할 수 있다.
  • with는 non-null의 객체를 사용하고 블럭의 return 값이 필요하지 않을 때 사용한다.
  • 주로 객체의 함수를 여러 개 호출할 때 그룹화하는 용도로 활용됨.

 

5-2. apply 함수

apply는 객체를 만들면서 인스턴스를 초기화 하고 싶을 때 사용한다.

 

fun <T> T.apply(block: T.() -> Unit): T

T의 확장 함수이고, 블럭 함수의 입력을 람다 리시버로 받았기 때문에 블럭 안에서 객체의 프로퍼티를 호출할 때 it이나 this를 사용할 필요가 없다.

 

val person = Person("", 0)
val result = person.apply {
    name = "Bob"
    age = 20
}

println("$person")
  • apply는 확장 함수로 정의 되어 있다.
  • 항상 자신에게 전달된 객체를 반환하는 점이 with와 다르다.
Comments