ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6장. 코틀린 타입 시스템 - (3) 컬렉션과 배열
    Kotlin 2022. 4. 4. 13:18

    널 가능성과 컬렉션

    컬렉션 안에 널 값을 넣을 수 있는지 여부는 어떤 변수의 값이 널이 될 수 있는지 여부와 마찬가지로 중요하다.

    변수 타입? : 변수에 널을 저장할 수 있다는 뜻 

    타입 인자? :  타입 인자에 널을 저장할 수 있다는 뜻

    //널이 될 수 있는 값으로 이뤄진 컬렉션 만들기
    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? 타입의 값을 저장할 수 있다. 즉, 리스트에 Int나 null을 저장할 수 있다. 

    - 현재 줄을 파싱할 수 있으면 result에 정수를 넣고 그렇지 않으면 null을 넣는다. 

     

    ※ 널이 될 수 있는게 컬렉션의 원소인가 컬렉션 자체인가 조심해야 한다. 

    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")
    }
    
    fun main() {
        val reader = BufferedReader(StringReader("1\nabc\n42"))
        val numbers = readNumbers(reader)
        addValidNumbers(numbers)
    }

     

    filterNotNull()

    널이 될 수 있는 값으로 이뤄진 컬렉션으로 널 값을 걸러내는 경우가 자주 있어서 코틀린 표준 라이브러리는 그런 일을 하는 함수를 제공한다. 

    //filterNotNull를 널이 될 수 있는 값으로 이뤄진 컬렉션에 대해 사용하기
    fun addValidNumbers2(numbers:List<Int?>){
        val validNumbers = numbers.filterNotNull()
        println("Sum of valid numbers : ${validNumbers.sum()}")
        println("Invalid numbers:${numbers.size - validNumbers.size}")
    }
    

    - 걸러내는 연산도 컬렉션의 타입에 영향을 끼친다. filterNotNull이 컬렉션 안에 널이 들어있지 않음을 보장해주므로 validNumbers는 List<Int> 타입이다. 

     

     

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

     

    코틀린 컬렉션과 자바 컬렉션을 나누는 중요한 특성 

    코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다. 

     

    1. kotlin.collections.Collection 인터페이스

    - 원소를 이터레이션 한다. [iterator()] 

    - 컬렉션의 크기를 얻는다. [size]

    - 어떤 컬렉션 안에 들어있는지 검사한다. [contains()]

    - 컬렉션에서 데이터를 읽는 여러 다른 연산을 수행한다. 

    원소를 추가하거나 제거하는 메소드는 없다. 

     

    2. kotlin.collections.MutableCollection 인터페이스

    - 일반 인터페이스인 kotlin.collections.Collection 인터페이스를 확장한다.

    - 원소를 추가 [add()]

    - 원소를 삭제 [remove()]

    - 컬렉션 안의 원소를 모두 지움 [clear()]

     

    ※ 코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼아라. 코드가 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용하라.

    //읽기 전용과 변경 가능한 컬렉션 인터페이스
    fun<T> copyElements(source:Collection<T>, target:MutableCollection<T>) {
        for(item in source){//source 컬렉션의 모든 원소에 대해 루프를 돈다.
            target.add(item) //변경 가능한 target 컬렉션에 원소를 추가한다.
        }
    }
    
    
    fun main() {
        val source: Collection<Int> = arrayListOf(3,5,7)
        val target: MutableCollection<Int> = arrayListOf(1)
        copyElements(source, target)
        println(target) //[1,3,5,7]
    }

     

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

    val source: Collection<Int> = arrayListOf(3,5,7)
    val target: Collection<Int> = arrayListOf(1)
    copyElements(source, target) //Type mismatch: inferred type is Collection<Int> but MutableCollection<TypeVariable(T)> was expected

     

    컬렉션 인터페이스를 사용할 때 항상 염두에 둬야 할 핵심은 읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션일 필요는 없다는 점이다. (ex. 같은 컬렉션 객체를 가리키는 다른 타입의 참조들(읽기 전용과 변경 가능 리스트))

    ※ 읽기 전용 컬렉션이 항상 스레드에서 안전하지 않다는 점을 명심해야 한다. 다중 스레드 환경에서 데이터를 다루는 경우 그 데이터를 적절히 동기화하거나 동시 접근을 허용하는 데이터 구조를 활용해야 한다. 

     

    코틀린 컬렉션과 자바 

    모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스라는 점은 사실이며, 코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없다. 또한 래퍼 클래스를 만들거나 데이터를 복사할 필요도 없다. 코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스라는 두 가지 표현을 제공한다.

    코틀린은 자바 호환성을 제공하는 한편 읽기 전용 인터페이스와 변경 가능 인터페이스를 분리한다. 

     

    컬렉션 생성 함수 

    컬렉션 타입 읽기 전용 타입 변경 가능 타입
    List listOf mutableListOf, arrayListOf
    Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf 
    Map mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf 

    - SetOf()와 mapOf()는 자바 표준 라이브러리에 속한 클래스의 인스턴스를 반환한다. 그들은 내부에서는 변경 가능한 클래스다. 하지만 변경 가능한 클래스라는 사실에 의존하면 안 된다. 나중에 바뀔 수도 있기 때문이다. 

     

    ※ 문제 :  자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로, 코틀린에서는 읽기 전용 Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.

    - 컬렉션을 자바로 넘기는 코틀린 프로그램을 작성한다면 호출하려는 자바 코드가 컬렉션을 변경할지 여부에 따라 올바른 파라미터 타입을 사용할 책임은 개발자에게 있다. 

    - 컬렉션을 자바 코드에게 넘길 때는 특별히 주의를 기울여야 하며, 코틀린 쪽 타입이 적절히 자바 쪽에서 컬렉션에게 가할 수 있는 변경의 내용을 반영(널 가능성이나 불변성 등)하게 해야 한다. 

     

     

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

    자바 코드에서 정의한 타입을 코틀린에서는 플랫폼 타입으로 본다. → 플랫폼 타입의 경우 코틀린 쪽에는 널 관련 정보가 없다. (컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느 쪽으로든 사용할 수 있게 허용한다.) 

    자바 쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다. →  플랫폼 타입인 컬렉션은 기본적으로 변경 가능성에 대해 알 수 없다. (코틀린 코드는 그 타입을 읽기 전용 컬렉션이나 변경 가능한 컬렉션 어느 쪽으로든 다룰 수 있다.

    컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드 하려는 경우 읽기 전용 컬렉션과 변경 가능 컬렉션의 차이가 문제가 된다. → 여러 가지 선택 후 선택한 내용을 코틀린에서 사용할 컬렉션 타입에 반영해야 한다. 

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

     

    import java.io.File;
    import java.util.List;
    
    interface FileContentProcessor {
        void processContents(File path,
                             byte[] binaryContents, 
                             List<String> textContents);
    }
    
    class FileIndexer : FileContentProcessor {
        override fun processContents(path: File?,
                                     binaryContents: ByteArray?,
                                     textContents: MutableList<String>?) {
        }
    }

    [선택사항] 

    • 일부 파일은 이진 파일이며 이진 파일 안의 내용은 텍스트로 표현할 수 없는 경우가 있으므로 리스트는 널이 될 수 있다. 
    • 파일의 각 줄은 널일 수 없으므로 이 리스트의 원소는 널이 될 수 없다. 
    • 이 리스트는 파일의 내용을 표현하며 그 내용을 바꿀 필요가 없으므로 읽기 전용이다. 

     

    interface DataParser<T> {
        void parseData(String input, 
                       List<T> output, 
                       List<String> errors);
    }
    class PersonParser : DataParser<Person> {
        override fun parseData(input: String,
                               output: MutableList<Person>,
                               errors: MutableList<String?>) {
        }
    }

     

    [선택사항]

    • 호출하는 쪽에서 항상 오류 메시지를 받아야 하므로 List<String>은 널이 되면 안된다. 
    • errors의 원소는 널이 될 수도 있다. output에 들어가는 정보를 파싱 하는 과정에서 오류가 발생하지 않으면 그 정보와 연관된 오류 메시지는 널이다. 
    • 구현 코드에서 원소를 추가할 수 있어야 하므로 List<String>은 변경 가능해야 한다. 

     

     

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

    배열보다는 컬렉션을 더 먼저 사용해야 한다. 

    fun main(args:Array<String>) {
        for(i in args.indices){// 배열의 인덱스 값의 범위에 대해 이터레이션하기 위해 array.indices 확장 함수를 사용한다. 
            println("Argument $i is: ${args[i]}") // array[index]로 접근해서 배열 원소에 접근한다. 
        }
    }

     

    코틀린 배열은 타입 파라미터를 받는 클래스다. 배열의 원소 타입은 바로 그 타입 파라미터에 의해 정해진다. 

     

    배열 만드는 방법

    • arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다. 
    • arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다. 물론 원소 타입이 널이 될 수 있는 타입인 경우에만 이 함수를 쓸 수 있다. 
    • Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화해준다. arrayOf를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용한다. 
    val letters = Array<String>(26){ i -> ('a' + i).toString()}
    println(letters.joinToString("")) //abcdefghijklmnopqrstuvwxyz

     

    - 코틀린에서는 배열을 인자로 받는 자바 함수를 호출하거나 vararg 파라미터(가변 인자)를 받는 코틀린 함수를 호출하기 위해 가장 자주 배열을 만든다. 

    - 데이터가 이미 컬렉션에 들어있다면 컬렉션을 배열로 변환해야 한다. (toTypedArray() : 컬렉션 → 배열) 

    val strings = listOf("a", "b", "c")
    println("%s/%s/%s".format(*strings.toTypedArray())) //vararg 인자를 넘기기 위해 스프레드 연산자(*)를 써야함
    //a/b/c

     

    ※ 다른 제네릭 타입에서처럼 배열 타입의 타입 인자도 항상 객체 타입이 된다. 

    - Array<Int> : 박싱 된 정수의 배열 (자바 타입은 java.lang.Integer[]) 

    - 박싱 하지 않은 원시 타입의 배열이 필요하다면 특별한 배열 클래스를 사용해야 한다. 코틀린은 각 원시 타입마다 하나씩 원시 타입의 배열을 표현하는 별도 클래스를 제공한다. [ IntArray, ByteArray, CharArray, BooleanArray 등] 이 모든 타입은 자바 원시 타입 배열인 int[], byte[], char[] 등으로 컴파일된다. 

     

    원시 타입의 배열 만드는 방법

    • 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값(보통은 0)으로 초기화된 size 크기의 배열을 반환한다. 
    • 팩토리 함수(IntArray를 생성하는 intArrayOf 등)는 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다. 
    • (일반 배열과 마찬가지로) 크기와 람다를 인자로 받는 생성자를 사용한다. 
    val fiveZeros = IntArray(5)
    val fiveZerosToo = intArrayOf(0,0,0,0,0)
    val squares = IntArray(5) {i -> (i+1) * (i+1)}
    println(squares.joinToString())//1, 4, 9, 16, 25

     

    코틀린 표준 라이브러리는 배열 기본 연산(배열 길이 구하기, 원소 설정하기, 원소 읽기)에 더해 컬렉션에 사용할 수 있는 모든 확장 함수(filter, map 등)를 배열에도 제공한다. 다만 이런 함수가 반환하는 값은 배열이 아니라 리스트라는 점에 유의하라.  

     

    forEachIndexed

    배열의 모든 원소를 갖고 인자로 받은 람다를 호출해준다. 배열의 원소와 그 원소의 인덱스를 람다에게 인자로 전달한다. 

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