조용한 담장

코틀린(Kotlin) Collections : Constructing Collections 본문

kotlin

코틀린(Kotlin) Collections : Constructing Collections

iosroid 2020. 1. 2. 18:03

코틀린(kotlin) 의 collection (set, list, map) 을 살펴보자.

원문 https://kotlinlang.org/docs/reference/collections-overview.html, https://kotlinlang.org/docs/reference/constructing-collections.html 을 보며 정리.

Collection Types

코틀린은 standard library 를 통해 collection type 인 set, list, map 세개를 제공한다.

크게 두개의 인터페이스가 있다.

  • 읽기 전용(read-only) 인터페이스 : collection element 에 접근하기 위한 기능을 제공
  • 변경 가능(mutable) 인터페이스 : collection element 를 더하거나, 지우거나, 변경하는 등의 기능을 제공

mutable collection 을 사용하기 위해서 var 를 사용해야만 하는것은 아니다. 변수가 참조하는 collection 이 쓰기 동작에 의해 변경되는 것이기 때문에 참조자인 변수가 변경되는 것이 아니기 때문이다.

val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five")   // this is OK    
//numbers = mutableListOf("six", "seven")      // compilation error

covariant read-only collection types

read-only collection type 들은 covariant 하다. 즉, Rectangle class 가 Shape 의 상속자 관계라면, List<Rectangle>List<Shape> 의 관계도 동일하게 된다.

정리하자면, collection type 들은 element 의 type 에 따라서 subtype 관계(subtyping relationship) 를 가지게 된다.

Map 은 key type 이 아닌 value type 에 따라 covariant 하게 된다.

반대로, mutable collection 은 covariant 하지 않는데, 이 특성은 가끔 런타임 오류를 발생시키는 요인이 된다.

mutable collection 이 covariant 하다고 가정하고, MutableList<Rectangle>MutableList<Shape> 의 subtype 이라고 한다면, Shape 의 다른 상속자인 Circle 같은 클래스를 Shape 을 대신하여 사용할 수 있겠지만, 이는 결국 Rectangle type 이 아닌 문제를 가지게 된다.

 

collection interface 의 다이어그램

https://kotlinlang.org/assets/images/reference/collections-overview/collections-diagram.png

Collections

Collection<T> 는 collection 의 관계도 최상위에 위치한다. read-only collection 의 공통적인 동작인(retrieving size, checking item membership, ...) 가진 인터페이스 이다.

CollectionIterable<T> 인터페이스를 상속하여 iterating element 동작을 수행할 수 있다.

아래의 예제처럼 Collection 을 그 상속자들 (List, Set) 타입을 대신하여 사용할 수도 있다.

fun printAll(strings: Collection<String>) {
        for(s in strings) print("$s ")
        println()
    }
    
fun main() {
    val stringList = listOf("one", "two", "one")
    printAll(stringList)
    
    val stringSet = setOf("one", "two", "three")
    printAll(stringSet)
}

// output:
// one two one 
// one two three 

MutableCollection 은 addremove 같은 쓰기 동작을 가진 Collection 이다.

fun List<String>.getShortWordsTo(shortWords: MutableList<String>, maxLength: Int) {
    this.filterTo(shortWords) { it.length <= maxLength }
    // throwing away the articles
    val articles = setOf("a", "A", "an", "An", "the", "The")
    shortWords -= articles
}

fun main() {
    val words = "A long time ago in a galaxy far far away".split(" ")
    val shortWords = mutableListOf<String>()
    words.getShortWordsTo(shortWords, 3)
    println(shortWords)
}

// output:
// [ago, in, far, far]

List

List<T> : 지정된 순서대로 element 저장, 0 부터 시작하는 인덱스로 element 접근.

val numbers = listOf("one", "two", "three", "four")
println("Number of elements: ${numbers.size}")
println("Third element: ${numbers.get(2)}")
println("Fourth element: ${numbers[3]}")
println("Index of element \"two\" ${numbers.indexOf("two")}")

