/**
* Performing operation on: 5
* Result: 10
*/
fun performOperation(num: Int, operation: (Int) -> Unit) {
println("Performing operation on: $num")
operation(num)
}
// 코드 조각을 performOperation 함수의 인자로 넘기기
// 람다가 함수 인자인 경우 괄호 밖으로 람다를 빼내기
performOperation(5) {
// // 람다 인자가 하나뿐인 경우 인자 이름을 지정하지 않고 디폴트 이름(it)으로 사용 가능
println("Result: ${it * 2}")
}
람다 안에서 바깥 함수의 변수 읽기/쓰기
var result = 0
val add: (Int) -> Unit = { result += it }
add(5)
assertEquals(5, result)
메소드, 생성자, 프로퍼티 이름 앞에 ::을 붙이면 각각에 대한 참조 생성 가능
참조를 람다 대신 다른 함수에게 넘길 수 있음
fun printMessage(message: String): String {
return message
}
// 메소드, 생성자, 프로퍼티 참조
val ref = ::printMessage
assertEquals("Hello", ref("Hello"))
컬렉션 함수 활용으로 컬렉션에 대한 연산을 직접 원소를 이터레이션 하지 않고 수행
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter { it % 2 == 0 }
val doubledNumbers = numbers.map { it * 2 }
val allEven = numbers.all { it % 2 == 0 }
val anyEven = numbers.any { it % 2 == 0 }
assertEquals(listOf(2, 4), evenNumbers)
assertEquals(listOf(2, 4, 6, 8, 10), doubledNumbers)
assertFalse(allEven)
assertTrue(anyEven)
시퀀스를 사용하면 중간 결과를 담는 컬렉션을 생성하지 않고도 컬렉션에 대한 여러 연산을 조합 가능
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.asSequence()
.filter { it % 2 == 0 }
.map { it * 2 }
.toList()
assertEquals(listOf(4, 8), result)
함수형 인터페이스(추상 메소드가 단 하나뿐인 SAM 인터페이스)를 인자로 받는 자바 함수를 호출할 경우 람다를 함수형 인터페이스 인자 대신 넘길 수 있다.
// Runnable 인터페이스를 인자로 받는 자바 메소드 호출
val thread = Thread { println("Running in a thread") }
thread.start()
수신 객체 지정 람다를 사용하면 람다 안에서 미리 정해둔 수신 객체의 메소드를 직접 호출 가능
@Test
fun `as? 연산자와 플랫폼 타입`() {
val obj: Any = "Hello"
val str: String? = obj as? String
assertNotNull(str)
val intObj: Any = 42
val int: Int? = intObj as? Int
assertNotNull(int)
}
컴파일러는 숫자 타입을 자바 원시 타입(int 등)으로 컴파일
@Test
fun `숫자 타입과 자바 원시 타입`() {
val intVal: Int = 42
assertTrue(intVal is Int)
}
널이 될 수 있는 원시 타입(Int? 등)은 자바의 박싱한 원시 타입에 대응
@Test
fun `널이 될 수 있는 원시 타입`() {
val nullableInt: Int? = 42
assertNotNull(nullableInt)
}
Any 타입은 다른 모든 타입의 조상 타입이며, 자바의 Object에 해당
Unit은 자바의 void와 유사
@Test
fun `Any 타입과 Unit 타입`() {
val anyVal: Any = 42
assertTrue(anyVal is Any)
fun returnUnit(): Unit {
// do something
}
assertEquals(Unit, returnUnit())
}
정상적으로 끝나지 않는 함수의 반환 타입을 지정할 때 Nothing 타입 사용
@Test
fun `Nothing 타입`() {
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
assertThrows<IllegalStateException> {
fail("This is an error")
}
}
코틀린 컬렉션은 표준 자바 컬렉션 클래스를 사용
코틀린은 자바보다 컬렉션을 더 개선해서 읽기 전용 컬렉션과 변경 가능한 컬렉션을 구분
@Test
fun `코틀린 컬렉션과 자바 컬렉션`() {
val readOnlyList: List<String> = listOf("a", "b", "c")
val mutableList: MutableList<String> = mutableListOf("a", "b", "c")
assertEquals(3, readOnlyList.size)
assertEquals(3, mutableList.size)
mutableList.add("d")
assertEquals(4, mutableList.size)
}
자바 클래스를 코틀린에서 확장하거나 자바 인터페이스를 코틀린에서 구현하는 경우
메소드 파라미터의 널 가능성과 변경 가능성에 대해 깊은 생각이 필요
class KotlinClass : JavaInterface {
override fun someMethod(param: String?) {
// handle nullability
}
}
코틀린의 Array 클래스는 일반 제네릭 클래스처럼 보이지만 Array는 자바 배열로 컴파일
@Test
fun `Array 클래스와 자바 배열`() {
val array: Array<String> = arrayOf("a", "b", "c")
assertEquals(3, array.size)
}
원시 타입의 배열은 IntArray와 같이 각 타입에 대한 특별한 배열로 표현
@Test
fun `원시 타입의 배열`() {
val intArray: IntArray = intArrayOf(1, 2, 3)
assertEquals(3, intArray.size)
assertEquals(1, intArray[0])
}
람다 프로그래밍
람다 식과 멤버 참조
코드 블록을 함수 인자로 넘기기
함수를 직접 다른 함수에 전달할 수 있다.
람다 식을 사용하면 코드가 더욱 더 간결해진다.
람다와 컬렉션
함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로 대치할 수 있다.
data class Person(val name: String, val age: Int)
/* Java */
fun findTheOldest(people: List<Person>): Person? {
var maxAge = 0
var theOldest: Person? = null
for (person in people) {
if (person.age > maxAge) {
maxAge = person.age
theOldest = person
}
}
return theOldest
}
@Test
fun `람다와 컬렉션`() {
val javaPeople = listOf(Person("Alice", 29), Person("Bob", 31))
assertEquals(Person("Bob", 31), findTheOldest(javaPeople))
val kotlinPeople = listOf(Person("Alice", 29), Person("Bob", 31))
assertEquals(Person("Bob", 31), kotlinPeople.maxByOrNull { it.age })
}
람다 현재 영역에 있는 변수에 접근
// 메시지의 목록을 받아 모든 메시지에 똑같은 접두사를 붙여서 출력
fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach {
println("$prefix $it")
}
}
코틀린은 자바와 달리 람다 밖 함수에 있는 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")
}
단, 람다를 이벤트 핸들러나 다른 비동기적으로 실행되는 코드로 활용하는 경우
함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다.
// 핸들러는 tryToCountButtonClicks가 clicks를 반환한 다음에 호출
fun tryToCountButtonClicks(button: Button) : Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
컬렉션 함수형 API
filter & map
filter 함수는 컬렉션에서 원치 않는 원소를 제거
하지만 filter는 원소를 변환은 불가
원소를 변환하려면 map 함수를 사용해야 한다.
map 함수는 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 생성
@Test
fun `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 ..)
@Test
fun `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)를 사용하면 중간 임시 컬렉션을 사용하지 않고도 컬렉션 연산을 연쇄할 수 있다.
@Test
fun `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() // 결과 시퀀스를 다시 리스트로 변환
}
시퀀스 연산 실행: 중간 연산과 최종 연산
시퀀스에 대한 연산은 중간 연산과 최종 연산으로 나뉜다.
중간 연산은 다른 시퀀스를 반환
그 시퀀스는 최초 시퀀스의 원소를 변환하는 방법을 안다.
최종 연산은 결과를 반환
@Test
fun `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)
}
자바 함수형 인터페이스 활용
자바 메소드에 람다를 인자로 전달
함수형 인터페이스를 인자로 원하는 자바 메소드에 코틀린 람다를 전달할 수 있다.
@Test
fun `자바 메소드에 람다를 인자로 전달`() {
fun postponComputation(num: Int, runnable: Runnable) {
println("num is : " + num)
runnable.run()
}
// 객체 식을 함수형 인터페이스 구현으로
postponComputation(1000, object : Runnable {
override fun run() {
println(42)
}
})
// 프로그램 전체에서 Runnable의 인스턴스는 단 하나만 생성
postponComputation(1000) { println(42) }
}
무명 객체는 메소드 호출 때마다 새로운 객체가 생성되지만,
람다는 메소드를 호출할 때마다 반복 사용
단, 람다가 주변 영역의 변수를 참조할 경우 매 호출마다 같은 인스턴스를 사용할 수 없음
이 경우 컴파일러는 매번 주변 영역의 변수를 참조한 새로운 인스턴스를 생성
fun handlerComputation(id: String) {
// handlerComputation 호출 때마다 새로 Runnable 인스턴스 생성
postponeComputation(1000) { println(id) }
}
람다를 함수형 인터페이스로 명시적으로 변경
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수
컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자 사용 가능
@Test
fun `with function`() {
fun alphabet(): String {
val result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("Now I know the alphabet!")
return result.toString()
}
fun alphabetUsingWith(): String {
val stringBuilder = StringBuilder()
// 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체 생성
return with(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를 사용해 생성하고 초기화
@Test
fun `apply function`() {
fun alphabet() = 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 함수를 사용하여 단순화
@Test
fun `buildString`() {
// buildString 함수는 StringBuilder를 활용해 String을 만드는 경우 사용할 수 있는 우아한 해법
fun alphabet() = buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("Now I know the alphabet!")
}
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZNow I know the alphabet!", alphabet())
}
타입 시스템
널 가능성
널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시
@Test
fun `안전한 호출 연산자`() {
class Address(val streetAddress: String, val zipCode: Int,
val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun Person.countryName(): String {
val country = this.company?.address?.country
return if (country != null) country else "Unknown"
}
val person = Person("Dmitry", null)
assertEquals("Unknown", person.countryName())
val person2 = Person("Dmitry", Company("JetBrains", null))
assertEquals("Unknown", person2.countryName())
val person3 = Person("Dmitry", Company("JetBrains", Address("ABC Street", 42, "Seoul", "KOREA")))
assertEquals("KOREA", person3.countryName())
}
엘비스 연산자: ?:
코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공
코틀린에서는 return 이나 throw 등의 연산도 식이다.
따라서 엘비스 연산자의 유항에 return, throw 등의 연산을 넣을 수 있고,
엘비스 연산자를 더욱 편하게 사용할 수 있다.
@Test
fun `엘비스 연산자`() {
class Address(val streetAddress: String, val zipCode: Int,
val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun printShippingLabel(person: Person) {
val address = person.company?.address
?: throw IllegalArgumentException("No address")
with (address) {
print(streetAddress + ". ")
println("$zipCode $city, $country")
}
}
val address = Address("ABC Street", 42, "Seoul", "KOREA")
val jetbrains = Company("JetBrains", address)
val person = Person("Dmitry", jetbrains)
// ABC Street. 42 Seoul, KOREA
printShippingLabel(person)
assertThrows<IllegalArgumentException> {
printShippingLabel(Person("Alexey", null))
}
}
@Test
fun `안전한 캐스트`() {
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
// 파라미터로 받은 값이 원하는 타입인지 쉽게 검사하고 캐스트하고,
// 타입이 맞지 않으면 쉽게 false 반환
val otherPerson = o as? Person ?: return false
return otherPerson.firstName == firstName &&
otherPerson.lastName == lastName
}
override fun hashCode(): Int =
firstName.hashCode() * 37 + lastName.hashCode()
}
val p1 = Person("Dmitry", "Jemerov")
val p2 = Person("Dmitry", "Jemerov")
assertTrue(p1 == p2)
assertFalse(p1.equals(42))
}
널 아님 단언: !!
!!으로 어떤 값이든 널이 될 수 없는 타입으로 (강제로) 바꿀 수 있다.
실제 널에 대해 !!를 적용하면 NPE가 발생
@Test
fun `널 아님 단언`() {
fun ignoreNulls(s: String?): Int {
val sNotNull: String = s!!
return sNotNull.length
}
assertEquals(3, ignoreNulls("abc"))
assertThrows<NullPointerException> {
ignoreNulls(null)
}
}
⚠️ !! 단언문 사용 시 주의사항이 있다.
!!를 사용해서 발생하는 NPE 예외의 스택 트레이스(stack trace)에는 어떤 파일의 몇 번째 줄인지에 대한 정보는 들어있지만, 어떤 식에서 예외가 발생했는지에 대한 정보는 들어있지 않다.
어떤 값이 널이었는지 확실히 하기 위해 여러 !! 단언문을 한 줄에 함께 쓰는 일을 피하자.
// 아래와 같은 식으로 코드를 작성하지 말자
person.company!!.address!!.country
let 함수
let 함수를 사용하면 널이 될 수 있는 식을 더 쉽게 다룰 수 있다.
let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음에
그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리
let 함수는 자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다.
널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되
널이 될 수 없는 타입을 인자로 받는 람다를 let에 전달
@Test
fun `let 함수`() {
fun sendEmailTo(email: String): String {
return "Sending email to $email"
}
var email: String? = "aaron@example.com"
assertEquals("Sending email to aaron@example.com",
email?.let { sendEmailTo(it) })
email = null
assertEquals(null,
email?.let { sendEmailTo(it) })
}
let을 쓰면 긴 식의 결과를 저장하는 변수를 따로 만들 필요가 없다.
여러 값이 널인지 검사해야 한다면 let 호출을 중첩시켜서 처리할 수 있다.
그렇게 let을 중첩시켜 처리하면 코드가 복잡해져서 알아보기 어려워진다.
그런 경우 일반적인 if를 사용해 모든 값을 한꺼번에 검사하는 편이 낫다.
나중에 초기화할 프로퍼티 (lateinit)
코틀린에서 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고,
특별한 메소드 안에서 초기화할 수는 없다.
코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.
프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 그 프로퍼티를 초기화해야 한다.
그런 초기화 값을 제공할 수 없으면 널이 될 수 있는 타입을 사용할 수밖에 없다.
하지만 널이 될 수 있는 타입을 사용하면 모든 프로퍼티 접근에 널 검사를 넣거나 !! 연산자를 써야 한다.
프로퍼티를 여러 번 사용해야 할 경우 코드가 지저분해지는데
이를 해결하기 위해 lateinit 변경자를 사용해서 프로퍼티를 나중에 초기화(late-initialized)할 수 있다.
// AS-IS
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
// null로 초기화하기 위해 널이 될 수 있는 타입인 프로퍼티를 선언
private var myService: MyService? = null
@Before fun setUp() {
// setUp 메소드 안에서 실제 초깃값을 지정
myService = MyService()
}
@Test fun testAction() {
// 널 가능성에 신경 써야 하므로, !!나 ? 사용 필수
assertEquals("foo",
myService!!.performAction())
}
}
---
// TO-BE
class MyService {
fun performAction(): String = "foo"
}
class MyTest {
// 초기화하지 않고 널이 될 수 없는 프로퍼티를 선언
private lateinit var myService: MyService
@Before fun setUp() {
myService = MyService()
}
@Test fun testAction() {
// 널 검사를 수행하지 않고 프로퍼티를 사용
Assert.assertEquals("foo",
myService.performAction())
}
}
코틀린의 원시 타입
원시 타입: Int, Boolean …
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용
@Test
fun `원시 타입`() {
fun showProgress(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? …
코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일
코틀린에서 적절한 타입을 찾으려면 그 변수나 프로퍼티에 널이 들어갈 수 있는지만 고민하면 된다.
@Test
fun `널이 될 수 있는 원시 타입`() {
data class Person(val name: String,
val age: Int? = null) {
fun isOlderThan(other: Person): Boolean? {
if (age == null || other.age == null)
return null
return age > other.age
}
}
assertEquals(false, Person("Sam", 35).isOlderThan(Person("Amy", 42)))
assertEquals(null, Person("Sam", 35).isOlderThan(Person("Jane")))
}
숫자 변환
코틀린과 자바의 가장 큰 차이점 중 하나는 숫자를 변환하는 방식
코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우 조차도 자동 변환은 불가능
@Test
fun `숫자 변환`() {
val i = 1
val l: Long = i // 컴파일 오류: Type mismatch. Required: Long, Found: Int
val l2: Long = i.toLong()
}
코틀린은 모든 원시 타입에 대한 변환 함수를 제공
ex) toByte(), toShort(), toChar() …
표현 범위가 더 넓은 타입으로 변환하는 함수도 있고,
표현 범위가 더 좁은 타입으로 변환하면서, 값을 벗어나는 경우 일부를 잘라내는 함수(Long.toInt())도 존재
자바에서는 Object가 클래스 계층의 최상위 타입이라면 코틀린에서는 Any 타입이 원시 타입(Int 등)을 포함한 모든 타입 조상 타입
@Test
fun `Any 타입 테스트`() {
val anyInt: Any = 42
val 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을 쓸 때 유용
interface Processor<T> {
fun process() : T
}
@Test
fun `unit type`() {
class NoResultProcessor : Processor<Unit> {
override fun process() {
// process
}
}
}
자바의 Void
의미: 반환값이 없음을 표현
특징: 실제로 반환되는 값이 없음
코틀린의 Unit
의미: 반환값이 없음을 나타내지만, 실제로는 단 하나의 인스턴스인 Unit 객체 반환
특징: 함수형 프로그래밍에서 '단 하나의 인스턴스만 갖는 타입'을 의미
코틀린의 Nothing
의미: 함수가 정상적으로 종료되지 않음을 표현
특징: 예외를 던지거나 무한 루프에 빠지는 함수에서 사용. 모든 타입의 하위 타입
Nothing 타입
코틀린에는 성공적으로 값을 돌려주는 일이 없는 '반환 값'이라는 개념 자체가 의미 없는 함수가 일부 존재한다
주로 예외를 던지거나 무한 루프에 빠지는 함수들은 정상적으로 종료되지 않기 때문에 반환값이 없음
이러한 함수들은 Nothing 타입을 반환
@Test
fun `nothing`() {
class Address(val streetAddress: String, val zipCode: Int,
val city: String, val country: String)
class Company(val name: String, val address: Address?)
fun fail(message: String) : Nothing {
throw IllegalStateException(message)
}
val company = Company("JetBrains", Address("ABC Street", 42, "Seoul", "KOREA"))
val address = company.address ?: fail("No address")
assertEquals("Seoul", address.city)
val company2 = Company("JetBrains", null)
assertThrows<IllegalStateException> {
company2.address ?: fail("No address")
}
}
Nothing 타입은 아무 값도 포함하지 않는다.
따라서, 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터만 사용 가능
컴파일러는 Nothing이 반환 타입인 함수가 정상 종료되지 않음을 알고 그 함수를 호출하는 코드 분석 시 사용
컬렉션과 배열
널 가능성과 컬렉션
컬렉션 안에 널 값을 넣을 수 있는지 여부는 어떤 변수의 값이 널이 될 수 있는지 여부와 마찬가지로 중요
fun addValidNumbers(numbers: List<Int?>) {
var sumOfValidNumbers = 0
var invalidNumbers = 0
// 리스트의 원소에 접근하면 Int? 타입의 값 획득
for (number in numbers) {
// 따라서 널 검사 필요
if (number != null) {
sumOfValidNumbers += number
} else {
invalidNumbers++
}
}
println("Sum of valid numbers: $sumOfValidNumbers")
println("Invalid numbers: $invalidNumbers")
}
널이 될 수 있는 값으로 이루어진 컬렉션으로 널 값을 걸러내는 경우가 자주 있다보니
코틀린 표준 라이브러리는 filterNotNull 함수를 제공
fun addValidNumbers(numbers: List<Int?>) {
val validNumbers = numbers.filterNotNull()
println("Sum of valid numbers: ${validNumbers.sum()}")
println("Invalid numbers: ${numbers.size - validNumbers.size}")
}
읽기 전용과 변경 가능한 컬렉션
코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 하나는
코틀린에서는 컬렉션안의 데이터에 접근하는 인터페이스와
컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점
일반적인 읽기 전용 라이브러리를 사용하려면 kotlin.collections.Collection 라이브러리를 사용
컬렉션의 데이터를 수정하려면 kotlin.collections.MutableCollection 인터페이스를 사용
원소를 추가/삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 제공
코틀린 컬렉션과 자바
코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스
두 가지 표현 제공
이런 성질로 컬렉션의 변경 가능성과 관련해 자바에서 중요한 문제가 발생
자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로,
코틀린에서 읽기 전용 컬렉션으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용 변경 가능
/* Kotlin */
object CollectionInterop {
fun getReadOnlyList(): List<String> {
// 읽기 전용 컬렉션으로 선언된 객체
return listOf("A", "B", "C")
}
}
/* Java */
@Test
void testModifyReadOnlyList() {
List<String> readOnlyList = CollectionInterop.getReadOnlyList();
readOnlyList.set(0, "Z");
// 자바 코드에서는 코틀린의 읽기 전용 컬렉션 내용 변경 가능
assertEquals("Z", readOnlyList.get(0));
}
객체의 배열과 원시 타입의 배열
코틀린 배열은 타입 파라미터를 받는 클래스
fun main(args: Array<String>) {
for (i in args.indices) {
println("Argument $i is: ${args[i]}")
}
}
배열의 원소 타입은 바로 그 타입 파라미터에 의해 정해지고, 코틀린에서 배열을 만드는 방법은 다양하다.
/*
* `arrayOf` 함수에 원소를 넘기면 배열을 만들 수 있다.
*/
@Test
fun `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이고
* 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다.
* 원소 타입이 널이 될 수 있는 타입인 경우에만 이 함수를 사용 가능
*/
@Test
fun `test arrayOfNulls`() {
val array = arrayOfNulls<String>(3)
assertEquals(3, array.size)
assertNull(array[0])
assertNull(array[1])
assertNull(array[2])
}
/*
* `Array 생성자`는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화
* `arryOf`를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용
*/
@Test
fun `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(""))
}
/**
* 컬렉션의 모든 요소를 포함하는 형식화된 배열을 반환
*/
@Test
fun `test toTypedArray`() {
val strings = listOf("a", "b", "c")
assertEquals("a/b/c", "%s/%s/%s".format(*strings.toTypedArray()))
}
/**
* [separator]를 사용하고 제공된 경우 주어진 [prefix] 및 [postfix]를 사용하여
* 구분된 모든 요소에서 문자열을 생성
*/
@Test
fun `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 = "}"))
}
/**
* 각 요소에 대해 지정된 작업을 수행하여 요소에 순차적 인덱스를 제공
*/
@Test
fun `test forEachIndexed`() {
fun testForEachIndexed(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"))
}