Kotlin
val array = arrayOf(100, 200)
// array.indices는 0부터 마지막 index까지의 Range이다.
for (i in array.indices) {
println("${i} ${array[i]}")
}
// withIndex() 를 사용하면, 인덱스와 값을 한 번에 가져올 수 있다.
for ((idx, value) in array.withIndex()) {
println("$idx $value")
}
// 값을 쉽게 넣을 수도 있다. (자바에서는 복사해야 했다)
array.plus(300)
(idx, value)는 구조분해라는 문법인데, 다음 챕터에서 다루게 된다..plus() 메서드로 값을 쉽게 추가할 수 있다.array.indices는 0부터 마지막 index까지의 Range를 반환해준다.
public val <T> Array<out T>.indices: IntRange
get() = IntRange(0, lastIndex)

Collection을 만들자마자 Collections.unmodifiableList() 등을 붙여준다.Reference Type인 Element의 필드는 바꿀 수 있다.Kotlin
// listOf를 통해 '불변 리스트'를 만든다.
val numbers = listOf(100, 200)
// emptyList<타입>() 으로 빈 리스트를 만들 수 있다.
// 타입 추론이 가능하다면 생략할 수 있다.
val emptyList = emptyList<Int>()
// 하나를 가져오기
println(numbers[0])
// For Each
for (number in numbers) {
println(number)
}
// 전통적인 For문 느낌
for ((index, number) in numbers.withIndex()) {
println("$index $number")
}
// 가변(Mutable) 리스트를 만들고 싶다면?
val mutableNumbers = mutableListOf(100, 200)
mutableNumbers.add(300)
컬렉션 생성 시 <> 생략이 가능한 경우 예시
printNumbers(emptyList()) // 이런 경우 타입 생략 가능
private fun printNumbers(numbers: List<Int>) {
}
[!TIP] Tip
우선은 불변 리스트를 만들고, 꼭 필요한 경우 가변 리스트로 바꾸자.
// 불변 집합
val numbers = setOf(100, 200)
// For Each
for (number in numbers) {
println(number)
}
// 전통적인 For문 느낌
for ((index, number) in numbers.withIndex()) {
println("$index $number")
}
// 가변(Mutable) 집합을 만들고 싶다면?
// 기본 구현체는 LinkedHashSet이다.
val mutableNumbers = mutableSetOf(100, 200)
// 가변 Map
// 타입을 추론할 수 없어, 타입을 지정해주었다.
val map = mutableMapOf<Int, String>()
// Java처럼 put을 쓸 수도 있고, map[key] = value 을 쓸 수도 있다.
map[1] = "MONDAY"
map[2] = "TUESDAY"
// mapOf(key to value) 를 사용해 불변 map을 만들 수 있다.
mapOf(1 to "MONDAY", 2 to "TUESDAY") // 중위 호출
// 순회
for (key in map.keys) {
println(key)
println(map[key])
}
for ((key, value) in map.entries) {
println(key)
println(value)
}
? 위치에 따라 null 가능성 의미가 달라지므로 차이를 잘 이해해야 한다.
List<Int?>: 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님List<Int>?: 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음List<Int?>?: 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음Java는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다.
Collections.unmodifiableXXX()를 활용해 변경 자체를 막을 수 있다.Java는 nullable 타입과 non-nullable 타입을 구분하지 않는다.
null을 추가할 수 있다.Kotlin에서 Java 컬렉션을 가져다 사용할 때 플랫폼 타입을 신경써야 한다.
List<Integer>를 Kotlin에서 받을 때, List<Int?>, List<Int>?, List<Int?>? 중 어떤 타입인지 알 수 없다. (플랫폼 타입으로 처리)어떤 클래스 안에 있는 메소드처럼 호출할 수 있지만, 함수 선언은 클래스 밖에 있는 함수를 확장함수라고 한다.
기존 Java 코드 위에 자연스럽게 코틀린 코드를 추가하거나, Java로 만들어진 라이브러리를 유지보수 및 확장할 때 Kotlin 코드를 덧붙일수 있도록 하기 위해 등장하였다.
기본 문법
fun 확장하려는클래스.함수이름(파라미터): 리턴타입 {
// this를 이용해 실제 클래스 안의 값(수신 객체)에 접근
}
확장하려는클래스: 수신 객체 타입this: 수신 객체예시
fun String.lastChar(): Char {
return this[this.length - 1]
}
확장 함수의 특징
1. 캡슐화 유지
private 또는 protected 멤버에 접근할 수 없다.멤버함수 우선
정적 타입에 의한 호출
open class Train(
val name: String = "새마을기차",
val price: Int = 5_000,
)
fun Train.isExpensive(): Boolean {
println("Train의 확장함수")
return this.price >= 10000
}
class Srt : Train("SRT", 40_000)
fun Srt.isExpensive(): Boolean {
println("Srt의 확장함수")
return this.price >= 10000
}
fun main() {
val train: Train = Train()
train.isExpensive() // Train의 확장함수
val srt1: Train = Srt()
srt1.isExpensive() // Train의 확장함수 (srt1의 정적 타입이 Train)
val srt2: Srt = Srt()
srt2.isExpensive() // Srt의 확장함수
}
Java에서의 호출
// StringUtilsKt.kt 파일에 선언된 확장함수
StringUtilsKt.lastChar("ABC");
public static char lastChar(String $this) { ... } 이렇게 컴파일된다.StringUtilsKt.class 기반 클래스파일이 만들어지므로, StringUtilsKt.lastChar("ABC"); 이렇게 호출하는 것이다.fun String.lastChar(): Char {
return this[this.length - 1]
}
// 위 함수를 프로퍼티로 만들 수도 있다.
val String.lastChar: Char
get() = this[this.length - 1]
변수.함수이름(argument) 대신 변수 함수이름 argument 형식으로 호출할 수 있다.infix 키워드를 사용해 정의하며, 멤버함수에도 붙일 수 있다.// 일반 확장함수
fun Int.add(other: Int): Int {
return this + other
}
// infix 확장함수
infix fun Int.add2(other: Int): Int {
return this + other
}
fun main() {
3.add(4)
3.add2(other = 4)
3 add2 4 // infix 호출
}
infix 함수는 연산처럼 자연스럽게 읽히는 DSL 스타일의 코드를 만들 때 유용하다고 한다.score shouldBeGreaterThan 90inline fun Int.add(other: Int): Int {
return this + other
}
fun main() {
3.add(4)
}
int var10000 = 3 + 4;fun createPerson(firstName: String, lastName: String): Person {
// 로직이 중복된다.
if (firstName.isEmpty()) {
throw IllegalArgumentException("firstName은 비어있을 수 없습니다! 현재 값 : $firstName")
}
if (lastName.isEmpty()) {
throw IllegalArgumentException("lastName은 비어있을 수 없습니다! 현재 값 : $lastName")
}
return Person(firstName, lastName, 1)
}
// 지역함수로 리팩토링
fun createPersonRefactored(firstName: String, lastName: String): Person {
fun validateName(name: String, fieldName: String) {
if (name.isEmpty()) {
throw IllegalArgumentException("${fieldName}은 비어있을 수 없습니다! 현재 값 : $name")
}
}
validateName(firstName, "firstName")
validateName(lastName, "lastName")
return Person(firstName, lastName, 1)
}
자바에서 람다를 다루는 방법에 대한 내용은 생략한다.
여기서 다시 짚고 가야할 부분은 Java에서 함수는 2급 시민으로, 변수에 할당되거나 파라미터로 직접 전달될 수 없다는 점이다. (항상 인터페이스를 통해 간접적으로 다뤄진다.)
// 방법 1
val isApple = fun(fruit: Fruit): Boolean {
return fruit.name == "사과"
}
// 방법 2
val isApple2 = { fruit: Fruit -> fruit.name == "사과" }
방법 2를 사용한다. // 방법 1
isApple(Fruit("사과", 1000))
// 방법 2
isApple.invoke(Fruit("사과", 1000))
함수의 타입은 (파라미터 타입...) -> 반환 타입 으로 표현한다.
private fun filterFruits(
fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {
val results = mutableListOf<Fruit>()
for (fruit in fruits) {
// if (filter(fruit)) 와 동일
if (filter.invoke(fruit)) {
results.add(fruit)
}
}
return results
}
// 사용
filterFruits(fruits, isApple)
filterFruits(fruits, { fruit: Fruit -> fruit.name == "사과" })
후행 람다(Trailing Lambda)
() 밖에 람다를 작성할 수 있다.filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과" }
it
it을 사용하여 파라미터를 바로 참조할 수 있다.filterFruits(fruits) { it.name == "사과" }
it를 사용하는 것 보단 파라미터를 밝혀주는 것을 선호한다고 하신다.마지막 줄이 반환 값
filterFruits(fruits) { fruit ->
println("사과만 받는다..!!")
fruit.name == "사과" // 이 줄이 반환값이 됨
}
람다식이 이미 존재하는 함수 호출 1줄로만 이루어져 있을 때, 람다를 더 간결하게 표현하기 위한 문법이다.
val numbers = listOf(1, 2, 3)
// 람다식
numbers.forEach { n -> println(n) }
// 메서드 참조
numbers.forEach(::println)
4가지 유형이 존재한다.
1. 최상위(top-level) 함수 참조
fun isApple(fruit: Fruit) = fruit.name == "사과"
filterFruits(fruits, ::isApple)
멤버 함수 참조
class Person(val name: String) {
fun printName() = println(name)
}
val person = Person("홍길동")
listOf(person).forEach(Person::printName)
Person::printName는 “Person 타입의 인스턴스의 printName() 호출”을 의미함.생성자 참조(Constructor Reference)
class Fruit(val name: String)
val factory = ::Fruit // (String) -> Fruit
val fruit = factory("사과")
확장 함수 참조
fun String.lastChar() = this.last()
val ref = String::lastChar
println(ref("ABC")) // C
final 또는 effectively final 이어야 한다는 제약이 있다.// Kotlin
var targetFruitName = "바나나"
targetFruitName = "수박"
// 외부 변수 targetFruitName이 변경 가능해도 람다 내에서 사용 가능
filterFruits(fruits) { it.name == targetFruitName }
use 함수는 람다, 확장함수, inline 함수의 좋은 활용 예시이다.public inline fun <T : Closeable?, R> T.use(block: (T) -> R): Ruse는 Closeable의 확장함수이다.inline 함수로 성능 오버헤드를 줄였다.람다를 파라미터로 받아 리소스를 사용하고 자동으로 close 해준다.fun readFile(path: String) {
BufferedReader(FileReader(path)).use { reader ->
println(reader.readLine())
} // 람다를 실행, 리소스 자동 close
}
코틀린에는 자바 Stream과 동일한 스트림 개념은 없다. Java는 컬렉션에 대해 스트림 API를 따로 만들어서 처리한다.
하지만 Kotlin은 언어 설계가 다르기 때문에 Stream을 별도로 만들지 않았다. 대신 Collection의 확장 함수들이 Stream처럼 동작한다.
다만, 이는 자바 스트림과 작동 방식 구조가 약간 다르다. 즉시 실행(Eager evaluation) 방식이다.
코틀린에서는 lazy 처리 방식이 필요할 때 Sequence를 사용해야 한다. (ex. list.asSequence())
// 예제용 데이터 클래스
data class Fruit(
val id: Long,
val name: String,
val factoryPrice: Long,
val currentPrice: Long,
)
// 사과만 주세요!
val apples = fruits.filter { fruit -> fruit.name == "사과" }val apples = fruits.filterIndexed { idx, fruit ->
println(idx)
fruit.name == "사과"
}// 사과의 가격들을 알려주세요!
val applePrices = fruits.filter { it.name == "사과" }
.map { it.currentPrice }val applePrices = fruits.filter { it.name == "사과" }
.mapIndexed { idx, fruit ->
println(idx)
fruit.currentPrice
}val values = fruits.filter { it.name == "사과" }
.mapNotNull { it.nullOrValue() } // null일 수 있는 값을 반환하는 함수xxxIndexed, mapNotNull 정도를 제외하면 자바와 Syntax sugar가 비슷하다.
true, 그렇지 않으면 false를 반환한다.// 모든 과일이 사과인가요?!
val isAllApple = fruits.all { it.name == "사과" }true, 그렇지 않으면 false를 반환한다.val isNoApple = fruits.none { it.name == "사과" }true, 그렇지 않으면 false를 반환한다.// 출고가 10,000원 이상의 과일이 하나라도 있나요?!
val hasExpensiveFruit = fruits.any { it.factoryPrice >= 10_000 }// 총 과일 개수가 몇 개인가요?!
val fruitCount = fruits.count()// 낮은 가격 순으로 보여주세요!
val sortedFruits = fruits.sortedBy { it.currentPrice }val sortedFruits = fruits.sortedByDescending { it.currentPrice }// 과일이 몇 종류 있죠?!
val distinctFruitNames = fruits.distinctBy { it.name }.map { it.name }first는 예외 발생, firstOrNull은 null 반환)last는 예외 발생, lastOrNull은 null 반환)groupBy: 주어진 키를 기준으로 그룹화하여 Map<K, List<T>>를 만든다.
// 과일이름 -> List<과일> Map이 필요해요!
val map: Map<String, List<Fruit>> = fruits.groupBy { it.name }
// Key와 value를 동시에 처리할 수도 있다.
// 과일이름 -> List<출고가> Map이 필요해요!
val map2: Map<String, List<Long>> = fruits.groupBy(
{ it.name }, { it.factoryPrice }
)
associateBy: 주어진 키를 기준으로 Map<K, T>를 만든다. (키가 중복되면 마지막 값만 남는다)
// id -> 과일 Map이 필요해요!
val map: Map<Long, Fruit> = fruits.associateBy { it.id }
// Key와 value를 동시에 처리할 수도 있다.
// id -> 출고가 Map이 필요해요!
val map2: Map<Long, Long> = fruits.associateBy(
{ it.id }, { it.factoryPrice }
)
생성된 Map에 대해서도 filter와 같은 함수들을 대부분 사용할 수 있다.
val map: Map<String, List<Fruit>> = fruits.groupBy { it.name }
.filter { (key, value) -> key == "사과" }
예제용 데이터
val fruitsInList: List<List<Fruit>> = listOf(
listOf( ... ), // 사과 리스트
listOf( ... ), // 바나나 리스트
listOf( ... ) // 수박 리스트
)
flatMap: 중첩된 컬렉션의 각 element를 원하는 기준으로 변환한 후 하나의 리스트로 flatten 한다.
// 출고가와 현재가가 동일한 과일을 골라주세요!
val samePriceFruits = fruitsInList.flatMap { list ->
list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
}
// list 안에 list filter가 있으므로, 다음과 같이 리팩토링할 수 있다.
fruitsInLIst.flatMap { list -> list.samePriceFilter }
val List<Fruit>.samePriceFilter: List<Fruit> // 확장 함수
get() = this.filter(Fruit::isSamePrice) // isSamePrice는 도메인 로직에
flatten: 중첩된 컬렉션을 단순히 하나의 리스트로 flatten 한다.
// List<List<Fruit>>를 List<Fruit>로 그냥 바꾸어주세요!
val flattenedFruits = fruitsInList.flatten()