// output:
// Number of elements: 4
// Third element: three
// Fourth element: four
// Index of element "two" 1

길이가 같고 구조적으로 동일(structural equality) 하면 동등한 list 로 판단한다.

data class Person(var name: String, var age: Int)

fun main() {
    val bob = Person("Bob", 31)
    val people = listOf<Person>(Person("Adam", 20), bob, bob)
    val people2 = listOf<Person>(Person("Adam", 20), Person("Bob", 31), bob)
    println(people == people2)
    bob.age = 32
    println(people == people2)
}

// output:
// true
// false

MutableList

val numbers = mutableListOf(1, 2, 3, 4)
numbers.add(5)
numbers.removeAt(1)
numbers[0] = 0
numbers.shuffle()
println(numbers)

// output:
// [0, 3, 4, 5]

List 의 기본 구현은 ArrayList 이다.

Set

Set<T> : 순서 없는 element 저장, 모든 element 는 유일함. null 도 유일하므로 한개만 가질 수 있다.

길이가 같고 각 element 가 같은 두개의 set 는 동등한 것으로 판단한다.

val numbers = setOf(1, 2, 3, 4)
println("Number of elements: ${numbers.size}")
if (numbers.contains(1)) println("1 is in the set")

val numbersBackwards = setOf(4, 3, 2, 1)
println("The sets are equal: ${numbers == numbersBackwards}")

// output:
// Number of elements: 4
// 1 is in the set
// The sets are equal: true

MutableSetMutableCollection 로 부터 상속되어 쓰기 기능을 가진 Set 이다.

SetLinkedHashSet 으로 기본 구현되기 때문에 first()list() 같이 순서대로 접근이 가능하다.

val numbers = setOf(1, 2, 3, 4)  // LinkedHashSet is the default implementation
val numbersBackwards = setOf(4, 3, 2, 1)

println(numbers.first() == numbersBackwards.first())
println(numbers.first() == numbersBackwards.last())

// output:
// false
// true

HashSet 으로 구현하면 순서는 의미없다. 메모리를 적게 먹는 장점이 있다.

Map

Map<K, V> : Collection 인터페이스의 상속자는 아니지만 kotlin collection type 으로 여겨진다. key-value pairs 저장. key 는 유일, 같은 value 를 가진 key 존재 가능.

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

println("All keys: ${numbersMap.keys}")
println("All values: ${numbersMap.values}")
if ("key2" in numbersMap) println("Value by key \"key2\": ${numbersMap["key2"]}")    
if (1 in numbersMap.values) println("The value 1 is in the map")
if (numbersMap.containsValue(1)) println("The value 1 is in the map") // same as previous

// output:
// All keys: [key1, key2, key3, key4]
// All values: [1, 2, 3, 1]
// Value by key "key2": 2
// The value 1 is in the map
// The value 1 is in the map

순서 상관 없이 같은 key-value 값들을 가지면 동등한 map 으로 판단한다.

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)    
val anotherMap = mapOf("key2" to 2, "key1" to 1, "key4" to 1, "key3" to 3)

println("The maps are equal: ${numbersMap == anotherMap}")

// output:
// The maps are equal: true

MutableMap 은 쓰기 기능을 가진 Map 이다.

val numbersMap = mutableMapOf("one" to 1, "two" to 2)
numbersMap.put("three", 3)
numbersMap["one"] = 11

println(numbersMap)

// output:
// {one=11, two=2, three=3}

map 은 LinkedHashMap 으로 기본 구현되기 때문에 element 삽입 시 순서가 지정된다.

HashMap 으로 구현하면 순서는 의미없다.

Constructing from elements

일반적인 collection 생성은 아래의 함수들을 사용한다.

listOf<T>(), setOf<T>(), mutableListOf<T>(), mutableSetOf<T>()

colleciton element 는 argument 에 , 로 구분하여 넣는다.

val numbersSet = setOf("one", "two", "three", "four")
val emptySet = mutableSetOf<String>()

map 은 mapOf() 와 mutableMapOf() 를 사용한다.

