본문 바로가기

코틀린

코틀린(Kotlin) - 컬렉션과 배열

코틀린 컬렉션은 자바 라이브러리를 바탕으로 만들어졌고 코틀린의 확장 함수를 통해서 다양한 기능이 제공되며, 추가할 수 있다는 사실을 알고 있습니다.

 

이 외에도 코틀린의 컬렉션 지원과 자바와 코틀린 컬렉션 간의 관계에 대해 더 상세히 살펴보도록 하겠습니다.

null 가능성과 컬렉션

컬렉션 안에 null 값을 넣을 수 있는지 여부는 어떤 변수의 null 여부를 정하는 것만큼이나 마찬가지로 중요합니다.

컬렉션 타입 인자에도 ? 를 붙이면 null이 될 수 있는 값으로 이뤄진 컬렉션을 만들 수 있습니다.

 

/* null이 될 수 있는 값으로 이뤄진 컬렉션 만들기 */
fun readNumbers(reader: BufferedReader) : List<Int?> {
   val result = ArrayList<Int?>()  //null이 될 수 있는 int값으로 이뤄진 리스트 생성
   for (line in  reader.lineSequence()) {
      try {
         val number = line.toInt()
         result.add(number)
      }
      catch(e: NumberFormatException) {
         result.add(null)  //null 원소 가능
      }
   }
   return result
}

 

List<Int?> 는 null이 가능한 Int? 타입의 값을 저장할 수 있습니다.

즉, 이 리스트에는 Int나 null을 저장할 수 있습니다.

 

null이 가능한 컬렉션을 만들 때에는 null이 될 수 있는 게 컬렉션의 원소인지, 아니면 컬렉션 자체인지를 잘 생각해야 합니다!

 

List<Int?> : 리스트 자체는 null이 아니고, 원소가 null이 될 수도 있다.

List<Int>? : 리스트가 null이 될 수도 있지만, 원소 자체는 null이 아닌 값만 들어간다.

List<Int?>? : 리스트 자체, 리스트 원소 둘 다 null이 가능하다.

이런 경우가 있기 때문에, 코틀린 표준 라이브러리에서는 리스트의 null값을 걸러내주는 "filterNotNull" 이라는 유용한 함수를 제공합니다.

 

 

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

코틀린 컬렉션과 자바 컬렉션의 차이를 나누는 가장 중요한 특성 하나는 코틀린에서는 컬렉션을 읽기 전용과 변경 가능한 인터페이스로 분리했다는 점입니다.

 

코틀린은 컬렉션 데이터를 읽기만 하는 인터페이스로 kotlin.collections.Collection 으로 시작합니다.

반대로 컬렉션의 데이터까지 수정을 하고자 한다면 kotlin.collections.MutableCollection 인터페이스를 사용하면 됩니다.

이 MutableCollection은 일반 인터페이스인 kotlin.collections.Collection 를 확장하면서 원소를 추가, 삭제 등의 메소드를 제공합니다.

 

MutableCollection은 Collection을 확장하면서 컬렉션 내용을 변경하는 메소드를 더 제공해준다.

 

코드에서 가능하면 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼는 것이 좋고, 컬렉션을 변경할 필요가 있을 때만! 변경 가능한 Mutable 버전을 사용해주는 것이 좋습니다!

 

코틀린에서 val과 var의 구별과 마찬가지로 컬렉션의 읽기 전용 인터페이스와 변경 가능 인터페이스를 구별한 이유는 프로그램에서 데이터에 어떤 일이 벌어지는지를 더 쉽게 컴파일러가 이해하기 위함입니다.

멀리 쓰레드 환경에서는 쓰레드가 항상 안전하지 않을 수 있기 때문에 데이터를 다루는 경우 그 데이터에 적절히 동기화하거나 동시 접근에 따라 허용하는 데이터 구조를 활용할 수 있게 하기 위함입니다.

 

코틀린 컬렉션과 자바

모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스이기 때문에, 따라서 코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없습니다.

코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용과 변경 가능한 인터페이스로 두 가지 표현을 제공합니다.

 

 

코틀린 컬렉션 인터페이스 계층 구조

 

코틀린의 읽기 전용과 변경 가능한 인터페이스의 기본 구조는 java.util 패키지에 있는 자바 컬렉션 인터페이스의 구조를 그대로 사용하며, 변경 가능한 각 인터페이스는 읽기 전용의 인터페이스를 확장(상속)하여 만들었습니다.

