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 를 생성하는 함수가 정의되어 있다.

  • booleanArrayOf
  • byteArrayOf
  • shortArrayOf
  • charArrayOf
  • intArrayOf
  • longArrayOf
  • floatArrayOf
  • doubleArrayOf

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, MutableListkotlin표준 콜렉션으로 모두 인터페이스이다.

Listimmutable 콜렉션, set add remove 같은 메서드가 정의되어 있지 않다.
MutableListmutable 표준 콜렉션

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

ArrayListMutableList 인터페이스를 구현한 클래스.
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]

Tuple

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

Mapimmutable 콜렉션.
MutableMapmutable 표준 콜렉션.

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]]

사실 chuckedwindowed 함수를 추상화 한 것

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)

사실 filterIsInstancefilterIsInstanceTo 함수를 구현한 함수이며
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

sequenceyield 함수를 사용해 시퀀스를 생성가능하다.

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를 발생시키며 값을 반환하는 함수

해당 yieldCoroutineyield 가 아닌 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 를 구현하고 있다.

ClosedRangecontains 메서드를 보면 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
    }
}

카테고리:

업데이트: