람다 식과 멤버 참조

람다 소개: 코드 블록을 함수 인자로 넘기기

  • ‘이벤트가 발생하면 이 핸들러를 실행하자’나 ‘데이터 구조의 모든 원소에 이 연산을 적용하자’와 같은 생각을 코드로 표현하기 위해서 예전에는 무명 내부 클래스를 사용했다.
  • 하지만 이는 코드가 너무 번잡스러워졌고, 자바 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을 남용하면 안된다. 람다 안에 람다가 중촙되는 경우 각 람다의 파라미터를 명시하는 편이 낫다.
people.maxBy { it.age }
  • 람다의 본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 된다.
println("Computing the sum of $x and $y...")

현재 영역에 있는 변수에 접근

  • 람다는 람다 밖에 있는 변수에 접근할 수 있다.
  • 코틀린은 자바와 다르게 람다 안에서 파이널 변수가 아닌 변수에도 접근하고 변경할 수 있다.
    • 변경 가능한 변수를 필드로 하는 클래스를 선언하는 기법을 사용한다.
  • 람다가 포획(capture)한 변수: 람다 안에서 사용하는 외부 변수

멤버 참조

  • 멤버 참조: 이중 콜론(::)을 사용하여 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어주는 것
Person::age
  • 최상위에 선언된 함수나 프로퍼티를 참조할 수도 있다.
    • REPL에서는 불가능하다.
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 })
  • 필터와 변환 함수를 맵에 적용할 수도 있다.
    • 맵의 경우 키와 값을 처리하는 함수가 따로 존재한다.
    • filterKeysmapKeys는 키를 걸러내거나 변환한다.
    • filterValuesmapValues는 값을 걸러 내거나 변환한다.

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

  • all함수는 모든 원소가 주어진 람다(Predicate)를 만족하는지 반환한다.
  • any함수는 주어진 람다(Predicate)를 만족하는 원소가 하나라도 있는지 반환한다.
    • !allany가 같고, !anyall이 같지만 가독성을 높이려면 anyall 앞에 !를 붙이지 않는 편이 낫다.
  • 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

  • 코틀린 표준 라이브러리의 withapply는 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있게할 수 있다.

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!")  
}