본문 바로가기

Dev Book/Kotlin IN ACTION

CH6(6.3). 컬렉션과 배열

1. 널 가능성과 컬렉션

- 컬렉션 안에 널 값을 넣을 수 있는지 여부는 어떤 변수의 값이 널이 될 수 있는지 여부와 마찬가지로 중요하다. 변수 타입 뒤에 ?를 붙이면 그 변수에 널을 저장할 수 있다는 뜻이다. 타입 인자에도 같은 표시를 사용할 수 있다.

 

fun readNumbers(reader: BufferedReader): List<Int?>{
    val result = ArrayList<Int?>() //널이 될 수 있는 Int 값으로 이뤄진 리스트
    for(line in reader.lineSequence()){
        try{
            val number = line.toInt()
            result.add(number)
        }
        catch(e:NumberFormatException){
            result.add(null) //현재 줄을 파싱할 수 없으므로 리스트에 널을 추가.
        }
    }
    return result
}

 

List<Int?>는 Int? 타입의 값을 저장할 수 있다. 

 

- List<Int?>는 리스트 안의 각 값이 널이 될 수 있지만 리스트 자체는 항상 널이 아니다. List<Int>?는 전체 리스트가 널이 될 수 있지만 리스트 안에는 널이 아닌 값만 들어간다.

- 널이 될 수 있는 값으로 이뤄진 널이 될 수 있는 리스트는 List<Int?>?로 표현한다. 이 경우 변수에 대해 널 검사를 수행한 다음 리스트에 속한 모든 원소에 대해 다시 널 검사를 수행해야 한다.

 

fun addValidNumbers(numbers:List<Int?>){
    var sumOfValidNumbers = 0
    var invalidNumbers = 0
    for (number in numbers){
        if(number!=null){
            sumOfValidNumbers += number
        }
        else{
            invalidNumbers++
        }
    }
    println("Sum of valid numbers: $sumOfValidNumbers")
    println("Invalid numbers: $invalidNumbers")
}

 

- 널 값을 걸러내는 filterNotNull 함수를 이용해 위의 예제를 단순화할 수 있다.

 

fun addValidNumbers2(numbers:List<Int?>){
    val validNumbers = numbers.filterNotNull() //validNumbers는 컬렉션 내 널이 없음을 보장, List<Int> 타입.
    println("Sum of valid numbers: ${validNumbers.sum()}")
    println("Invalid numbers: ${numbers.size - validNumbers.size}")
}

 

filterNotNull이 컬렉션 안에 널이 들어있지 않음을 보장해주므로 validNumbers는 List<Int> 타입이다.

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

- 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다. 컬렉션의 데이터를 수정하려면 kotlin.collections.MutableCollection 인터페이스를 사용해야 한다. Mutable collection은 일반 인터페이스인 kotlin.collections.Collection을 확장하면서 원소를 추가하거나, 삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 더 제공한다. 

- 코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼아라.

 

fun <T> copyElements(source: Collection<T>, target:MutableCollection<T>){
    for(item in source){
        target.add(item) //MutableCollection에 원소 추가.
    }
}

 

copyElements 함수가 source 컬렉션은 변경하지 않지만 target 컬렉션을 변경하리라는 사실을 분명히 알 수 있다.

 

- 읽기 전용 컬렉션이라고 꼭 변경 불가능한 컬렉션일 필요는 없다. 읽기 전용 인터페이스 타입인 변수를 사용할 때 그 인터페이스는 실제로 어떤 컬렉션 인스턴스를 가리키는 수많은 참조 중 하나일 수 있다. 이 경우 다른 컬렉션이 그 컬렉션의 내용을 변경하는 상황이 있을 수 있으므로 읽기 전용 컬렉션이 항상 스레드 안전하지는 않다는 점을 명심해야 한다.

3. 코틀린 컬렉션과 자바

- 모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스이다. 따라서 코틀린-자바 사이에 아무 변환도 필요없다. 래퍼클래스를 만들거나 데이터를 복사할 필요도 없다.