argument 는 Pair object 로 전달되며 주로 to 를 쓰는 infix function 을 사용한다.

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 1)

to 는 Pair object 를 만드는 일을 하기 때문에 성능이 중요한 경우에는 좋지 않을 수 있다.

대안으로, 아래의 예제처럼 collection 을 생성하고 apply() 같은 쓰기 기능의 함수을 사용하는 방법이 있다.

val numbersMap = mutableMapOf<String, String>().apply { this["one"] = "1"; this["two"] = "2" }

Empty collections

element 가 없는 collection 을 만드는 함수가 있다. element 의 type 을 명시해야 한다.

emptyList()emptySet()emptyMap()

val empty = emptyList<String>()

Initializer functions for lists

list 의 생성자 중에 list 의 크기와 인덱스 기반으로 값을 정의하는 초기화 함수(initializer funciton) 을 가지는 것이 있다.

val doubled = List(3, { it * 2 })  // or MutableList if you want to change its content later
println(doubled)

// output:
// [0, 2, 4]

Concrete type constructors

ArrayListLinkedList 같은 concrete type collection 은 따로 정의된 생성자(constructor)들을 사용하여 만들 수 있다.

Set 과 Map 의 구현에 사용할 수 있는 비슷한 생성자들이 있다.

val linkedList = LinkedList<String>(listOf("one", "two", "three"))
val presizedSet = HashSet<Int>(32)

Copying

collection 의 복사는 toList()toMutableList()toSet() 같은 함수를 사용한다.

val sourceList = mutableListOf(1, 2, 3)
val copyList = sourceList.toMutableList()
val readOnlyCopyList = sourceList.toList()
sourceList.add(4)
println("Source size: ${sourceList.size}")
println("Copy size: ${copyList.size}")   

//readOnlyCopyList.add(4)             // compilation error
println("Read-only copy size: ${readOnlyCopyList.size}")

// output:
// Source size: 4
// Copy size: 3
// Read-only copy size: 3

이런 함수들은 collection 간의 type 변환에도 사용될 수 있다.

val sourceList = mutableListOf(1, 2, 3)    
val copySet = sourceList.toMutableSet()
copySet.add(3)
copySet.add(4)    
println(copySet)

// output:
// [1, 2, 3, 4]

collection 의 element 복사 대신에 인스턴스의 새로운 레퍼런스를 생성하는 방법이 있다.

새로운 레퍼런스는 이미 존재하는 collection 변수로 초기화 할 때 생성되고, 그 collection 인스턴스가 레퍼런스를 통해 변경되면 연관된 모든 레퍼런스에 반영된다.

val sourceList = mutableListOf(1, 2, 3)
val referenceList = sourceList
referenceList.add(4)
println("Source size: ${sourceList.size}")

// output:
// Source size: 4

collection 초기화는 가변성을 제한(restricting mutability) 하기 위한 용도로 사용될 수 있다.

예를들어, MutableList 를 참조하는 List 를 생성하면 컴파일러는 이 레퍼런스를 통해 collection 을 수정하는 행위에 에러를 발생시킨다.

val sourceList = mutableListOf(1, 2, 3)
val referenceList: List<Int> = sourceList
//referenceList.add(4)            //compilation error
sourceList.add(4)
println(referenceList) // shows the current state of sourceList

// output:
// [1, 2, 3, 4]

Invoking functions on other collections

다른 collection 의 다양한 동작의 결과로써 collection 이 생성될 수 있다.

filtering

val numbers = listOf("one", "two", "three", "four")  
val longerThan3 = numbers.filter { it.length > 3 }
println(longerThan3)

// output:
// [three, four]

Mapping

val numbers = setOf(1, 2, 3)
println(numbers.map { it * 3 })
println(numbers.mapIndexed { idx, value -> value * idx })

// output:
// [3, 6, 9]
// [0, 2, 6]

Association

val numbers = listOf("one", "two", "three", "four")
println(numbers.associateWith { it.length })

// output:
// {one=3, two=3, three=5, four=4}

 

Comments