arrayOf 을 통해 배열 자료구조를 생성할 수 있다.val array = arrayOf(100, 200)
array.plus(300) // 배열에 새로운 원소 추가
array.withIndex() 를 통해 인덱스와 값을 한꺼번에 조회할 수 있다.for (i in array.indices)) { // 0 부터 마지막 idex 까지의 Range
println(array[i])
}
for ( (idx, value) in array.withIndex()) {
}
코틀린에서 컬렉션을 선언할 때는, 반드시 불변인지 가변인지 명시해주어야 한다.
불변 컬렉션: 컬렉션에 조회를 제외한 원소를 추가 및 삭제 불가능하다. 단, 레퍼런스 타입의 필드에는 접근하여 변경이 가능하다.가변 컬렉션: 컬렉션에 원소 추가 및 삭제가 가능하다.

기본적으로 ArrayList 를 상속받고 있기 때문에 자바에서 제공하는 기능을 모두 제공한다.
listOf: 불변 리스트를 생성
mutableListOf: 가변 리스트를 생성
참고로 빈 배열을 생성할 때는, 타입을 추론할 수 있는 경우를 제외하고는 타입을 명시해주어야 한다.
val numbers = listOf(100, 200) // Arrays.asList()
val mutable_numbers = mutableListOf(100, 200)
val emptyList = emptyList<Int>() // 타입 명시
numbers.add(300) // 값 추가
private fun userNumbers(numbers: List<Int>) {
}
println(numbers[0]) // numbers.get(0)
for (number in numbers) {
println(number)
}
for ((idx, value) in numbers.withIndex()) {
println("${idx} ${number}")
}
집합은 리스트와 다르게 순서가 없고, 같은 원소는 하나만 존재할 수 있다. 가변과 불변 집합 모두 LinkedHashSet 을 상속받아서 구현된다.
val numbers = setOf(100, 200) // 불변 집합
val numbers = mutableSetOf(100, 200) // 가변 집합
가변과 불변 맵 모두 LinkedHashMap 을 상속받아서 구현된다.
val newMap = mapOf(1 to "MONDAY", 2 to "TUESDAY") // 불변 맵(정적 팩터리 메서드)
val oldMap = mutableMapOf<Int, String>() // 가변 맵
oldMap[1] = "MONDAY" // oldMap.put(1, "MONDAY")
맵 활용
키만 가져오는 경우 Map.keys 를 사용하고, 키와 값을 한꺼번에 가져오는 경우 Map.entries 를 사용한다.
for (kery in oldMap.keys) {
println(key)
println(oldMap[key]) // oldMap.get(key)
}
for ((key, value) in oldMap.entries) {
println(key, value)
}
? 위치에 따라 다음과 같이 null 가능성의 의미가 달라진다.
List<Int?> : 리스트에는 null이 들어갈 수 있지만, 리스트 자체는 null 이 아니다.List<Int>? : 리스트에는 null이 들어갈 수 없지만, 리스트 자체는 null 이 될 수 있다. List<Int?>? : 리스트에는 null이 들어갈 수 있고, 리스트 자체도 null 이 될 수 있다. 🔖 자바와 함께 사용할 때 주의점
코틀린 쪽의 컬렉션이 자바에서 호출되면, 컬렉션 내용이 변할 수 있다. 혹은, 코틀린 쪽에서Collections.unmodifableXXX()를 활용하면 변경 자체를 막을 수는 있다.
자바 코드가 있는 상황에서, 코틀린 코드로 추가 기능 개발을 하기 위해 확장함수와 확장 프로퍼티라는 개념이 등장하였다. Standard Library 또는 다른 사람이 만든 라이브러리를 사용할 때 함수를 추가하기가 어렵기 때문이다.
확장 함수를 사용하면, 함수를 기존에 클래스 안에 있던 멤버 함수 같이 사용할 수 있다. 즉 어떤 클래스 안에 있는 메서드처럼 호출 할 수 있지만, 함수는 외부에 생성하는 것이다.
fun main() {
val str = "ABC"
println(str.lastChar())
}
fun String.lastChar(): Char { // String 클래스 확장
return this[this.length - 1] // this를 통해 실제 클래스 안의 값에 접근
}
확장 함수는 다음과 같은 특징을 가진다.
private 혹은 protected 멤버를 가져올 수 없다.val train: Train = Train()
train.isExpensive() // Train의 확장함수
val srt1: Train = Srt()
srt1.isExpensive() // Train의 확장함수
val srt2: Srt = Srt()
srt2.isExpensive() // Srt의 확장함수
infix는 함수를 호출하는 새로운 방법으로, 변수.함수이름(매개변수) 대신 변수 함수이름 매개변수 형식으로 호출하는 방식이다.
예를 들어, 아래 함수를 부를 때는 infix 가 붙어 있으므로 3 add 4 형태로 부를 수 있다.
infix fun Int.add(other: Int): Int {
return this + other
}
inline 은 함수가 호출되는 대신, 함수를 호출하는 지점에 함수 본문을 복사 붙여넣기 해주는 키워드이다.
실제 함수가 호출되지 않고 로직 자체가 바이트 코드로 들어온 것을 볼 수 있는데, 이는 함수를 파라미터로 전달할 때 오버헤드를 줄일 수 있다는 장점이 존재한다.(단, 성능 측정과 함께 신중하게 사용되어야 한다)

