Kotlin - 콜렉션!
콜렉션
java kotlin 콜렉션을 사용하며 immutable콜렉션, mutable콜렉션을 제공한다.
mutable콜렉션은 함수형 구조 작성을 위해 반드시 필요한 클래스이다.
하지만 java 의 경우 immutable콜렉션, mutable콜렉션 모두 동일한 인터페이스(List, Map) 을 구현하며, add, put 과 같은 함수 호출시 실행시간에 UnsupportedOperationException 을 발생시킨다.
반면 kotlin 은 컴파일 시점에서 mutable 메서드 사용시 오류를 발생시킨다.
Array
kotlin 에서 배열을 사용할땐 Array 클래스를 와 arrayOf 팩토리 메서드를 통해 배열을 생성한다.
java 배열과 동일하게 선언과 동시에 배열의 크기가 정해지며 크기 변경이 불가능하지만 내부 요소는 변경 가능한 콜렉션이다.
최적화를 위한 낮은 수준의 프로그래밍이 가능할 때만
Array를 사용하고, 이외에는List콜렉션 사용을 권장한다.
val array: Array<Any> = arrayOf(1, 2, "three")
array[0] = -1;
println(array.contentToString()) // [-1, 2, three]
null 로 채워진 배열 생성은 arrayOfNulls 메서드 사용
var nullArray: Array<String?> = arrayOfNulls<String>(5)
arrayOfNulls의 경우 요소의 타입 정의 필수
Array 를 생성하는 함수는 이 외에도 오토박싱, 언박싱 비용을 방지할 수 있도록
기본 타입의 Array 를 생성하는 함수가 정의되어 있다.
booleanArrayOfbyteArrayOfshortArrayOfcharArrayOfintArrayOflongArrayOffloatArrayOfdoubleArrayOf
Array 생성자
public class Array<T> {
public inline constructor(size: Int, init: (Int) -> T)
...
}
Array 의 기본생성자는 한개뿐이다,
인자값으로 크기값과 람다함수(반환값이 Int 인) 2개를 전달받는다.
반복적인 패턴의 Array 를 만들 때 사용할만 한다.
var array = Array(5) {i -> (i * i).toString()}
println(array.contentToString()) // [0, 1, 4, 9, 16]
in 연산자를 사용하면 요소가 해당 콜렉션 안에 있는지 확인 가능하다.
앞으로 소개할 List, ArrayList 에서도 사용 가능
val array: Array<Any> = arrayOf(1, 2, "three")
println("three" in array) // true
withIndex
값과 인덱스를 가지고 있는 IndexedValue 클래스를 반환하는 withIndex 메서드를 사용,
그리고 구조분해 연산자를 사용하면 아래와 같이 깔끔한 형태로 (index, value) 를 같이 사용할 수 있다.
val strings = arrayOf("this", "is", "an", "array", "of", "strings")
for ((index, value) in strings.withIndex()) {
println("Index: $index, value: $value")
}
/*
Index: 0, value: this
Index: 1, value: is
Index: 2, value: an
Index: 3, value: array
Index: 4, value: of
Index: 5, value: strings
*/
List, MutableList, Set
List, MutableList 은 kotlin 의 표준 콜렉션으로 모두 인터페이스이다.
List 는 immutable 콜렉션, set add remove 같은 메서드가 정의되어 있지 않다.
MutableList 는 mutable 표준 콜렉션
val list: List<Int> = listOf(1, 2, 3)
// list[0] = -1
// list.add(4)
// 에러 발생, set 명령도 없음
var mlist: MutableList<Int> = mutableListOf(1, 2, 3)
mlist[0] = -1
mlist.add(4)
println(mlist) // [-1, 2, 3, 4]
// 형 지정을 자동화 하려면 제너릭 지정 필요
val emptyList1: MutableList<String> = mutableListOf()
val emptyList2 = mutableListOf<String>()
val emptySet1: MutableSet<String> = mutableSetOf()
val emptySet2 = mutableSetOf<String>()
요소에 접근할 때에는 get 메서드나 인덱스 연산자를 사용할 수 있다.
val list: List<Int> = listOf(1, 2, 3)
println(list.get(0)) // 1
println(list[0]) // 1
ArrayList는MutableList인터페이스를 구현한 클래스.
listOf,mutableListOf,arrayListOf두 함수 모두ArrayList클래스를 반환한다.
MutableList,ArrayList두 클래스간 큰 차이는 없지만 표준 콜렉션 타입인MutableList사용을 권장한다.
또한kotlin은 함수형 코딩을 지향함으로mutable보다도immutable콜렉션 사용을 권장한다.
Set 콜렉션도 Set, MutableSet 으로 나뉜다.
마찬가지로 둘다 인터페이스이고 내부 구현체는 LinkedHashSet 이다.
var set = setOf(1, 2, 3, 4, 5)
println(set) // [1, 2, 3, 4, 5]
var mset = mutableSetOf(1, 2, 3, 4, 5)
println(mset) // [1, 2, 3, 4, 5]
fold
배열 형태의 콜렉션을 순회하면서 연산하고 싶을때 사용, reduce 와 비슷한 역할.
public inline fun <R> IntArray.fold(initial: R, operation: (acc: R, Int) -> R): R { ... }
초기값과 순회하면서 처리할 함수를 지정한다.
fun main() {
val nums = intArrayOf(1, 2, 3, 4, 5)
val result = nums.fold(0) { acc, i -> acc + i }
println(result) // 15
}
reduce
fold 와 동일한 기능을 수행하지만 초기값을 배열의 첫번째 값으로 사용한다.
public inline fun IntArray.reduce(operation: (acc: Int, Int) -> Int): Int { ... }
fun main() {
val nums = intArrayOf(1, 2, 3, 4, 5)
val result = nums.reduce() { acc, i -> acc + i }
println(result) // 15
}
contains
콜렉션 내부에 요소가 있는지 확인하는 메서드
val fruits: List<String> = listOf("Apple", "Banana", "Grape")
println(fruits.contains("Apple")) //true
println("Apple" in fruits) //true
immutable, mutable 변환
List 콜렉션 뿐 아니라 앞으로 소개할 콜렉션에서도 immutable, mutable 콜렉션으로 나뉘며
immutable, mualbe 간 변환할 수 있는 메서드를 제공한다.
val list = listOf(1, 2, 3)
val mlist = mutableListOf(1, 2, 3)
val list2 = mlist.toList()
val mlist2 = list.toMutableList()
구조분해, 오버라이딩 함수
List 에 기본적으로 component1 ~ component5 함수가 정의되어 있는데
아래 사용처럼 구조분해 문법 사용이 가능하다.
val range: CharRange = 'a'..'g'
val (a, b, c, d, e) = range.toList();
println("$a $b $c $d $e") // a b c d e
// val (a1, b1, c1, d1, e1, f1) = range.toList(); 에러 발생 component6 가 없기 때문
kotlin 의 연잔사 오버라이딩을 통해 +, - 연산자로 새로운 콜렉션을 생성해서 요소를 추가하거나 삭제할 수 있다.
val fruits: List<String> = listOf("Apple", "Banana", "Grape")
var fruits2: List<String> = fruits + "orange"
println(fruits2) // [Apple, Banana, Grape, orange]
var fruits3: List<String> = fruits - "Apple"
println(fruits3) // [Banana, Grape]
Pair, Triple
Pair 는 값이 2개인 튜플, Triple 값이 3개인 튜플, 둘다 kotlin 의 표준 콜렉션 클레스이다.
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable { ... }
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
) : Serializable { ... }
val p1: Pair<String, Int> = "a" to 1
val p2: Pair<String, Int> = Pair("b", 2)
val t1: Triple<String, String, String> = Triple("John", "Quincy", "Adams")
Pair Triple 모두 구조분해 연산자를 제공한다.
아래처럼 값을 추출하면 좀더 간결하게 값을 할당할 수 있다.
fun getFullName() = Triple("John", "Quincy", "Adams")
fun main() {
val (one, two, three) = getFullName()
println("$one $two $three") // John Quincy Adams
val (first, _, middle) = getFullName()
println("$first $middle") // John Adams
val (only) = getFullName()
println("$only") // John
val full = getFullName()
println("$full") // (John, Quincy, Adams)
}
중간에 값이 빌 경우 언더바를 통해 생략할 수 있다.
Map
Map 는 immutable 콜렉션.
MutableMap 는 mutable 표준 콜렉션.
Pair 객체를 사용해서 생성할 수 있다.
val p1 = "a" to 1
val p2 = Pair("b", 2)
val map = mapOf(p1, p2, "c" to 3)
val mmap = mutableMapOf(p1, p2, "c" to 3)
println(map) // {a=1, b=2, c=3}
println(mmap) // {a=1, b=2, c=3}
get, set 메서드는 인덱스 연산자로 대체해서 사용할 수 있다.
val mmap = mutableMapOf("hello" to "world", "foo" to "bar")
println(mmap.get("hello")) // world
println(mmap["foo"]) // bar
mmap["baz"] = "qux"
println(mmap) // {hello=world, foo=bar, baz=qux}
associate, associateWith
콜렉션간의 결합을 통해 Map 콜렉션 생성이 가능하다.
var keys: CharRange = 'a'..'f'
val map: Map<Char, String> = keys.associate { it to it.toString().repeat(5).capitalize() }
println(map) // {a=Aaaaa, b=Bbbbb, c=Ccccc, d=Ddddd, e=Eeeee, f=Fffff}
var keys: CharRange = 'a'..'f'
val map: Map<Char, Pair<Char, String>> = keys.associateWith { it to it.toString().repeat(5).capitalize() }
println(map) // {a=(a, Aaaaa), b=(b, Bbbbb), c=(c, Ccccc), d=(d, Ddddd), e=(e, Eeeee), f=(f, Fffff)}
contains
key, value 에 요소가 포함되어 있는지 검사하는 메서드를 제공한다.
val map = mapOf("hello" to "world", "foo" to "bar")
println(map.contains("hello")) // true, containsKey 와 동일
println(map.containsKey("hello")) // true
println(map.containsValue("bar")) // true
println("hello" in map) // true
구조분해, 오버라이딩 함수
Pair 콜렉션을 기반으로 반복문을 구성하는데
Pair 의 구조분해 문법과 같이 사용할 수 있다.
val mmap = mutableMapOf("hello" to "world", "foo" to "bar")
for (entry in mmap) {
println("${entry.key} : ${entry.value}")
}
for ((k, v) in mmap) {
println("$k : $v")
}
// hello : world
// foo : bar
List 콜렉션과 마찬가지로 Map 콜렉션에서도 오버라이딩 함수를 제공한다.
val map = mapOf("hello" to "world", "foo" to "bar")
var newMap1 = map + ("baz" to "quz")
println(newMap) // {hello=world, foo=bar, baz=quz}
var newMap2= map - "hello"
println(newMap2) // {foo=bar}
내부 반복자 메서드
kotlin 에서는 [filter, map] 과 같은 콜렉션을 쉽게 컨트롤할 수 있는 내부 반복자 메서드를 제공한다, 내부 반복자 메서드 는 즉시 처리되어 모든원소에 접근한다.
java 에선 성능상의 이유로 stream 을 통해 해당 함수를 호출시켰지만, kotlin 에선 편의성을 위해 바로 사용가능하다.
내부 반복자 메서드는 크기가 작은 콜렉션에서만 사용해야 한다.
chucked, windowed
콜렉션을 아래와 같이 윈도우 단위로 콜렉션을 분할하는 기능
val range: IntRange = 0..10
val chucked: List<List<Int>> = range.chunked(3)
println(chucked) // [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
사실 chucked 는 windowed 함수를 추상화 한 것
val range: IntRange = 0..10
// size, step, partialWindows
val chucked: List<List<Int>> = range.windowed(3, 3, true)
println(chucked) // [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
size: 각 윈도우 원소 수
step: 전진할 윈도우 개수 (default 1)
partialWindows: size 로 나누어 떨어지지 않을때 버림 여부 (deafult false 버림)
filter, map, joinToString
data class Product(
val name: String,
val price: Double,
val onSale: Boolean = false
)
fun generateList(): List<Product> {
val product1 = Product("product1", 2000.0, true)
val product2 = Product("product2", 1000.0, false)
val product3 = Product("product3", 3000.0, true)
return listOf(product1, product2, product3)
}
위 Product 클래스로 사용법 확인
fun main() {
var products: List<Product> = generateList();
var result: String = products
.filter { it.onSale }
.map { it.name }
.joinToString(separator = ", ")
println(result) // product1, product3
}
ifEmpty, ifBlank
fun generateList(): List<Product> {
val product1 = Product("product1", 2000.0, onSale = false)
val product2 = Product("product2", 1000.0, onSale = false)
val product3 = Product("product3", 3000.0, onSale = false)
return listOf(product1, product2, product3)
}
fun namesOfProductsOnSale1(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.ifEmpty { listOf("none") }
.joinToString(separator = ", ")
fun namesOfProductsOnSale2(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.joinToString(separator = ", ")
.ifEmpty { "none" }
fun main() {
var products: List<Product> = generateList();
var result1 = namesOfProductsOnSale1(products)
var result2 = namesOfProductsOnSale2(products)
println(result1) // none
println(result2) // none
}
sortedWith, compareBy
요소 정렬을 위한 스트림 메서드
fun main() {
var products: List<Product> = generateList();
products = products.sortedWith(compareBy({ it.price }, { it.name }))
println(products)
// [Product(name=product2, price=1000.0, onSale=false), Product(name=product1, price=2000.0, onSale=true), Product(name=product3, price=3000.0, onSale=true)]
}
filterIsInstance, filterIsInstanceTo
List<Any> 형식의 콜렉션을 필터링할 때에는 아래와 같은 문제가 발생할 수 있다.
fun main() {
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val strings: List<Any> = list.filter { it is String }
for (str in strings) {
str.length // error, Any 타입에는 length 가 없음
}
}
filterIsInstance 를 사용해 해당 요소만 뽑아내어 타입을 지정할 수 있다.
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val all: List<Any> = list.filterIsInstance<Any>()
val strings: List<String> = list.filterIsInstance<String>()
val ints: List<Int> = list.filterIsInstance<Int>()
val dates: List<LocalDate> = list.filterIsInstance(LocalDate::class.java)
사실 filterIsInstance 은 filterIsInstanceTo 함수를 구현한 함수이며
filterIsInstanceTo 를 사용하면 어떤 인스턴스에 해당 요소를 집어넣을지 지정 가능하다.
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val all: MutableList<Any> = list.filterIsInstanceTo(mutableListOf())
val strings: MutableList<String> = list.filterIsInstanceTo(mutableListOf<String>())
val ints: MutableList<Int> = list.filterIsInstanceTo(mutableListOf<Int>())
val dates: MutableList<LocalDate> = list.filterIsInstanceTo(mutableListOf<LocalDate>())
Iterable 클래스
기본적으로 콜렉션은 순회가능한 Iterable 클래스이다.
Iterable인터페이스 구현
data class Player(val name: String)
class Team(
val name: String,
val players: MutableList<Player> = mutableListOf()
) : Iterable<Player> {
fun addPlayers(vararg people: Player) = players.addAll(people)
override fun iterator(): Iterator<Player> = players.iterator()
}
fun main() {
val team = Team("Warriors")
team.addPlayers(
Player("Curry"),
Player("Thompson")
)
for (player in team) {
println(player)
// Player(name=Curry)
// Player(name=Thompson)
}
}
다음과 같이 operator 키워드로 iterator 함수만 재정의하면 Iterator 구현 없이 for...each 문을 실행할 수 있다.
class Team(
val name: String,
val players: MutableList<Player> = mutableListOf()
) {
fun addPlayers(vararg people: Player) = players.addAll(people)
}
operator fun Team.iterator(): Iterator<Player> = players.iterator()
시퀀스
kotlin 시퀀스는 java stream 과 비슷한 기능으로 콜렉션의 성능 향상을 위한 wrapper 라 볼 수 있다.
콜렉션의 first 와 같은 내부 반복자 메서드를 사용할 경우 전체 요소를 순회하는 문제가 발생한다.
반면 asSequence() 함수를 사용하면 동일하게 조건을 효율적으로 빠져나올 수 있다.
심지어 실제 result2 를 참조하기 전까지는 시퀀스가 실행되지 않는다.
var result1: Int = (100..200)
.map { it * 2 }
.filter { println("filter1: $it"); it % 3 == 0 }
.first();
/*
filter1: 200
filter1: 201
... 201 ~ 399 까지 출력
filter1: 400
*/
var result2: Int = (100..200).asSequence()
.map { it * 2 }
.filter { println("filter2: $it"); it % 3 == 0 }
.firstOrNull();
println(result2) // result2 를 참조하지 않으면 시퀀스 실행 X
/*
filter2: 200
filter2: 202
filter2: 204
204
*/
시퀀스 연산은 중간연산과 최종연산으로 나뉘며 최종연산까지 작성하여야 전체파이프라인이 구성되며 시퀀스가 동작한다.
map, filter 와 같은것을 중간연산,
first, toList 같은함수를 최종연산이라 한다.
시퀀스 최종값이 비어있을경우
first는 예외를 발생함으로firstOrNull사용을 추천.
시퀀스 생성 - sequenceOf, asSequence, generateSequence
- 원소는
sequenceOf iterable객체는asSequence
val numSequence1 = sequenceOf(3, 1, 4, 1, 5, 9)
val numSequence2 = listOf(3, 1, 4, 1, 5, 9).asSequence()
generateSequence 는 초기값과 다음값을 생성할 nextFunction 함수를 인자로 받아 시퀀스를 생성한다.
nextFunction 에서 계속 값을 반환하면 무한시퀀스가 생성된다.
내부적으로 재귀형식으로 함수가 구성됨
public fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> =
if (seed == null)
EmptySequence
else
GeneratorSequence({ seed }, nextFunction)
단순이 1씩 증가시키는 함수를 nextFunction 으로 지정, take 최종연산으로 값을 출력한다.
val buffer = StringBuffer();
var joinResult = generateSequence(1) { it + 1 }
.take(10)
.joinTo(buffer, ",")
println(joinResult) // 1,2,3,4,5,6,7,8,9,10
아래처럼 탈출조건을 삽입해도 된다.
val buffer = StringBuffer();
generateSequence(1) { if (it > 10) null else it + 1 }
.joinTo(buffer, ",")
println(buffer) // 1,2,3,4,5,6,7,8,9,10,11
탈출한다 해도 해당요소까지 시퀀스에 포함됨으로 아래처럼 직관적인 takeWhile 문을 사용하는것을 권장
val buffer = StringBuffer();
generateSequence(1) { it + 1 }
.takeWhile { it < 10 }
.joinTo(buffer, ",")
println(buffer) // 1,2,3,4,5,6,7,8,9
시퀀스 생성 - sequence, yield
sequence 와 yield 함수를 사용해 시퀀스를 생성가능하다.
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }
fun fibonacciSequence() = sequence {
var terms = Pair(0, 1)
while (true) {
yield(terms.first) // stop & return
terms = terms.second to terms.first + terms.second
}
}
fun main() {
val fibs = fibonacciSequence()
.take(10)
.toList()
println(fibs) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
다른언어의 yield 와 마찬가지로 stop interrupt를 발생시키며 값을 반환하는 함수
해당
yield는Coroutine의yield가 아닌SequenceScope내부에 정의된 함수임의 주의
public abstract class SequenceScope<in T> internal constructor() {
public abstract suspend fun yield(value: T)
public abstract suspend fun yieldAll(iterator: Iterator<T>)
}
rangeTo
범위클래스를 상속하면 .. 키워드를 사용해 범위타입 클래스인 ClosedRange 을 사용할 수 있다.
val range1: CharRange = 'a'..'f'
val range2: CharRange = 'a'.rangeTo('f')
범위클래스로 CharRange 외에도 IntRange, LongRange 등이 있으며 모두 ClosedRange 를 구현하고 있다.
ClosedRange 의 contains 메서드를 보면 infix 연산자로 비교문만 정의하면
아래와 같이 in 키워드를 사용할 수 있다.
public interface ClosedRange<T: Comparable<T>> {
// The minimum value in the range.
public val start: T
// The maximum value in the range (inclusive).
public val endInclusive: T
// Checks whether the specified [value] belongs to the range.
public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive
// Checks whether the range is empty.
public fun isEmpty(): Boolean = start > endInclusive
}
val startDate = LocalDate.now()
val midDate = startDate.plusDays(3)
val endDate = startDate.plusDays(5)
val overDate = startDate.plusDays(6)
val dateRange: ClosedRange<LocalDate> = startDate..endDate
println(startDate in dateRange) // true
println(midDate in dateRange) // true
println(endDate in dateRange) // true
println(overDate in dateRange) // false
Progression(수열, 연속)
ClosedRange<LocalDate> 타입이 수열처럼 연속된 값의 나열은 아니기 때문에 for...each 사용은 불가능하다.
public class CharRange(start: Char, endInclusive: Char) : CharProgression(start, endInclusive, 1), ClosedRange<Char> {...}
public class IntRange(start : Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> { ... }
public class LongRange(start: Long, endInclusive: Long) : LongProgression(start, endInclusive, 1), ClosedRange<Long> { ... }
CharRange, IntRange, LongRange 처럼 Iterable 인터페이스를 구현한 CharProgression, IntProgression, LongProgression 과 같은 클래스를 상속받아야 한다.
class LocalDateProgression(
override val start: LocalDate,
override val endInclusive: LocalDate,
val step: Long = 1 // 1day
) : Iterable<LocalDate>, ClosedRange<LocalDate> {
override fun iterator(): Iterator<LocalDate> = LocalDateProgressionIterator(start, endInclusive, step)
infix fun step(days: Long) = LocalDateProgression(start, endInclusive, days)
internal class LocalDateProgressionIterator(
var current: LocalDate,
val endInclusive: LocalDate,
val step: Long
) : Iterator<LocalDate> {
override fun hasNext() = current <= endInclusive
override fun next(): LocalDate {
val next = current
current = current.plusDays(step)
return next
}
}
}
operator fun LocalDate.rangeTo(other: LocalDate) = LocalDateProgression(this, other, 1)
fun main() {
val startDate = LocalDate.now()
val midDate = startDate.plusDays(3)
val endDate = startDate.plusDays(5)
val overDate = startDate.plusDays(6)
val dateRange = startDate..endDate
for (date in dateRange) {
println(date)
// 2023-02-20
// 2023-02-21
// 2023-02-22
// 2023-02-23
// 2023-02-24
// 2023-02-25
}
}