728x90
반응형
✅ 컬렉션과 배열
- 코틀린 컬렉션이 자바 라이브러리를 바탕으로 만들어졌고 확장 함수를 통해 추가할 수 있다는 것을 배웠다.
- 이 외에 코틀린의 컬렉션 지원과 자바와 코틀린 컬렉션 간의 관계에 대해 더 살펴보도록 하자.
📌 널 가능성과 컬렉션
- 컬렉션 안에 널 값을 넣을 수 있는지 여부는 어떤 변수의 값이 널이 될 수 있는지 여부와 마찬가지로 중요하다.
- 변수 타입 뒤에 ?를 붙이면 그 변수에 널을 저장할 수 있다는 뜻인 것처럼 타입 인자로 쓰인 타입에도 같은 표시를 사용할 수 있다.
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")
}
- 위의 예제를 보면 리스트의 원소에 접근할 경우 Int? 타입의 값을 얻는다.
- 따라서 그 값을 산술식에 사용하기 전에 널 여부를 검사해야 한다.
- 널이 될 수 있는 값으로 이뤄진 컬렉션으로 널 값을 걸러내는 경우가 자주 있어 코틀린 표준 라이브러리는 그런 일을 하는 filterNotNull이라는 함수를 제공한다.
- 다음 예제를 살펴보면 filterNotNull이 컬렉션 안에 널이 들어있지 않음을 보장해주므로 validNumbers는 List<Int> 타입이다.
fun addValidNumbers(numbers: List<Int?>) {
val validNumbers = numbers.filterNotNull()
println("Sum of valid numbers: ${validNumbers.sum()}")
println("Invalid numbers: ${numbers.size - validNumbers.size}")
}
📌 읽기 전용과 변경 가능한 컬렉션
- 코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 하나는 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다.
- Collection (kotlin.collections.Collection)
- 컬렉션 안의 원소에 대해 이터레이션한다.
- 컬렉션의 크기를 얻는다.
- 어떤 값이 컬렉션 안에 들어있는지 검사한다.
- 하지만 컬렉션에 원소를 추가하거나 제거하는 메소드가 없다.
- MutableCollection (kotlin.collections.MutableCollection)
- 컬렉션의 데이터를 수정한다.
- Collection을 확장하면서 원소를 추가하거나, 삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메소드를 더 제공한다.
- Collection (kotlin.collections.Collection)
- 코틀린이 Collection과 MutableCollection을 구별한 이유는 프로그램에서 데이터에 어떤 일이 벌어지는지를 더 쉽게 이해하기 위함이다.
- 어떤 함수가 Collection 타입의 인자를 받는다면 그 함수는 읽기만 할 것이다.
- 반면에 MutableCollection 타입의 인자를 받는 경우 그 함수가 컬렉션의 데이터를 바꾸리라 가정할 수 있다.
- 이러한 구분을 통해 데이터 변경에 대한 작업을 더 쉽게 이해할 수 있다.
📌 코틀린 컬렉션과 자바
- 모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스이다.
- 따라서 코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없다.
- 또한 래퍼 클래스를 만들거나 데이터를 복사할 필요도 없다.
- 하지만 코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스라는 두 가지 표현을 제공한다.
- 컬렉션과 마찬가지로 Map 클래스도 코틀린에서 Map과 MutableMap이라는 두 가지 버전으로 나타난다.
- 아래 표는 컬렉션 생성 함수를 보여준다.
컬렉션 타입 | 읽기 전용 타입 | 변경 가능 타입 |
List | listOf | mutableListOf, arrayListOf |
Set | setOf | mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf |
Map | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf |
- 이러한 성질로 인해 컬렉션의 변경 가능성과 관련해 중요한 문제가 생긴다.
- 자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로, 코틀린에서 읽기 전용 Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.
- 따라서 컬렉션을 변경하는 자바 메소드에 읽기 전용 Colleciton을 넘겨도 코틀린 컴파일러가 이를 막을 수 없다.
/* 자바 코드 */
// CollectionUtils.java
public class CollectionUtils {
public static List<String> uppercaseAll(List<String> items) {
for(int i=0; i<itmes.size(); i++) {
items.set(i, items.get(i).toUpperCase());
}
return items;
}
}
/* 코틀린 코드 */
// collections.kt
fun printInUppercase(list: List<String) { // 읽기 전용 파라미터를 선언한다.
println(CollectionUtils.uppercaseAll(list)) // 컬렉션을 변경하는 자바 함수를 호출한다.
println(list.first()) // 컬렉션이 변경했는지 살펴본다.
}
val list = listOf("a", "b", "c")
printInUppercase(list)
// [A, B, C]
// A
- 따라서 컬렉션을 자바로 넘기는 코틀린 프로그램을 작성한다면 호출하려는 자바 코드가 컬렉션을 변경할지 여부에 따라 올바른 파라미터 타입을 사용해야 한다.
📌 컬렉션을 플랫폼 타입으로 다루기
- 앞서 설명한 널 가능성을 기억한다면 자바 코드에서 정의한 타입을 코틀린에서는 플랫폼 타입으로 본다.
- 아래 글을 통해 플랫폼 타입에 대해 확인할 수 있다.
[Kotlin In Action] 6장. 코틀린 타입 시스템(2) - 널 가능성[2]
널 가능성[1]의 내용과 이어집니다. [Kotlin In Aciton] 6장. 코틀린 타입 시스템(1) - 널 가능성[1] ✅ 널 가능성 널 가능성(nullability)은 NullPointer Exception(NPE)를 피할 수 있게 돕기 위한 코틀린 타입 시스
dahoonkk.tistory.com
- 플랫폼 타입의 경우 코틀린 쪽에는 널 관련 정보가 없다.
- 따라서 컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입 혹은 널이 될 수 없는 타입 어느 쪽으로든 다룰 수 있다.(개발자가 책임을 져야 한다.)
- 하지만 컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드하려는 경우 읽기 전용 컬렉션과 변경 가능 컬렉션의 차이가 문제가 된다.
- 플랫폼 타입에서 널 가능성을 다루 ㄹ때처럼 이런 경우에도 오버라이드하려는 메소드와 자바 컬렉션 타입을 어떤 코틀린 컬렉션 타입으로 표현할지 결정해야 한다.
- 그런 상황에서는 여러 가지를 선택해야 하며, 선택한 내용을 바탕으로 코틀린에서 사용할 컬렉션 타입에 반영해야 한다.
- 플랫폼 타입을 Kotlin에서 사용할 때 고려할 사항은 다음과 같다.
- 컬렉션이 널이 될 수 있는가?
- 컬렉션의 원소가 널이 될 수 있는가?
- 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?
- 아래 코드를 통해 내용을 살펴보자.
/* 자바 */
interface FileContentProcessor {
void processContents(File path,
byte[] binaryContents,
List<String> textContents);
}
- 위의 자바 인터페이스를 구현하려면 코틀린에서는 아래 내용을 선택해야 한다.
- 일부 파일은 이진 파일이며 이진 파일 안의 내용은 텍스트로 표현할 수 없는 경우가 있으므로 리스트는 널이 될 수 있다.
- 파일의 각 줄은 널일 수 없으므로 이 리스트의 원소는 널이 될 수 없다.
- 이 리스트는 파일의 내용을 표현하며 그 내용을 바꿀 필요가 없으므로 읽기 전용이다.
- 다음은 위의 고려사항을 바탕으로 코틀린으로 구현한 모습이다.
class FileIndexer : FileContentProcessor {
override fun processContents(path: File,
binaryContents: ByteArray?,
textContents: List<String>) {
// ...
}
}
- 이처럼 컬렉션과 플랫폼 타입 변환 시에 선택을 제대로 하기 위해서는 자바 인터페이스나 클래스가 어떤 맥락에서 사용되는지 정확히 알아야 한다.
- 보통 자바에서 가져온 컬렉션에 대해 코틀린 구현에서 어떤 작업을 수행해야 할지 검토하면 쉽게 결정을 내릴 수 있을 것이다.
📌 객체의 배열과 원시 타입의 배열
- 코틀린 배열은 타입 파라미터를 받는 클래스이다.
- 배열의 원소 타입은 바로 그 타입 파라미터에 의해 결정된다.
- 코틀린에서 배열은 만드는 방법은 다양하다.
- arrayOf() 함수에 원소를 넘기면 배열을 만들 수 있다.
- arrayOfNulls() 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다.
- 원소 타입이 널이 될 수 있는 타입인 경우에만 사용
- Array 생성자는 배열 크기와 람다를 인자로 받아 람다를 호출하여 각 배열 원소를 초기화 해준다.
- arrayOf()를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 사용
val letters = Array<String>(26) { i -> {'a' + i).toString() }
println(letters.joinToString(""))
// abcdefghijklmnopqrstuvwxyz
- 코틀린은 원시 타입의 배열을 표현하는 별도 클래스를 각 원시 타입마다 하나씩 제공한다.
- IntArray : int []
- ByteArray : byte[]
- CharArray : char[]
- 원시 타입의 배열을 만드는 다음과 같다.
- 각 배열 타입의 생성자는 size 인자를 받아 해당 원시 타입의 디폴트 값으로 초기화된 size 크기의 배열을 반환한다.
- 팩토리 함수(IntArray를 생성하는 intArrayOf 등)는 여러 값을 가변 인자로 받아 그런 값이 들어간 배열을 반환한다.
- 크기와 람다를 인자로 받는 생성자를 사용한다.
val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
val squares = IntArray(5) { i -> (i+1) * (i+1) }
728x90
반응형
'Studying > Kotlin' 카테고리의 다른 글
[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례(2) - 비교 연산자 오버로딩 (0) | 2023.06.02 |
---|---|
[Kotlin In Action] 7장. 연산자 오버로딩과 기타 관례(1) - 산술 연산자 오버로딩 (0) | 2023.06.02 |
[Kotlin In Action] 6장. 코틀린 타입 시스템(3) - 코틀린의 원시 타입 (0) | 2023.05.31 |
[Kotlin In Action] 6장. 코틀린 타입 시스템(2) - 널 가능성[2] (0) | 2023.05.30 |
[Kotlin In Aciton] 6장. 코틀린 타입 시스템(1) - 널 가능성[1] (0) | 2023.05.25 |