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