/** * Performing operation on: 5 * Result: 10 */funperformOperation(num: Int, operation: (Int) -> Unit) {println("Performing operation on: $num")operation(num)}// 코드 조각을 performOperation 함수의 인자로 넘기기// 람다가 함수 인자인 경우 괄호 밖으로 람다를 빼내기performOperation(5) { // // 람다 인자가 하나뿐인 경우 인자 이름을 지정하지 않고 디폴트 이름(it)으로 사용 가능println("Result: ${it *2}") }
람다 안에서 바깥 함수의 변수 읽기/쓰기
var result =0val add: (Int) -> Unit= { result += it }add(5)assertEquals(5, result)
// 메시지의 목록을 받아 모든 메시지에 똑같은 접두사를 붙여서 출력funprintMessagesWithPrefix(messages: Collection<String>, prefix: String) { messages.forEach {println("$prefix $it") }}
코틀린은 자바와 달리 람다 밖 함수에 있는 final이 아닌 변수에 접근할 수 있고, 그 변수를 변경할 수도 있다.
funprintProblemCounts(responses: Collection<String>) {var clientErrors =0var serverErrors =0 responses.forEach {if (it.startsWith("4")) { clientErrors++// 람다 밖에 있는 변수를 변경 } elseif (it.startsWith("5")) { serverErrors++ } }println("$clientErrors client errors, $serverErrors server errors")}
단, 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우
함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다.
// 핸들러는 tryToCountButtonClicks가 clicks를 반환한 다음에 호출funtryToCountButtonClicks(button: Button) : Int {var clicks =0 button.onClick { clicks++ }return clicks}
컬렉션 함수형 API
filter & map
filter 함수는 컬렉션에서 원치 않는 원소를 제거
하지만 filter는 원소를 변환은 불가
원소를 변환하려면 map 함수를 사용해야 한다.
map 함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 생성
@Testfun`filter and map`() {val people1 =listOf(Person("Alice", 29), Person("Bob", 31))assertEquals(listOf(Person("Bob", 31)), people1.filter { it.age >30 })val people2 =listOf(Person("Alice", 29), Person("Bob", 31))assertEquals(listOf("Alice", "Bob"), people2.map { it.name })val numbers =mapOf(0 to "zero", 1 to "one")assertEquals(mapOf(0 to "ZERO", 1 to "ONE"), numbers.mapValues { it.value.uppercase() })}
all, any, count, find
컬렉션의 모든 원소가 어떤 조건을 만족하는지 판단하는 연산(ex. all, any ..)
@Testfun`all any count find`() {val canBeInClub27 = { p: Person -> p.age <=27 }val people1 =listOf(Person("Alice", 27), Person("Bob", 31))// Returns true if all elements match the given predicate.assertFalse(people1.all(canBeInClub27))val people2 =listOf(Person("Alice", 27), Person("Bob", 31))// Returns true if at least one element matches the given predicate.assertTrue(people2.any(canBeInClub27))val people3 =listOf(Person("Alice", 27), Person("Bob", 31))// Returns the number of elements matching the given predicate.assertEquals(1, people3.count(canBeInClub27))val people4 =listOf(Person("Alice", 27), Person("Bob", 31))// Returns the first element matching the given predicate, or null if no such element was found.assertEquals(Person("Alice", 27), people4.find(canBeInClub27))}
flatMap & flatten
flatMap 함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고
람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 모은다.
@Test// Returns a single list of all elements yielded from results of transform function being invoked on each element of original collection.fun`flatMap`() {val strings =listOf("abc", "def")assertEquals(listOf('a', 'b', 'c', 'd', 'e', 'f'), strings.flatMap { it.toList() })val books =listOf(Book("Thursday Next", listOf("Jasper Fforde")),Book("Mort", listOf("Terry Pratchett")),Book("Good Omens", listOf("Terry Pratchett", "Neil Gaiman")) )assertEquals(setOf("Jasper Fforde", "Terry Pratchett", "Neil Gaiman"), books.flatMap { it.authors }.toSet())}@Test// Returns a single list of all elements from all collections in the given collection.fun`flatten`() {val lists =listOf(listOf(1, 2, 3),listOf(4, 5, 6),listOf(7, 8, 9) )assertEquals(listOf(1, 2, 3, 4, 5, 6, 7, 8, 9), lists.flatten())}
sequence
지연 계산(lazy) 컬렉션 연산
map, filter 같은 컬렉션 함수는 결과 컬렉션을 즉시 생성하는데
이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 뜻
시퀀스(sequence)를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
@Testfun`lazy`() {/** * map(1) filter(1) * map(2) filter(4) * map(3) filter(9) * map(4) filter(16) */listOf(1, 2, 3, 4).asSequence() // 원본 컬렉션을 시퀀스로 변환 .map { print("map($it) "); it * it } // 시퀀스도 컬렉션과 똑같은 API 제공 .filter { println("filter($it) "); it %2==0 } .toList() // 결과 시퀀스를 다시 리스트로 변환}
시퀀스 연산 실행: 중간 연산과 최종 연산
시퀀스에 대한 연산은 중간 연산과 최종 연산으로 나뉜다.
중간 연산은 다른 시퀀스를 반환
그 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 안다.
최종 연산은 결과를 반환
@Testfun`sequence`() {// 시퀀스의 모든 연산은 각 원소에 대해 순차적으로 적용// 첫 번째 원소가 처리되고, 다시 두 번째 원소가 처리되며, 이런 처리가 모든 원소에 대해 적용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) }
자바 함수형 인터페이스 활용
자바 메소드에 람다를 인자로 전달
함수형 인터페이스를 인자로 원하는 자바 메소드에 코틀린 람다를 전달할 수 있다.
@Testfun`자바 메소드에 람다를 인자로 전달`() {funpostponComputation(num: Int, runnable: Runnable) {println("num is : "+ num) runnable.run() }// 객체 식을 함수형 인터페이스 구현으로postponComputation(1000, object : Runnable {overridefunrun() {println(42) } })// 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 생성postponComputation(1000) { println(42) }}
무명 객체는 메소드 호출 때마다 새로운 객체가 생성되지만,
람다는 메소드를 호출할 때마다 반복 사용
단, 람다가 주변 영역의 변수를 참조할 경우 매 호출마다 같은 인스턴스를 사용할 수 없음
이 경우 컴파일러는 매번 주변 영역의 변수를 참조한 새로운 인스턴스를 생성
funhandlerComputation(id: String) {// handlerComputation 호출 때마다 새로 Runnable 인스턴스 생성postponeComputation(1000) { println(id) }}
람다를 함수형 인터페이스로 명시적으로 변경
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수
컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자 사용 가능
@Testfun`with function`() {funalphabet(): String {val result =StringBuilder()for (letter in'A'..'Z') { result.append(letter) } result.append("Now I know the alphabet!")return result.toString() }funalphabetUsingWith(): String {val stringBuilder =StringBuilder()// 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체 생성returnwith(stringBuilder) {for (letter in'A'..'Z') {// 본문에서는 this를 사용해 인자로 받은 수신 객체에 접근 가능this.append(letter) }append("Now I know the alphabet!")// with가 반환하는 값은 람다 코드를 실행한 결과(람다 코드의 마지막 식의 값)// 람다의 결과 대신 수신 객체가 필요한 경우 apply 라이브러리 함수를 사용this.toString() } }assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZNow I know the alphabet!", alphabet())assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZNow I know the alphabet!", alphabetUsingWith())}
apply 함수
거의 with와 동일하고, 항상 자신에게 전달된 객체(즉 수신 객체)를 반환
어떤 객체라도 빌더 스타일의 API를 사용해 생성하고 초기화
@Testfun`apply function`() {funalphabet() =StringBuilder().apply {for (letter in'A'..'Z') {append(letter) }append("Now I know the alphabet!") }.toString()assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZNow I know the alphabet!", alphabet())}
with와 apply는 수신 객체 지정 람다를 사용하는 일반적인 예제 중 하나
더 구체적인 함수를 비슷한 패턴으로 활용 가능
ex. 표준 라이브러리의 buildString 함수를 사용하여 단순화
@Testfun`buildString`() {// buildString 함수는 StringBuilder를 활용해 String을 만드는 경우 사용할 수 있는 우아한 해법funalphabet() =buildString {for (letter in'A'..'Z') {append(letter) }append("Now I know the alphabet!") }assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZNow I know the alphabet!", alphabet())}
타입 시스템
널 가능성
널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시
프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 그 프로퍼티를 초기화해야 한다.
그런 초기화 값을 제공할 수 없으면 널이 될 수 있는 타입을 사용할 수밖에 없다.
하지만 널이 될 수 있는 타입을 사용하면 모든 프로퍼티 접근에 널 검사를 넣거나 !! 연산자를 써야 한다.
프로퍼티를 여러 번 사용해야 할 경우 코드가 지저분해지는데
이를 해결하기 위해 lateinit 변경자를 사용해서 프로퍼티를 나중에 초기화(late-initialized)할 수 있다.
// AS-ISclassMyService {funperformAction(): String="foo"}classMyTest {// null로 초기화하기 위해 널이 될 수 있는 타입인 프로퍼티를 선언privatevar myService: MyService? =null@BeforefunsetUp() {// setUp 메소드 안에서 실제 초깃값을 지정 myService =MyService() }@TestfuntestAction() {// 널 가능성에 신경 써야 하므로, !!나 ? 사용 필수assertEquals("foo", myService!!.performAction()) }}---// TO-BEclassMyService {funperformAction(): String="foo"}classMyTest {// 초기화하지 않고 널이 될 수 없는 프로퍼티를 선언privatelateinitvar myService: MyService@BeforefunsetUp() { myService =MyService() }@TestfuntestAction() {// 널 검사를 수행하지 않고 프로퍼티를 사용 Assert.assertEquals("foo", myService.performAction()) }}
코틀린의 원시 타입
원시 타입: Int, Boolean …
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용
@Testfun`원시 타입`() {funshowProgress(progress: Int): String {// 원시 타입의 값에 대해 메소드 호출 가능val percent = progress.coerceIn(0, 100)return"We're ${percent}% done!" }assertEquals("We're 90% done!", showProgress(90))assertEquals("We're 100% done!", showProgress(130))}
코틀린은 실행 시점에 숫자 타입이 가능한 한 가장 효율적인 방식으로 표현
대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일
자바 원시 타입에 해당하는 타입들
정수 타입 : Byte, Short, Int, Long
부동소수점 수 타입 : Float, Double
문자 타입 : Char
불리언 타입 : Boolean
널이 될 수 있는 원시 타입: Int?, Boolean? …
코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일
코틀린에서 적절한 타입을 찾으려면 그 변수나 프로퍼티에 널이 들어갈 수 있는지만 고민하면 된다.
@Testfun`널이 될 수 있는 원시 타입`() {dataclassPerson(val name: String,val age: Int? =null) {funisOlderThan(other: Person): Boolean? {if (age ==null|| other.age ==null)returnnullreturn age > other.age } }assertEquals(false, Person("Sam", 35).isOlderThan(Person("Amy", 42)))assertEquals(null, Person("Sam", 35).isOlderThan(Person("Jane")))}
숫자 변환
코틀린과 자바의 가장 큰 차이점 중 하나는 숫자를 변환하는 방식
코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우 조차도 자동 변환은 불가능
@Testfun`숫자 변환`() {val i =1val l: Long= i // 컴파일 오류: Type mismatch. Required: Long, Found: Intval l2: Long= i.toLong()}
코틀린은 모든 원시 타입에 대한 변환 함수를 제공
ex) toByte(), toShort(), toChar() …
표현 범위가 더 넓은 타입으로 변환하는 함수도 있고,
표현 범위가 더 좁은 타입으로 변환하면서, 값을 벗어나는 경우 일부를 잘라내는 함수(Long.toInt())도 존재
자바에서는 Object가 클래스 계층의 최상위 타입이라면 코틀린에서는 Any 타입이 원시 타입(Int 등)을 포함한 모든 타입 조상 타입
@Testfun`Any 타입 테스트`() {val anyInt: Any=42val anyString: Any="Hello, World!"val anyList: Any=listOf(1, 2, 3)assertTrue(anyInt is Int)assertTrue(anyString is String)assertTrue(anyList is List<*>)}
Unit 타입: 코틀린의 void
코틀린의 Unit 타입은 자바 void와 같은 기능
반환이 없는 함수의 반환 타입으로 사용 가능
이는 반환 타입 선언 없이 정의한 블록이 본문인 함수와 동일
코틀린의 Unit이 자바 void와 다른 점?
Unit은 모든 기능을 갖는 일반적인 타입이며, void와 달리 Unit을 타입 인자로 사용 가능
Unit 타입에 속한 값은 단 하나뿐이며, 그 이름도 Unit
Unit 타입의 함수는 Unit 값을 묵시적으로 반환
이 두 특성은 제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환 타입으로 Unit을 쓸 때 유용
일반적인 읽기 전용 라이브러리를 사용하려면 kotlin.collections.Collection 라이브러리를 사용
컬렉션의 데이터를 수정하려면 kotlin.collections.MutableCollection 인터페이스를 사용
원소를 추가/삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 제공
코틀린 컬렉션과 자바
코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스
두 가지 표현 제공
이런 성질로 컬렉션의 변경 가능성과 관련해 자바에서 중요한 문제가 발생
자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로,
코틀린에서 읽기 전용 컬렉션으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용 변경 가능
/* Kotlin */objectCollectionInterop {fungetReadOnlyList(): List<String> {// 읽기 전용 컬렉션으로 선언된 객체returnlistOf("A", "B", "C") }}/* Java */@Testvoid testModifyReadOnlyList() { List<String> readOnlyList = CollectionInterop.getReadOnlyList(); readOnlyList.set(0, "Z");// 자바 코드에서는 코틀린의 읽기 전용 컬렉션 내용 변경 가능assertEquals("Z", readOnlyList.get(0));}
객체의 배열과 원시 타입의 배열
코틀린 배열은 타입 파라미터를 받는 클래스
funmain(args: Array<String>) {for (i in args.indices) {println("Argument $i is: ${args[i]}") }}
배열의 원소 타입은 바로 그 타입 파라미터에 의해 정해지고, 코틀린에서 배열을 만드는 방법은 다양하다.
/* * `arrayOf` 함수에 원소를 넘기면 배열을 만들 수 있다. */@Testfun`test arrayOf`() {val array =arrayOf(1, 2, 3)assertEquals(3, array.size)assertEquals(1, array[0])assertEquals(2, array[1])assertEquals(3, array[2])}/** * `arrayOfNulls` 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 * 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다. * 원소 타입이 널이 될 수 있는 타입인 경우에만 이 함수를 사용 가능 */@Testfun`test arrayOfNulls`() {val array =arrayOfNulls<String>(3)assertEquals(3, array.size)assertNull(array[0])assertNull(array[1])assertNull(array[2])}/* * `Array 생성자`는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화 * `arryOf`를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용 */@Testfun`test Array constructor`() {val array =Array(3) { i -> i *2 }assertEquals(3, array.size)// 람다를 호출해서 각 배열 원소를 초기화assertEquals(0, array[0])assertEquals(2, array[1])assertEquals(4, array[2])val letters =Array(26) { i -> ('a'+ i).toString() }assertEquals("abcdefghijklmnopqrstuvwxyz" , letters.joinToString(""))}/** * 컬렉션의 모든 요소를 포함하는 형식화된 배열을 반환 */@Testfun`test toTypedArray`() {val strings =listOf("a", "b", "c")assertEquals("a/b/c", "%s/%s/%s".format(*strings.toTypedArray()))}/** * [separator]를 사용하고 제공된 경우 주어진 [prefix] 및 [postfix]를 사용하여 * 구분된 모든 요소에서 문자열을 생성 */@Testfun`test joinToString`() {val squares =IntArray(5) { i -> (i+1) * (i+1) }assertTrue(squares.contentEquals(intArrayOf(1, 4, 9, 16, 25)))assertEquals("{1, 4, 9, 16, 25}", squares.joinToString(separator =", ", prefix ="{", postfix ="}"))}/** * 각 요소에 대해 지정된 작업을 수행하여 요소에 순차적 인덱스를 제공 */@Testfun`test forEachIndexed`() {funtestForEachIndexed(args: Array<String>) { args.forEachIndexed { index, element ->println("Argument $index is: $element") } }/** * Argument 0 is: 10 * Argument 1 is: 20 * Argument 2 is: 30 */testForEachIndexed(arrayOf("10", "20", "30"))}