반응형
람다식 또는 람다
- 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각
- 람다를 사용하면 쉽게 공통 코드 구조를 뽑아낼 수 있음
- 함수를 값처럼 다루는 접근 방법
- 무명 클래스의 역할을 대신 함
- 람다는 함수에 인자로 넘어갈때 정의해서 바로 사용함
- 실행시점의 코틀린 람다 호출은 아무 비용도 들지 않는다
람다의 사용처
- 컬렉션 처리
- 자바 라이브러리에 람다 함께 사용
- 수신 객체 지정 람다
람다 소개 : 코드 블록을 함수 인자로 넘기기
컬레션 처리에서 람다 없이 자바에서 처리하는 방법
data class Person(val name: String, val age: Int)
/* Java */
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)
}
위 코드를 코틀린 람다로 리팩토링 해보자
data class Person(val name: String, val age: Int)
fun main(args: Array<String>) {
val people = listOf(Person("Alice", 29), Person("Bob", 31))
findTheOldest(people)
}
/* Kotlin */
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age }) //나이 프로퍼티를 비교해서 값이 가장 큰 원소 찾기
>>> println(people.maxBy { Person::age }) //멤버 참조를 사용해도 가능
Person(name=Bob, age=31)
- maxBy는 모든 컬렉션에서 사용할 수 있는 최대값을 구해주는 함수
- maxBy는 비교에 사용할 값을 돌려주는 함수를 인자로 받는다.
- 중괄호에 둘러싸인 코드 {it.age} 를 비교에 사용할 값으로 넘겨준다. (it은 그 인자를 가리킴)
- 함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로도 대치할 수 있음
람다 식의 문법
- 람다 식은 항상 중괄호로 둘러싸여 있다
- 인자 목록 주변엔 괄호가 없다
- 화살표 (->) 가 인자 목록과 람다 본문을 구분해준다
- 람다식을 변수에 저장 할 수 있다 (변수 이름 뒤에 괄호를 놓고 그 안에 필요한 인자를 넣어서 람드를 호출할 수 있음)
- val sum = { x : Int, y : Int -> x + y }
- println(sum(1, 2)) 출력3
- 원한다면 람다식을 직접 호출도 가능
- { println(42) }() 출력42
- run은 인자로 받은 람다를 실행할 수 있는 함수
- run { println(42) } 출력42
- 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입은 생략 가능
- 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다 (default name은 it, but 중첩 람다식이면 it 자제, 파라미터 명시)
- 맨 끝 인자가 람다식이면 람다식을 괄호 밖으로 빼낼 수 있는 관습이 존재
- 하나밖에 없는 인자가 람다식이면 람다식을 괄호 밖으로 빼고, 빈 괄호 생략 가능
people.maxBy({ p: Person -> p.age }) //정식으로 람다 작성
people.maxBy(){ p: Person -> p.age } //마지막 인자 람다식이면 괄호 밖으로 뺄 수 있는 관습
people.maxBy{ p: Person -> p.age } //인자가 하나고 괄호 밖으로 람다식을 썼다면, 빈 괄호 생략 가능
people.maxBy{ p -> p.age } //똑똑한 컴파일러가 문맥으로 부터 타입 추론 가능해서 타입 생략
people.maxBy{ it.age } //인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다 (default name은 it)
람다의 변수 접근
- 람다를 함수 안에서 정의하면 함수의 파라미터, 람다 정의 앞에 선언된 로컬 변수까지 람다에서 접근 가능
- 람다는 자바와 달리 final이 아닌 변수에 접근 및 변경 가능
- 람다 안에서 사용하는 외부 변수를 '람다가 포획한 변수'라고 칭함
- 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝나지만, 변수포획을 당하면 로컬변수의 생명주기가 달라질 수 있음 (즉 함수는 끝난 뒤에도 포획 당한 변수를 읽거나 쓸 수 있음)
- final 변수 : 람다 코드와 변수 값과 함께 저장
- final 아닌 변수 : 변수를 특별한 래퍼로 감싸고(읽고/쓸수있음), 해당 래퍼에 대한 참조를 람다 코드와 함께 저장
fun printProblemCounts(responses: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
responses.forEach {
if (it.startsWith("4")) {
clientErrors++ //람다 안에서 람다 밖의 변수를 변경
} else if (it.startsWith("5")) {
serverErrors++ //람다 안에서 람다 밖의 변수를 변경
}
}
println("$clientErrors client errors, $serverErrors server errors")
}
멤버 참조
- ::를 사용하는 식을 '멤버 참조' 라고 부른다
- 프로퍼티나 메서드를 단 하나만 호출하는 함수 값을 만들어 준다
- 클래스 :: 프로퍼티 또는 클래스 :: 메서드 형태로 사용
- 멤버 참조 뒤에는 괄호를 넣으면 안된다
people.maxBy{ p -> p.age} //람다표기
people.maxBy{ it.age} //람다표기
people.maxBy{ Person::age } //멤버 참조
fun limky() = println("Limky!!")
>>> run(::limky) // 최상위 함수를 참조, 클래스 이름 생략하고 ::으로 시작 가능
>>> Limky!!
fun Person.isAdult() = age >= 21 //확장함수 정의
val predicate = Person:: isAdult //확장함수에도 멤버 참조 가능
생성자 참조
- 멤버 참조과 동일하게 :: 사용한다.
- 클래스 생성 작업을 저장하거나, 생성 작업을 연기할 수 있다.
- :: 클래스 이름 형태로 사용
val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)
>>> Person(name=Alice, age=29)
컬렉션 함수형 API
필수적인 함수 filter
- 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
- 반환하는 원소를 모을땐 새로운 컬렉션을 생성
- 원치 않는 원소를 제거할 때 사용
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter{ it.age > 30 })
>>> [Person(name=Bob, age=31)]
필수적인 함수 map
- 원소를 변환하기 위해선 map 함수를 사용해야 함
- 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새로운 컬렉션을 만든다
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.map { it.name })
println(people.map(Person::name ) //멤버 참조 사용도 가능
>>> [Alice, Bob]
>>> [Alice, Bob]
filter 와 map 을 조합해서 사용하면?
컬렉션에서 나이가 31 이상인 사람의 이름을 알아보자..
people.filter{it.age > 30}.map(Person::name)
>>>[Bob]
이밖에도 map 을 처리하는 함수들이 있다
- 키 관련 함수(걸러내는 : filterKeys, 변환하는 : mapKeys)
- 값 관련 함수(걸러내느 : filterValues, 변환하는 : mapValues)
val numbers = mapOf(0 to "zero", 1 to "one")
val valuesMap = numbers.mapValues{ it.value.toUpperCase() }) //value값을 대문자로 변환한 후 새로운 컬렉션으로 반환
>>> {0=ZERO, 1=ONE}
all, any, count, find : 컬렉션에 술어 적용
- 술어 : 참 거짓을 반환하는 함수
- all : 모든 원소가 만족하면 true
- any : 하나의 원소라도 만족하면 true
- count : 만족하는 원소의 갯수를 리턴
- find : 만족하는 첫번째 원소를 반환, 없으면 Null (null 를 반환하는걸 명시적으로 표현하고 싶다면 firstOrNull)
val canBeInClub27 = { p: Person -> p.age <= 27 } //술어 생성
val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.all(canBeInClub27) // false
!people.all(canBeInClub27) // true
people.any(canBeInClub27) // true
!people.any(canBeInClub27) // false
people.filter(canBeInClub27).size // 1 (중간 컬렉션이 생겨서 비효율적)
people.count(canBeInClub27) // 1 (중간 컬렉션이 생기지 않아 효율적)
people.find(canBeInClub27) // 조건에 맞는 첫번째 객체 반환
groupBy : 리스트를 여러 그룹으로 이뤄진 맵으로 변경
- 컬렉션의 원소를 구분하는 특성을 가지고 각 그룹을 만든다
val list = listOf("a", "ab", "b")
println(list.groupBy(String::first)) //String의 확장함수, 멤버 참조를 통해 접근 가능
>>> {a=[a, ab], b=[b]}
flatMap 과 flatten : 중첩된 컬렉션 안의 원소 처리
flatMap
- 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고, 그 결과로 얻어지는 여러 리스트를 한 리스트로 만든다
val strings = listOf("abc", "def")
//toList 함수는 문자열에 적용하면, 문자열에 속한 문자들로 이루어진 리스트를 생성
println(strings.flatMap{ it.toList() })
>>> [a, b, c, d, e, f]
- flatMap 에서 저자로만 이뤄진 리스트를 만들고, toSet 을 통해 중복된 저자를 제거
class Book(val title: String, val author: List<String>)
fun main(args: Array<String>) {
val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
Book("Mort", listOf("Terry Pratchett")),
Book("Good Omens", listOf("Terry Pratchett",
"Neil Gaiman")))
println(books.flatMap { it.authors }.toSet() )// 저자들로만 리스트를 만들고, toSet을 통해 중복을 제거
}
>>>Jasper Fforde, Terry Pratchett, Neil Gaiman]
flatten
- 중첩된 컬렉션의 원소를 한 리스트로 모으기만 해줌
- 특별히 변환해야 할 내용이 없다면 사용 가능
지연 계산(lazy) 컬렉션 연산
- 즉시 계산 컬렉션 연산
- map, filter 연산은 결과 컬렉션을 즉시 생성
- 따라서, 매 단계마다 계산의 중간 결과를 새로운 컬렉션에 임시로 저장
- 지연 계산 컬렉션 연산
- 시퀀스를 통해서 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산 가능
- 시퀀스는 컬레션에서 제공하는 함수 동일하게 사용 가능
//즉시 계산
//map과 filter 각각 중간 컬렉션 생성됨 (총 2개)
people.map(Person::name).filter{ it.startsWith("A") }
//지연 연산
//시퀀스는 원소를 하나씩 각 단계별로 수행하는 방식이기 때문에 중간 컬렉션 생성하지 않음
//최종 결과만 컬렉션 생성
people.asSequence()
.map(Person::name)
.filter{ it.startsWith("A") }
.toList()
- 컬렉션 -> 시퀀스 변환
- asSequence() 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다
- 시퀀스 -> 컬렉션 변환
- 시퀀스를 이용한 결과는 시퀀스 형태이다.
- 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메서드가 필요하면 리스트로 변환 필요
시퀀스 연산 실행 : 중간 연산과 최종 연산
- 시퀀스는 중간 연산과 최종 연산으로 나뉜다
- 중간 연산은 시퀀스를 반환하며, 최초 시퀀스의 원소를 변환하는 방법을 안다 (*지연 계산이 수행 됨)
- 최종 연산은 결과를 반환하며, 중간 연산들을 실제로 수행하게 된다
- 시퀀스는 원소들이 연산이 순차적으로 수행된다.
// 최종 연산이 없어서 아무 내용도 출력되지 않음
listOf(1,2,3,4).asSequence()
.map{ print("map($it)"); it * it }
.filter{ print("filter($it)"); it % 2 == 0 }
/ .toList() 최종 연산 수행 시점에 map, filter동작도 수행
listOf(1,2,3,4).asSequence()
.map{ print("map($it)"); it * it }
.filter{ print("filter($it)"); it % 2 == 0 }
.toList() // 최종 연산
>>> map(1)filter(1)map(2)filter(4)map(3)filter(9)map(4)filter(16) //순차적인 원소 처리
지연 연산의 이점
지연 연산의 연산 특징은 결과를 만들어 내는데 불필요한 연산을 하지 않아도 된다.
println(listOf(1, 2, 3, 4).asSequence().map{ it * it }.find{ it > 3})
>>> 4
- 즉시 연산보다 지연 연산의 연산 횟수가 2번이나 적다. (지연 연산이 효율적)
- 시퀀스는 Java 8 의 Stream 과 매우 유사하게 동작함
- 스퀀스와 달리 Java 8 의 Stream 연산은 CPU에서 병렬실행도 가능하다!
스퀀스 만들기
- generateSequence() 함수를 사용해 스퀀스를 직접 생성할 수 있다
- 이 전의 원소를 인자로 받아서 다음 원소를 계산하는 시퀀스 생성
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
println(numbersTo100.sum())
자바 함수형 인터페이스 활용
- 자바 8 이전에는 특정 로직을 수행하기 위해서는 무명 클래스를 이용했다
- 코틀린에서는 무명 클래스 인스턴스 대신 람다를 넘길 수 있다
- 이게 가능한 이유는 SAM 인터페이스이기 때문이다
- SAM 인터페이스는 추상메소드가 단 하나만 있는 인터페이스 (ex Runnable, Callable)
//Java 8 이상
public interface OnClickListener{
void onClick(View v);
}
public class Button{
public void setOnClickListener(OnClickListener l)
}
button.setOnClickListener{ /* 클릭시 수행할 동작 */} // 람다로 인자를 전달
//Java 8 미만
button.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v){
/* 클릭시 수행할 동작 */
}
})
//코틀린
//무명 클래스 인스턴스를 정의하고 활용할 필요가 없어서 깔끔함
button.setOnClickListener( view -> ...) //void onClick(View v); 파라미터와 대응 함
자바 메서드에 람다를 인자로 전달
- 람다를 인자로 넘길때 컴파일러는 자동으로 람다를 무명클래스와 인스턴스를 생성해줌
- 명시적으로 객체 식을 넘기는 경우 메서드를 호출할 때마다 새로운 객체가 생성됨
- 하지만 변수 포획이 없는 람다를 넘기는 경우 메서드를 호출할 때 마다 동일 인스턴스를 사용
- 만약 명시적인 object 선언(객체 식)을 사용하면서 람다와 동일한 동일 인스턴스 사용을 하려면 전역으로 선언해야 함
//Java 메서드 정의
void postponeComputatuion(int delay, Runnable computation);
//Java 메서드에 람다를 인자로 전달 (동일한 Runnable 인스턴스 객체 사용)
void postponeComputatuion(1000) {println(42)}
//Java 메서드에 객체 식을 전달 (메서드 호출할 때마다 Runnable 새로운 객체 생성)
postponeComputation(1000, object : Runnable {
override fun run() {
println(42)
}
})
//Java 메서드에 객체 식을 전달 & 생성자를 통한 전역 선언
val runnable = Runnable{ println(42) }
fun handleComputation(){
postponeComputation(1000, runnable) //동일한 Runnable 인스턴스 객체 사용
}
//Java 메서드에 람다를 인자로 전달 & 메서드 영역 변수를 포획
fun handleComputation(id:String){
postponeComputation(1000) {println(id)} //변수 포획..매번 새로운 인스턴스 생성
}
SAM 생성자 : 람다를 함수형 인터페이스로 명시적으로 변경
- SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다
- 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다
- SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다
- SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메서드의 본문에 사용할 람다만 인자로 받아서, 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다
fun createAllDoneRunnable() : Runnable {
return Runnable { println("All done!") }
}
createAllDoneRunnable().run()
>>>All done!
수신 객체 지정 람다 : with 와 apply
- 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출 할 수 있게 하는 것
- with
- 객체 이름을 반복하지 않고 그 객체에 대해 다양한 연산을 가능케 함
- 람다 결과를 반환
- apply
- 람다의 결과 대신 수신 객체가가 필요한 경우에 사용 (객체 자체를 반환)
- with와 유사하지만, 항상 자신에게 전달된 객체를 반환한다
- 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화하는 경우에 사용
//with 사용하지 않음
fun alphabet() : String{
val result = StringBuilder()
for(letter in 'A'..'Z'){
result.append(letter) // result 중복
}
result.append("append!") // result 중복
return result.toString() // result 중복
}
// with 사용함 (람다 안에서 사용)
fun alphabet() : String{
val stringBuilder = StringBuilder()
return with(stringBuilder){ //람다를 괄호 밖으로 빼냄
for(letter in 'A'..'Z'){
this.append(letter) // this 사용하여 메서드 호출
}
append("append!") // this 생략, 메서드 호출
this.toString() // this 사용하여 결과 리턴
}
}
fun alphabet() = with(stringBuilder){ //람다를 괄호 밖으로 빼냄
for(letter in 'A'..'Z'){
append(letter)
}
append("append!")
toString() //마지막 식의 결과가 결과 값임
}
}
// apply 사용함
fun alphabet() = StringBuilder().apply { //apply의 수신 객체가 람다식 안에서 수신 객체가 된다
for(letter in 'A'..'Z'){
append(letter) // 함수 바로 접근
}
append("append!") // 함수 바로 접근
}.toString()
>>> ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> Now I know the alphabet!
반응형
'프로그래밍 > kotiln' 카테고리의 다른 글
클래스, 객체, 인터페이스(4장) (0) | 2023.02.19 |
---|---|
함수의 정의와 호출(3장) (0) | 2023.02.05 |
코틀린이란 무엇이며 왜 필요한가? (1장) (0) | 2023.01.15 |
댓글