오늘도 더 나은 코드를 작성하였습니까?

코틀린 타입 시스템( 컬렉션과 배열) 본문

Kotlin in Action/코틀린 기초

코틀린 타입 시스템( 컬렉션과 배열)

hik14 2020. 8. 21. 19:11

널 가능성과 컬렉션

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

 

1. List<Int?> = 리스트 자체는 항상 존재하며, 리스트의 각 원소는 널이 될 수 있다.

 

2. List<Int>? = 리스트가 null 일수는 있지만, 리스트가 참조가 존재한다면 그 원소는 전부 널 일수 없다.

 

3. List<Int?>? = 리스트 자체가 널 일수도 있고 리스트가 존재하더라도 그 원소 역시 널 일수 있다.

 

3번과 같은 리스트는 널 값 검사를 List에 대해서 하고 그 원소를 참조할 때 역시 필요하다.

 

fun addValidNumbers(numbers: List<Int?>){

    var sumOfValidNumbers = 0
    var inValidNumbers =0

    for(number in numbers){
        if (number != null)
            sumOfValidNumbers++
        else
            inValidNumbers++
    }

    println("Sum of valid numbers: $sumOfValidNumbers")
    println("Invalid numbers: $inValidNumbers")
}


fun main() {

    val numbers: List<Int?> = listOf(1, 2, 3, 4, 5, null, 6, 7, 8, null)
    addValidNumbers(numbers)
}

리스트 원소에 접근하면 Int? 타입의 값을 얻기 때문에 그 값을 산술식에 사용하기 전에 널 검사를 해야 된다.

컬렉션에서 원소가 널 일수 있다면 걸러내는 작업을 자주 있어서 코틀린 표준 라이브러리는 filterNotNull 함수를 제공한다.

fun addValidNumbers(numbers: List<Int?>){

    var validNumbers = numbers.filterNotNull()  // 타입이 List<Int> 이다.
    var inValidNumbers = numbers.size - validNumbers.size
    
    println("Sum of valid numbers: ${validNumbers.size}")
    println("Invalid numbers: $inValidNumbers")
}

 

 

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

코틀린의 컬렉션과 자바의 컬렉션의 가장 중요한 차이는

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

 

kotlin.collections.Collection

 

- 원소에 대한 이터레이션 iterator

- 컬렉션의 크기. size

- 어떤 값이 들어있는지 검사 contains

- 컬렉션 내의 데이터를 읽는 여러 다른 연산

 

kotlin.collection.MutableCollection (Collection을)

 

- 원소에 대한 추가(add), 수정, 삭제(remove), 전부 삭제(removeAll)

 

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

코드가 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 만들어야 한다. 

 

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

fun main() {

    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> =  arrayListOf(1)

    copyElements(source, target)
    println(target)
}

target에는 읽기 전용 컬렉션을 넘길 수없다. 실제 값이 변경 가능한 컬렉션(arrayListOf(3, 5, 7))인지 여부와 상관이 없이 선언된 타입(Collection <Int>)이 읽기 전용이라면 target에 넘기면 컴파일 에러가 난다.

 

컬렉션 인터페이스 사용할 때  염두해야 될 핵심은 타입이 읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션 인스턴스일 필요는 없다. 

읽기 전용 인터페이스 타입인 변수를 사용할 때 그 인터페이스는 실제로 어떤 컬렉션 인스턴스를 가리키는 수많은 참조 중 하나다.

 

    val list = arrayListOf("hello", "kotlin")
    val readOnlyList: List<String> = list
    val mutableList: MutableList<String> = list

이러한 컬렉션을 참조하는 다른 코드를 호출하거나 병렬 처리를 하다 보면 컬렉션 사용 도중 다른 컬렉션이 원소를 변경하는 경우가 생길 수 있다.  ConcurrentModificationException이 발생할 수 있다.

 

읽기 전용 컬렉션이 항상 스레드 안전하지 않다는 점을 명심한다. (동시 접근이 가능하다.)

다중 스레드 환경에서 데이터를 다룰 때는 동기화 처리와 동시 접근 허용하는 데이터 구조를 활용한다. 

 

코틀린 컬렉션과 자바

모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스가 존재한다.

모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스라는 2가지 표현을 제공한다. 

 

 

컬렉션과 마찬가지로 Map 클래스(Collection, Iterable 상속하지 않음)도 코틀린에서 Map과 MutableMap 2가지 버전이 있다.

컬렉션 타입 읽기 전용 타입 변경 가능 타입
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf,

linkedSetOf,  sortedSetOf
Map mapOf mutableMapOf, hashMapOf,

linkedMapOf, sortedMapOf

setOf(), mapOf는 자바 표준 라이브러리에 속한 클래스의 인스턴스를 반환하는데 내부에서 변경 가능한 클래스다.

변경 가능한 클래스라는 사실에 의존하면 안 되고 미래에는 불변 컬렉션 인스턴스를 리턴할 수 있다.

 

자바 메서드를 호출하되 컬렉션을 인자로 넘겨야 한다면 변환 및 복사는 추가 작업은 필요 없고 Collection, MutableCollection 값을 인자로 둘 다 가능하다.

 

이 때문에 컬렉션의 변경 가능성과 관련해 중요한 문제가 생긴다. 

자바는 코틀린에서 읽기 전용으로 선언된 객체라도 자바 코드에서 그 컬렉션의 원소를 변경할 수 있다.

컴파일러는 자바 코드가 컬렉션은 변경하는지 분석할 수 없고, Collection을 넘겨도 막을 수가 없다.

 

import java.util.List;

public class CollectionUtils {