자바에서는, 함수형 인터페이스를 통해 메서드 자체를 넘기는 것처럼 보이게 사용하였다.즉, 자바에서 함수는 변수에 할당되거나 파라미터로 전달할 수 없는 것이다.
하지만 코틀린에서는 함수가 그 자체로 값이 될 수 있다.
아래는 특정 조건에 따라서 과일을 찾는 예제이며, 람다 생성 방법은 다음과 같이 2가지가 존재한다.
val fruits = listOf(
Fruit("사과", 1_000)
Fruit("바나나", 2_000)
)
// 1. 람다 생성 방법(1)
val isApple = fun(fruit: Fruit): Boolean { // 함수를 변수에 할당
return fruit.name == "사과"
}
// 2. 람다 생성 방법(2)
val isApple2 = { fruit: Fruit -> fruit.name == "사과" }
🔖 람다의 타입
함수 자체의 타입을 표현할 때는,(파라미터 타입,,) -> 반환 타입형태로 표기한다.val isApple: (Fruit) -> Boolean = {...}
자바에서 Predicate 과 같이 인터페이스가 아니라, 함수 자체를 타입으로 받고 있는 것을 볼 수 있다.
filterFruits(fruits, isApple) // 파라미터로 함수 전달
private fun filterFruits(
fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {
val results = mutableListOf<Fruit>()
for (fruit in fruits) {
if (filter.invoke(fruit)) {
results.add(fruit)
}
}
}
람다의 특징은 다음과 같다.
fruit -> fruit 과 같이 파라미터가 하나일 뿐에는 줄여서 it 으로 사용할 수 있으며 타입도 생략 가능하다.val isApple2 = { it.name == "사과" }
filterFruits(fruits) { it.name == "사과" }
return이 없어도 마지막 줄의 결과가 람다의 반환값이 된다.filterFruits(fruits) { fruit ->
println()
fruit.name == "사과"
}
return carNames.map({ carName -> Car(carName) })
return carNames.map({ Car(it) })
return carNames.map { Car(it) }
return carNames.map(::Car)
자바에서는, 람다를 쓸 때 사용할 수 있는 변수에 제약이 있다. 예를 들어, 다음과 같이 바깥에서 선언된 변수는 사용하지 못한다.
String targetFruitName = "바나나"
targetFruitName = "수박"
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));
하지만, 람다에서는 이것이 바로 Closure 라는 개념을 통해 가능해진다. Clousure 란, 람다가 실행되고 있는 시점에 사용되는 모든 변수들의 정보를 포획한 데이터 구조를 의미하기 때문이다. 따라서, 값이 바뀌는 non-final 변수도 람다에서 참조하여 사용할 수 있게 된다.
그렇다면 어떻게 변수를 포획(capture)할 수 있을까?
코틀린에서는 단순 값을 복사하는 방식이 아닌, 어떤 변수를 Object로 만든 다음 그 Object의 레퍼런스를 가리키게끔 한다. 즉, 포획 시점에 상관 없이 해당 변수에 값이 바뀌면 람다에서 사용할 때도 영향이 미치게 되므로 주의해야 한다.
또한 레퍼런스를 공유하는 방식으로 이루어지는 만큼, thread-safe 하지 않으므로 동시성 문제를 고려해야 한다.


private fun filterFruits(
fruits: List<Fruit>, filter: (Fruit) -> Boolean
): List<Fruit> {
return fruits.filter(filter)
}
all : 조건을 모두 만족하면 true, 아니면 falsenone : 조건을 모두 불만족하면 true, 아니면 falseany : 조건을 하나라도 만족하면 true, 아니면 falsesortedBy : 기본 오름차순 정렬distinctBy : 변형된 데이터 기준으로 중복 제거first() : 첫 번째 값을 가져오고, null 일 경우 예외 발생firstOrNull() : 첫 번째 값 혹은 null 을 가져옴last() : 마지막 값을 가져오고, null 일 경우 예외 발생리스트를 맵으로 변경하고 싶다면, groupBy { 키, 값 } 를 사용하면 된다.
아래 예제를 보면 그 과일 이름에 해당되는 과일들이 List<Fruit> 으로 되어있는 맵이 생성되는 것을 볼 수 있다.
val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }