Kotlin Collection : List, Set, Map

홍성덕·2024년 9월 9일

Kotlin의 Collection

Kotlin 뿐만 아니라 여러 프로그래밍 언어에서 Collection(컬렉션)이라는 개념이 존재한다. 컬렉션이란 여러 개의 아이템을 그룹화한 데이터 타입을 말한다.

Kotlin의 컬렉션 타입은 두 가지 타입으로 나뉜다.
1. Read-only Collection : 읽기 전용 컬렉션으로 요소에 접근만 가능하다.
2. Mutable Collection : 요소 추가, 제거, 업데이트 등의 write 작업을 수행할 수 있는 변경 가능한 컬렉션

Collection 특징

1. mutable 컬렉션은 val로 선언해도 변경 가능

mutable 컬렉션을 반드시 var로 선언할 필요는 없다. val로 선언하더라도 write 작업은 여전히 가능하다. val로 선언하여 컬렉션에 대한 참조가 수정되는 것을 방지할 수 있으므로, var보다는 val로 선언하도록 하자.

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four")
    numbers.add("five")
    println(numbers) // 출력 : [one, two, three, four, five]
	// numbers = mutableListOf("six", "seven") // 컴파일 오류 발생
}

위와 같이 numbers mutable 리스트에 추가는 가능하지만 새로운 mutable 리스트를 할당하는 것은 불가능하다.

2. Read-only 컬렉션은 공변성을 가지지만 Mutable 컬렉션은 공변성을 가지지 않는다

open class Shape
class Rectangle: Shape()
class Circle: Shape()

fun main() {
    val rectangles: List<Rectangle> = listOf(Rectangle(), Rectangle())
    val shapes: List<Shape> = rectangles
}

위와 같이 Rectangle 클래스가 Shape 클래스를 상속받는다면, List<Rectangle>을 List<Shape>이 필요한 곳에서 사용할 수 있다. 즉 컬렉션 타입은 요소 타입과 동일한 상속 관계를 가진다. 단, Map의 경우 Value의 타입에 대해서는 공변성이 적용되지만 Key의 타입에 대해서는 적용되지 않는다.

open class Shape
class Rectangle : Shape()
class Circle : Shape()

fun main() {
    val rectangles: MutableList<Rectangle> = mutableListOf(Rectangle(), Rectangle())
    // val shapes: MutableList<Shape> = rectangles // 컴파일 오류
    // shapes.add(Circle()) // 만약 허용된다면 Rectangle 타입 파라미터 위반
}

반면, Mutable 컬렉션은 공변성을 가지지 않는다. 위와 같이 MutableList<Rectangle>를 MutableList<Shape>으로 업캐스팅을 시도하면 Type mismatch로 컴파일 오류가 발생한다. 만약 공변성을 가지게 된다면 Rectangle 타입으로 제한된 MutableList에 Circle 객체를 추가하는 것이 가능해진다. 그래서 공변성을 가지지 않는 것이다.

public interface Set<out E> : Collection<E> { ... }

public interface MutableSet<E> : Set<E>, MutableCollection<E> { ... }

컬렉션 중 하나인 Set이 선언된 곳을 보면 읽기 전용 Set은 공변성 키워드인 out을 사용했지만 MutableSet은 out을 사용하지 않은 것을 볼 수 있다.


Kotlin의 대표적인 컬렉션 타입에는 List, Set, Map이 존재한다. 이 글에서는 읽기 전용 List, Set, Map만 다룬다.

1. List

public interface List<out E> : Collection<E> { ... }

List는 인덱스를 통해 요소에 접근할 수 있는 순서가 있는 컬렉션이다. 인덱스는 고유하지만 요소들은 중복된 값이 있을 수 있다.

fun main() {
    val telephoneNumber = listOf(0, 2, 1, 2, 3, 4, 4, 3, 2, 1)
    println(telephoneNumber.joinToString("")) // 출력 : 0212344321
}

예를 들면 전화번호는 숫자들의 그룹이며 그 순서가 중요하고 숫자는 중복될 수 있다.

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

fun main() {
    val bob = Person("Bob", 31)
    val people = listOf(Person("Adam", 20), bob, bob)
    val people2 = listOf(Person("Adam", 20), Person("Bob", 31), bob)
    println(people == people2) // 출력 : true
}

그리고 중복된 값이 있을 수도 있다는 말에 대해 추가적인 예시로, 위와 같이 bob 인스턴스를 중복으로 리스트의 요소로 사용할 수 있다. 그리고 두 개 리스트의 크기가 같고 순서도 같으며 모든 요소가 동등성을 만족한다면 두 개 리스트의 동등성도 만족한다. 예시에서도 people[1]people2[1]은 다른 객체이지만 동등하기 때문에 두 개의 리스트는 동등하게 취급된다.

2. Set

public interface Set<out E> : Collection<E> { ... }

Set은 순서가 정의되지 않은 고유한 요소들로 구성된 컬렉션이다. 고유한 요소들로 구성되어 있다는 뜻은 중복되는 요소가 없는 컬렉션이라는 의미이다. Set은 null을 요소로도 가질 수 있는데 null도 고유한 요소여야 하므로, Set은 오직 하나의 null만 가질 수 있다.

fun main() {
    val lottoNumber = setOf(21, 42, 44, 1, 38, 12, 33)
    println(lottoNumber) // 출력 : [21, 42, 44, 1, 38, 12, 33]
}

예를 들면 로또 번호는 중복되지 않는 고유한 숫자들로 구성되어 있지만 순서는 중요하지 않다.

fun main() {
    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}")
}

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

그리고 두 개 Set의 크기가 같고 같은 요소들을 가지고 있으면 두 Set은 동등한 것으로 간주된다. 위의 예시에서 setOf(1, 2, 3, 4), setOf(4, 3, 2, 1)으로 작성한 숫자의 순서는 다르지만, Set은 순서가 정의되지 않는 컬렉션이기 때문에 두 Set은 동등하다.

3. Map

public interface Map<K, out V> { ... }

Map은 List, Set과 다르게 Collection interface를 상속받지 않는다. 하지만 공식 문서에서는 Kotlin의 컬렉션 중 하나로 분류하고 있다.

Map은 키-값 쌍으로 이루어진 컬렉션으로 키는 고유하며 각각 정확히 하나의 값과 매핑된다. 키는 중복을 허용하지 않지만 값은 중복을 허용한다.

fun main() {
    val idNameMap = mapOf(1 to "John", 12 to "Michael", 31 to "Alice")
    println(idNameMap) // 출력 : {1=John, 12=Michael, 31=Alice}
}

예를 들면 직원의 ID와 그들의 이름을 Map으로 표현할 수 있다. 직원의 ID는 고유하지만 이름은 동명이인이 존재할 수 있다.

fun main() {
    val pair = "key1" to 1
    val numbersMap = mapOf(pair, "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}")
}

두 개 Map이 같은 크기에 같은 요소를 가지고 있으면 동등한 것으로 간주한다. 순서는 상관없다.


참고자료

profile
안드로이드 주니어 개발자

0개의 댓글