    public static List<String> upperCaseAll(List<String> item){

        for(int i=0; i<item.size(); i++){
           item.set(i, item.get(i).toUpperCase());
        }
        item.set(0,"D");
        return item;
    }
}
fun printInUpperCase(list: List<String>){ // 읽기 전용
    println(CollectionUtils.upperCaseAll(list))
}

fun main() {
    val list = listOf("a", "b", "c")
    printInUpperCase(list)
}

자바 코드가 컬렉션을 변경할지의 여부에 따라 올바른 파라미터 타입을 사용할 책임은 개발자에게 있다.

(변경 불가능한 컬렉션을 자바에서 변경한다고 판단되면 아예 변경 가능한 컬렉션으로 사용해라)

 

널 역시 마찬가지로 널이 될 수 없는 원소를 가진 컬렉션을 자바로 넘겼는데 널이 포함되어 돌아올 수 있다.

컬렉션을 자바와 함께 사용할 때는 특히 주의해야 되며 코틀린 쪽에서 타입을 적절히 자바 쪽에서 컬렉션에게 가할 수 있는 변경의 내용을 반영해서 변경한다.

 

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

플랫폼 타입의 경우 널이 가능한 타입 널이 될 수 없는 타입 어느 쪽이든 허용한다.

컬렉션의 경우도 그 타입이 읽기 전용 인지 변경 가능한 컬렉션인지 어느 쪽으로도 다룰 수 있다.

 

컬렉션의 타입이 자바 메서드 구현을 오버 라이딩할 때 시그니처에 있다면 문제가 된다.

 

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

 

예시) 

일부 파일은 이진 파일이며 이진 파일 내용은 텍스트로 표현할 수 없는 경우가 있기에 리스트는 널이 될 수 있다. List< >?

파일의 각 줄은 null이 아니기 때문에 리스트의 원소는 널 일수 없다.  List<String>?

리스트는 파일의 내용을 표현할 뿐 변경하지 않는다

 

 textContents: List<String>?

import java.io.File;
import java.util.List;

public interface FileContentProcessor {

    void processContents(File path, byte[] binaryContents, List<String> textContents);
}
class FileIndexer: FileContentProcessor{
    
    override fun processContents(path: File, binaryContents: ByteArray?, textContents: List<String>?) {
    
    }

}

 

호출하는 쪽은 항상 오류 메시지를 받아야 된다 List <String> errors는 널이 돼서는 안 되지만 

오류가 일어나지 않는다면 원소는 널 일수 있다.

오류를 추가해야 되기 때문에 변경 가능해야 된다.

 

errors: MutableList<String?>

public 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?>) {



    }
}

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

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

 

배열 생성 방법

 

1. arrayOf 원소를 넣어 넘기면 배열을 만들 수 있다.

2. arrayOfNulls에 정수 값을 넣어 넘기면 모든 원소가 널인 넘겨받은 정수 크기의 배열은 만든다.(타입은 널이 가능)

3. Array 생성자는 배열의 크기, 람다를 인자로 받아서 람다를 호출해서 각 배열의 원소를 초기화한다.

 

 

예제

fun main() {
    val letters = Array<String>(26){ ('a' + it).toString()}
    println(letters.joinToString(" "))
}

 

배열을 인자로 받는 자바 함수를 호출하거나, vararg 파라미터를 받는 코틀린 함수를 호출하기 위해 자주 배열을 사용한다.

데이터가 이미 컬렉션에 들어있는 상태라면 이 컬렉션을 배열로 변환할 필요가 있다.

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

 

fun main() {
    val strings = listOf("a", "b", "c")
    println("%s %s %s".format( *strings.toTypedArray() )) // vararg인자를 넘기기 위해 스프레드 연산자(*)를 사용한다.
}

 

제네릭 타입에서 처럼 배열 타입의 타입 인자 <T>도 항상 객체 타입이 된다. 따라서 Array <Int>와 같이 선언하면

그 배열은 박싱 된 정수의 배열 Integer [ ]가 된다. 

 

박싱 되지 않은 원시 타입의 배열이 필요하다면, 코틀린은 원시 타입의 배열을 표현하는 별도의 클래스를 각 원시 타입마다 제공한다. 

 

원시타입 코틀린 자바
Int  IntArray  int[ ]
Byte  ByteArray  byte[ ]
Char  CharArray  char[ ]
Boolean  BooleanArray  boolean[ ]

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

 

size를 인자로 받아서 해당 원시타입의 디폴트 값으로 초기화시킨 size크기의 배열을 생성한다.

 

팩토리 함수는 여러 값을 가변 인자로 받아 그런 값이 들어간 배열을 반환한다.

 

크기와 람다를 인자로 받는 생성자를 사용한다. 

 

fun main() {

    val fiveZeros = IntArray(5)

    val fiveZerosTwo = intArrayOf(0,0,0,0,0)

    val squares = IntArray(5){(it+1)*(it+1)}

    println(fiveZeros.joinToString(" "))
    println(fiveZerosTwo.joinToString(" "))
    println(squares.joinToString(" "))
}

 

박싱 된 값이 들어있는 컬렉션이나 배열이 있다면 toIntArray 등의 변환 함수를 사용해 박싱 하지 않은 값이 들어있는 배열로 변환할 수 있다.

 

배열로 할 수 있는 기본 연산

 

길이, 원소 설정, 원소 읽기

filter, map도 배열에서 잘 작동한다 

fun printArray(arr: Array<String>){
    arr.forEachIndexed{ index, element ->
        println("$element index in: $index")
    }
}

fun main() {
    val stringArr = arrayOf("a","b","c","d","e")
    printArray(stringArr)
}