- 자바는읽기 전용 컬렉션과 변경 가능한 컬렉션을 구분하지 않으므로, 코틀린에서 읽기 전용 Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다. 따라서 호출하려는 자바 코드가 컬렉션을 변경할 지 여부에 따라 올바른 파라미터 타입을 사용할 책임은 개발자에게 있다.

4. 컬렉션을 플랫폼 타입으로 다루기

- 플랫폼 타입의 경우 코틀린 쪽에는 널 관련 정보가 없다. 따라서 컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느 쪽으로든 사용할 수 있게 허용한다. 마찬가지로 자바 쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서 플랫폼 타입으로 본다. 플랫폼 타입의 컬렉션은 기본적으로 변경 가능성에 대해 알 수 없다.

- 컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드하려는 경우 문제가 될 수 있다. 이 경우 어떤 코틀린 컬렉션 타입으로 표현할 지 결정해야 한다.

  • 컬렉션이 널이 될 수 있는가?
  • 컬렉션의 원소가 널이 될 수 있는가?
  • 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?

5. 객체의 배열과 원시 타입의 배열

- 코틀린에서 배열을 만드는 방법은 다양하다.

  • arrayOf 함수에 원소를 넘긴다.
  • arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값이 크기인 배열을 만들 수 있다.
  • Array 생성자는 배열 크기와 람다를 인자로 받아서 널이 아닌 배열을 만든다.
val letters = Array<String>(26){ i -> ('a'+i).toString() }

>>>println(letters.joinToString(""))
abcdefghijklmnopqrstuvwxyz

 

인덱스 값에 a 문자를 더한 결과를 문자열로 반환한다. 

 

val strings = listOf("a","b","c")

>>>println("%s/%s/%s".format(*strings.toTypedArray())) //vararg 인자를 넘기기 위해 스프레드 연산자 * 사용
a/b/c

 

toTypedArray 메소드를 사용하면 쉽게 컬렉션을 배열로 바꿀 수 있다.

 

- 다른 제네릭 타입에서처럼 배열 타입의 타입 인자로 항상 객체 타입이 된다. Array<Int> 같은 타입을 선언하면 그 배열은 박싱된 정수의 배열(java.lang.Integer[])이다. 박싱되지 않은 원시 타입의 배열이 필요하다면 그런 타입을 위한 특별한 배열 클래스를 사용해야 한다. 코틀린은 원시 타입의 배열을 표현하는 별도 클래스를 각 원시 타입마다 하나씩 제공한다. IntArray, ByteArray, CharArray, BooleanArray 등의 원시 타입 배열을 제공한다. 이 모든 타입은 자바 원시 타입 배열인 int[], byte[], char[] 등으로 컴파일 된다. 원시 타입의 배열을 만드는 방법은 다음과 같다.

  • size 인자를 받아서 디폴트 값(보통 0)으로 초기화 된 size 크기의 배열을 반환한다. 
  • 팩토리 함수는 여러 값을 가변 인자로 받아서 그 값이 들어간 배열을 반환한다.
  • 크기와 람다를 인자로 받는 생성자를 사용한다.
val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
val squares = IntArray(5){ i -> (i+1)*(i+1)}

 

- 코틀린 표준 라이브러리는 배열 기본 연산에 더해 컬렉션에 사용할 수 있는 모든 확장 함수를 배열에도 제공한다.(다만 배열이 아닌 리스트를 반환한다.)

 

fun forEachIndexed(args:Array<String>){
    args.forEachIndexed{index, element -> println("Argument $index is: $element")}
}

 

'Dev Book > Kotlin IN ACTION' 카테고리의 다른 글

CH7(7.2). 비교 연산자 오버로딩  (0) 2022.02.15
CH7(7.1). 산술 연산자 오버로딩  (0) 2022.02.13
CH6(6.2). 코틀린의 원시 타입  (0) 2022.01.21
CH6(6.1-2). 널 가능성  (0) 2022.01.20
CH6(6.1-1). 널 가능성  (0) 2022.01.16