코틀린은 자바의 ArrayList와 HashSet이 마치 코틀린의 MutableList, MutableSet을 상속한 것처럼 취급하여 이런 방식을 통해 자바 호환성을 제공하며 읽기 전용과 변경 가능 인터페이스를 분리하는 모습을 보여줍니다.

 

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

 

자바 메소드를 호출하되 컬렉션을 파라미터로 넘겨야 한다면 코틀린에서 아무 Collection이나 MutableCollection을 넘기면 됩니다.

자바는 읽기 전용과 변경 가능 컬렉션 구분이 따로 없으므로 코틀린에서 읽기 전용 컬렉션 객체라도 자바 코드에서는 그 컬렉션의 내용을 변경할 수 있습니다.

이유는, 코틀린 컴파일러는 자바 코드가 컬렉션에 대해 어떤 일을 하는지 완전히 분석할 수도 없고 감지할 방법도 없기 때문에 이런 부분까지는 막을 수가 없습니다.

 

따라서 올바른 파라미터 타입을 사용할 책임은 우리에게 있습니다. 😂

 

예로 코틀린의 읽기 전용 컬렉션을 자바에서 사용하는 코틀린/자바 혼용 예제를 보여드리겠습니다.

 

/* 자바 코드 */
public List<String> updateItem(List<String> items) {
   items.set(0, "A");  //수정 가능
   return items;
}

/* 코틀린 코드 */
>>> val list = lisfOf("a", "b", "c")  //읽기 전용 컬렉션
>>> println(updateItem(list))  //자바 메소드에 파라미터로 코틀린 읽기 전용 컬렉션 전달

[A, b, c]

 

객체의 배열과 원시 타입(primitive)의 배열

코틀린에서 배열을 만드는 방법은 아래와 같이 다양합니다.

 

- arrayOf 함수에 원소를 명시하여 배열을 만들 수 있습니다.

  ex) val array = arrayOf("a", "b", "c")  // [a, b, c]

 

- arrayOfNulls 함수에 배열 크기를 위한 정수 값을 명시하면 모든 원소가 null이고 정수 값만큼의 크기인 배열을 만들 수 있습니다.

  배열의 크기만 지정하고 값은 나중에 초기화하고자 할 경우 사용합니다.

  ex) val array = arrayOfNulls<Int>(3)  // [null, null, null]  크기가 3이고 null 값을 포함할 수 있는 배열

 

- Array 생성자는 배열 크기와 람다를 파라미터로 받아서 람다를 호출해서 각 배열 원소를 초기화해줍니다.

  arrayOf를 쓰지 않고 각 원소가 null이 아닌 배열을 만들어야 하는 경우에 이 생성자를 사용합니다.

  ex) val array = Array(3, { "" })  // 크기는 3이고, 각 원소는 "" 공백으로 채워져 있는 배열

 

Int 배열을 위해 Array<Int> 로 사용해도 되지만, 코틀린은 원시타입(primitive) 마다의 배열 클래스를 제공해줍니다.

IntArray, ByteArray, CharArray 등...과 같은 기본 타입의 배열 클래스가 존재하며, String은 기본 타입이 아니기 때문에 StringArray는 존재하지 않습니다.

대신 String Array는 Array(10, { "" }) 와 같은 식으로 구현할 수 있습니다.

 

아래는 Boxing하지 않은, 코틀린 원시타입(primitive) 배열을 만드는 예제 입니다.

 

val anyArray = arrayOf(1, "코틀린", true, 0.5f)

/* 제네릭 */
val array = arrayOf<Int>(1, 2, 3) // [1, 2, 3]
val array = Array<Int>(3, {0})    // [0, 0, 0]

/* 원시타입(primitive) 배열 클래스 */
val array = IntArray(3)           // [0, 0, 0]
val array = IntArray(3) { 1 }     // [1, 1, 1]

val array = intArrayOf(0, 0, 0)   // [0, 0, 0]
val array = charArrayOf('A', 'B', 'C')

 

Q. Array<Int> 또는 arrayOf(1,2,3)... 을 써도 되는데 왜 굳이 IntArray를 쓰는 건가요?

A. 코틀린은 자바의 primitive 타입 배열인 int[ ], byte[ ], char[ ] 등을 지원하기 때문에, 이러한 기본 자료형을 Wrapper 타입으로 바꾸는 Boxing 과정을 하지 않고 가장 효율적인 방식으로 사용하기 위해서입니다. 😉