널 가능성과 컬렉션

타입 인자 안에서 ?가 하는 일을 이해하기 위해 파일의 각 줄을 읽어서 숫자로 변환하기 위해 파싱하는 다음 예제를 보자. List<Int>는 Int? 타입의 값을 저장할 수 있다.

fun readNumbers(reader: BufferedReader): List<Int?> {
    val result = ArrayList<Int?>()
    for (line in reader.lineSequence()) {
        try {
            val number = line.toInt() // toIntOrNull도 가능
            result.add(number)
        }
        catch (e: NumberFormatException) {
            result.add(null)
        }
    }
    return result
}

어떤 변수 타입의 널 가능성과 타입 파라미터로 쓰이는 타입의 널 가능성 사이의 차이는 다음과 같다.

20240320_104041.jpg

경우에 따라 널이 될 수 있는 값으로 이뤄진 널이 될 수 있는 리스트를 정의해야 할 수도 있다. 코틀린에서는 List<Int?>?로 이를 표현한다.

읽기 전용과 변경 가능한 컬렉션

코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 하나는 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다. 이런 구분은 코틀린 컬렉션을 다룰 때 사용하는 가장 기초적인 인터페이스인 kotlin.collections.Collection부터 시작한다. Collection 인터페이스를 사용하면 다음을 할 수 있다.

하지만 Collection에는 원소를 추가하거나 제거하는 메소드가 없다.

컬렉션의 데이터를 수정하려면 kotlin.collections.MutableCollection 인터페이스를 사용해야 한다. MutableCollection은 Collection을 확장하면서 원소를 추가하거나, 삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 더 제공한다.

코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하고, 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용하자.

어떤 컴포넌트의 내부 상태에 컬렉션이 포함된다면 그 컬렉션을 MutableCollection을 인자로 받는 함수에 전달할 때는 원본의 변경을 막기 위해 컬렉션을 복사해서 전달하는 것이 좋다. 이러 패턴을 방어적 복사(defensive copy)라고 부른다.

fun main() {
    val source: Collection<Int> = arrayListOf(2,3,4)
    val target : MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    println(target) // 1, 2, 3, 4
}

fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
    for (item in source) {
        target.add(item)
    }
}

target에 해당하는 인자로 읽기 전용 컬렉션을 넘길 수 없다. 실제 그 값(컬렉션)이 변경 가능한 컬렉션인지 여부와 관계없이 선언된 타입이 읽기 전용이라면 target에 넘기면 컴파일 오류가 난다.

읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션일 필요는 없다. 읽기 전용 인터페이스 타입인변수를 사용할 때 그 인터페이스는 실제로는 어떤 컬렉션 인스턴스를 가리키는 수많은 참조 중 하나일 수 있다.