해당 글은
Kotlin in Action
도서를 읽으며 정리한 내용입니다
[ 람다 & 람다식 ]
- 람다
- 다른 함수에 넘길 수 있는 작은 코드 조각
- 함수를 값처럼 다루는 접근방법 (함수는 직접 다른 함수에 전달 가능)
- 무명 내부 클래스로 일련의 동작을 변수에 저장하거나, 넘겼던 작업을 대신함
- 대부분 람다는 함수에 인자로 넘기면서 바로 정의해서 사용하는 경우가 많다
- 람다 식의 문법
- 파라미터와 본문으로 구성
->
를 통해서 인자 목록과 람다 본문을 구분- 항상 중괄호
{}
를 통해 둘러쌓여 있음- 실행 시점에 코들린 람다 호출은 아무 부가 비용이 들지 않는다
/* 람다식을 변수에 저장 */ val sum = { x: Int, y: Int -> x+y } /* 사용 */ println(sum(1,2)) /* 생성 후 즉시 사용 방법 (1) */ { println(42) }() /* 생성 후 즉시 사용 방법 (2) -> run을 통한 즉시 사용이 더 보기 좋다! */ run { println(42) }
- run은 인자로 받은 람다를 실행해주는 라이브러리
- 람다의 변형
- 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면
=> 괄호 밖으로 람다를 뺄 수 있는 관습이 존재
=> 람다가 어떤 함수의 유일한 인자이고, 괄호 뒤에 람다가 있다면 빈 괄호를 없앨 수 있다- 컴파일러가 문맥으로 유추할 수 있는 인자 타입을 생략해서 간단하게 표기할 수 있음
=> 파라미터가 여러개인 경우, 일부 타입만 지정해도 가능- 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다
=> default name인it
로 사용 가능
=> 람다가 중첩되는 경우에는 겹치기 때문에 명시적으로 선언하는 것이 좋다/* 원래 호출 */ people.maxBy({p: Person -> p.age}) /* 람다가 유일한 인자라서 괄호 밖으로 추출 */ people.maxBy(){p: Person -> p.age} /* 유일한 인자라서 빈 괄호 삭제 */ people.maxBy{p: Person -> p.age} /* 컴파일러가 문맥으로 타입을 추론할 수 있어서 타입 지정 X */ people.maxBy{p -> p.age} /* 인자가 하나뿐이라서, 이름 생략 후 default값이 it로 사용 */ people.maxBy{it.age} /* 변수에 람다를 저장할 때에는, 타입 추론 문맥이 없으므로 타입 명시 필요 */ val = getAge = { p: Person -> p.age } /* 본문이 여러 줄인 람다는 마지막 식이 결과(return)값을 의미 */ val sum = { x: Int, y: Int -> ... ... x + y // result값을 의미 }
[ 변수 포획 ]
- 변수 포획
- 람다 함수 안에서 외부 변수를 사용하는 것
- Java와 다르게 코틀린 람다는
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++ // 람다 내부에서 외부 필드를 접근 & 변경 } } }
- 변수 포획과 생명주기
- 함수 내부의 일반적인 로컬 변수의 생명 주기는 함수가 반환되면서 끝난다
- 하지만, 해당 로컬 변수를 람다가 포획하여 반환하거나, 다른 변수에 저장하면
=> 함수의 생명주기와 달라질 수 있다
=> 클로저(closure)- 생명 주기(life cycle)가 달라질 수 있는 원리
- final 변수를 포획
=> 람다 코드를 변수 값과 함께 저장- final이 아닌 변수를 포획
=> 특별한 래퍼(wrapper)로 감싼 뒤, 나중에 변경하거나 읽을 수 있게 한다
=> 감싼 래퍼의 참조를 람다 코드와 함께 저장
[ 멤버 참조 ]
- 멤버 참조
- 이중 콜론(
::
)을 통해서 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어 준다클래스 이름 :: 프로퍼티
or클래스 이름 :: 메소드
형식으로 사용- 최상위에 선언된 함수 / 프로퍼티를 참조할 수 있음
- 확장 함수에도 적용 가능
- Java 8의 메소드 레퍼런스와 유사
/* 람다 표기 */ people.maxBy{p -> p.age} people.maxBy{it.age} /* 멤버 참조 표기 */ people.maxBy(Person::age) /* 최상위 함수 참조 */ fun salute() = println("Salute!") run(::salute) // 최상위 함수를 참조 /* 확장 함수에 적용 */ fun Person.isAdult() = age >= 21 val predicate = Person:: isAdult
- 생성자 참조
- 클래스 생성 작업 내용을 값으로 저장 하는 것
- 클래스 생성을 연기하거나 저장해둘 수 있다
/* 생성자 참조 */ data class Person(val name: String, val age: Int) val createPerson = ::Person // Person 인스턴스 생성 작업을 값으로 저장 val p - createPerson("hue", 26) // 생성자 참조를 통해 Person 인스턴스 생성
[ 필수적인 함수 : filter와 map ]
- filter 함수
- 컬렉션을 이터레이션하면서, 람다에 각 원소를 넘기고 true를 반환하는 원소만 모은다
- 결과는 조건에 true를 만족하는 원소로 이루어진 새로운 컬렉션(Collection)
- 원치 않는 원소를 제거하는 목적으로 사용
val list = listOf(1,2,3,4) val newList = list.filter {it%2 == 0} // 짝수인 원소들로 이루어진 컬렉션을 반환
- map 함수
- 주어진 람다를 각 원소에 적용한 결과를 모아서 새 컬렉션으로 반환
/* map함수로 새로운 컬렉션 반환 */ val people = listOf(Person("Alice", 29), Person("Bob", 31)) val nameList = people.map{it.name} /* 멤버 참조를 이용한 같은 표현 */ val nameList = people.map(Person::name) /* filter + map */ people.filter{ it.age > 30 }.map(Person::name)
- map 컬렉션과 filter / map
- 맵의 경우 키와 값을 처리하는 filter / map함수가 따로 존재
- key 처리
- filterKeys()
- mapKeys()
- value 처리
- filterValues()
- mapValues()
val numbers = mapOf(0 to "zero", 1 to "one") /* value값을 대문자로 변환한 후 새로운 컬렉션으로 반환 */ val valuesMap = numbers.mapValues{ it.value.toUpperCase() }
[ 컬렉션에 술어 적용 : all, any, count, find ]
- 술어
- 참 / 거짓을 반환하는 함수
- 다양한 함수
- all : 모든 원소가 술어를 만족하는지 여부를 true / false로 반환
- any : 술어를 만족하는 원소가 하나라도 있는지 여부를 true / false로 반환
- count : 술어를 만족하는 원소의 개수를 반환
- find : 술어를 만족하는 가장 첫 원소를 찾아서 반환
/* 술어 */ val canBeInClub27 = { p: Person -> p.age <= 27 } /* Person List */ val people = listOf(Person("Alice", 29), Person("Bob", 31)) /* all 과 !all */ people.all(canBeInClub27) // false !people.all(canBeInClub27) // true /* any 과 any */ people.any(canBeInClub27) // true !people.any(canBeInClub27) // false /* 개수 구하기 ( filter + size vs count ) */ /* filter 후 size로 개수를 가져오는 방식 => 조건을 만족하는 중간 컬렉션이 생김 => 비효율적 */ people.filter(canBeInClub27).size // 1 /* count => 조건을 만족하는 원소의 개수만 추적 => 따로 중간 컬렉션이 없어서 filter + size보다 효율적! */ people.count(canBeInClub27) // 1 /* find => 술어에 해당하는 것이 없다면 null 반환 */ people.find(canBeInClub27) // 조건에 맞는 첫번째 객체 반환
[ group : 리스트를 여러 그룹으로 이루어진 맵으로 변경 ]
- group 함수
- 컬렉션의 모든 원소를 어떤 특성에 따라 그룹으로 분리
- 구분하고 싶은 특성을 파라미터로 전달
/* 특정 특성을 통해 그룹화 */ val list = listOf("a", "ab", "b") println(list.groupBy(String::first)) // {a=[a, ab], b=[b]}
[ flatMap 과 flatten : 중첩된 컬렉션 안의 원소 처리 ]
- flatMap
- 인자로 주어진 람다를 컬렉션의 모든 객체에 적용 후, 여러 리스트를 한 리스트로 모은다
람다 적용
+평평하게
/* 저자의 정보를 뺀 후, set으로 변환해서 중복 값을 제거 */ class Book(val title: String, val author: List<String>) books.flatMap{ it.author }.toSet() /* String.toList() : String 문자열을 모든 문자로 분리 */ val strings = listOf("abc", "def") println(strings.flatMap( it.toList() )) // [a, b, c, d, e, f]
- flatten
- 중첩된 컬렉션의 원소를 한 리스트로 모으기만 해주는 역할
- 굳이 반환할 것이 없는 중첩 리스트에서 유용하게 사용
평평하게
만 해줌
[ 컬렉션 연산 비교 ]
- 즉시 계산(eager) 컬렉션 연산
- 앞서 살펴본 map, filter와 같은 연산은 결과 컬렉션을 즉시 생성
- 즉시라는 말은, 매 단계마다 계산의 중간 결과를 새로운 컬렉션이 임시로 담게 된다
- 지연 계산(lazy) 컬렉션 연산
- 시퀀스(sequence)를 통해서 중간 임시 컬렉션 없이 컬렉션 연산을 수행
=> 효율적 계산 수행 가능/* 즉시 계산 컬렉션 연산 => 2개의 임시 컬렉션이 생성 (map, filter) */ people.map(Person::name).filter{ it.startsWith("A") } /* 지연 계산 컬렉션 연산 => 중간 임시 컬렉션 없이 최종 결과 컬렉션만 생성 => 하나의 원소가 차례대로 각 단계를 수행하는 방식이기 때문 */ people.asSequence() .map(Person::name) .filter{ it.startsWith("A") } .toList()
- 컬렉션 => 시퀀스 변환
- Sequence 인터페이스의 asSequence()를 통해 어떤 컬렉션이든 sequence로 변환 가능
- 시퀀스 => 컬렉션 변환
- 시퀀스를 이용한 결과는 시퀀스 형태이다
- 시퀀스를 통해 인덱스로 접근하는 다른 API가 필요하다면 결국 리스트로 변환해야 한다
.toList()
등등
[ 시퀀스 연산 실행 : 중간 연산과 최종 연산 ]
- 시퀀스 연산 종류
- 중간(intermediate) 연산
- 결과(return) : 또 다른 시퀀스를 반환
- 항상 지연 계산이 수행됨 => 즉, 최종 연산이 있을 때에 수행
- 최종(terminal) 연산
- 결과(return) : 결과를 반환
- 앞의 중간 연산들을 실제로 수행하게 한다
- 시퀀스 연산의 동작 수행
- 최종 연산이 수행 될 때, 앞에 정의된 모든 중간 연산이 수행
- 모든 원소들에 대해, 하나의 원소가 한 단계의 연산을 거쳐 최종 결과까지 수행 됨
(원소를 하나씩 끝까지 처리하는 방식)
=> 모든 원소가 하나의 연산을 통해 중간 결과를 만드는 non-sequence 방식 보다 효율적!/* 최종 연산이 없어서 아무런 동작도 수행하지 않는다 */ listOf(1,2,3,4).asSequence() .map{ it * it } .filter{ it % 2 == 0 } /* .toList()라는 최종 연산이 있어서 비로소 이 때 map, filter동작도 수행 */ listOf(1,2,3,4).asSequence() .map{ it * it } .filter{ it % 2 == 0 } .toList() // 최종 연산
- Java 8의 Stream vs Kotlin의 Sequence
- Java 8의 Stream과 매우 유사하게 동작한다
- Java 8에서는 스트림 연산을 CPU에서 병렬적으로 실행하는 기능이 존재
=> 필요시 Sequence가 아니라, Stream을 선택적으로 이용해도 좋다!
[ 시퀀스(Sequence) 만들기 ]
- Collection.asSequence() 함수
- Collection => Sequence로 변환
- generateSeqence() 함수
- 이 전의 원소를 인자로 받아서 다음 원소를 계산하는 시퀀스를 생성
val naturalNumbers = generateSequence(0) { it + 1 } val numbersTo100 = naturalNumbers.takeWhile { it <= 100 } /* sum() 이라는 최종 연산을 통해 모든 원소를 더한결과를 반환 */ println(numbersTo100.sum())
- Java에서 특정 로직을 수행하기 위해서는
무명 클래스
를 이용했다
=> Kotlin에서는람다
를 통해서 이를 대신할 수 있다
=> 왜냐하면, SAM이기 때문!
- SAM 인터페이스
- Single Abstract Method 의 약자
- 추상 메소드가 단 하나만 있는 인터페이스
- ex)
Runnable
/Callable
[ Java 메소드에 람다를 인자로 전달 ]
- 람다를 인자로 넘길 때, 람다가 주변 외부 변수를 포획하면
=> 포획한 변수를 저장하는 필드가 생긴다
=> 즉, 호출 시 마다 새로운 인스턴스를 생성하게 된다- 람다가 주변 외부 변수를 포획하지 않으면
=> 호출 시 마다 같은 인스턴스를 사용/* Java 메소드 */ void postponeComputation(int delay, Runnable computation) /* 람다를 인자로 전달 => 객체를 명시적으로 선언하지 X + 변수 포획 X => 호출시마다 같은 객체를 공유 */ postponeComputation(1000) {println(42)} /* 람다를 인자로 전달 => 객체를 명시적으로 선언 O => 호출시마다 새로운 객체를 생성 */ postponeComputation(1000, object : Runnable { override fun run() { println(42) } })
[ 공통 ]
- 수신 객체 지정 람다
- 수신 객체를 명시하지 않고, 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있는 것
- with와 apply 를 통해 구현
[ with 함수 ]
- 개념
- 객체의 이름을 반복하지 않고, 객체에 대해 다양한 연산을 수행하도록 도와주는 라이브러리
- 원리
- 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 지정하는 원리
- 람다에서 사용
- this를 통해서 접근 가능
- 프로퍼티나 메소드 이름만으로도 접근 가능
- 반환 값
- 람다 코드를 실행한 결과
- 람다 식의 본문에서 마지막 식의 값
- 만약, 수신 객체 자체를 return하려면 => apply 함수를 사용!
/* with 사용 X */ fun alphabet() : String{ val result = StringBuilder() for(letter in 'A'..'Z'){ result.append(letter) // result 중복 } result.append("append!") // result 중복 return result.toString() // result 중복 } /* with 사용 O */ fun alphabet() : String{ val stringBuilder = StringBuilder() return with(stringBuilder){ for(letter in 'A'..'Z'){ this.append(letter) // this를 통해서 접근 } append("append!") // this 없이도 함수에 바로 접근 가능 this.toString() // return 값 } }
[ apply 함수 ]
- apply
- with와 유사하지만, 항상 자신에게 전달된 객체를 반환한다는 차이점이 존재
- with는 람다의 결과를 반환 / apply는 객체 자체를 반환
/* apply 사용 O */ fun alphabet() = StringBuilder().apply { for(letter in 'A'..'Z'){ append(letter) // 함수에 바로 접근해서 사용 } append("append!") // 함수에 바로 접근해서 사용 }.toString()
람다
를 사용하면코드 조각
을다른 함수
에게인자
로 넘길 수 있다- Kotlin에서
람다
가함수 인자
인 경우,괄호 밖
으로 빼낼 수 있다람다의 인자
가단 하나뿐
인 경우, 이름을 지정하지 않고기본 값
인it
로 사용할 수 있다- Kotlin의
람다 안에 있는 코드
는외부 변수
를읽거나 쓸
수 있다메소드
,생성자
,프로퍼티
의 이름 앞에::
을 붙이면각각에 대한 참조
를 만들 수 있다filter
,map
,all
,any
등의 함수로컬렉션의 대부분의 연산
을 간편하게 할 수 있다시퀀스(sequence)
를 사용하면,중간 결과를 담는 컬렉션 생성 없이
도연산을 조합
할 수 있다수신 객체 지정 람다
를 사용하면람다 안
에서미리 정해둔 수신 객체
의메소드
를직접 호출
할 수 있다with 함수
를 통해특정 객체의 참조
를 반복해서 언급하지 않고메소드를 호출
할 수 있다apply
를 통해서어떤 객체
라도빌더 스타일의 API
를 사용해서생성
하고초기화
할 수 있다