원시 타입과 래퍼 타입을 구분하지 않는 이유와 코틀린 내부에서 어떻게 기본 타입에 대한 래핑이 이루어지는지 살펴보자.
Object, Void 등의 자바 타입과 코틀린 타입 간의 대응 관계를 살펴보자
8.1.1 정수, 부동소수점 수, 문자, 불리언 값을 원시 타입으로 표현
코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다. 항상 같은 타입을 사용한다.
컬렉션에 담는 래퍼 타입이 따로 있지 않다는 것이다.
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
래퍼 타입을 따로 구분하지 않으면 편리한데, 숫자 타입 등 원시 타입의 값에 대해 메서드를 호출할 수 있다
fun showProgress(progress: Int){
val percent = progress.coreIn(0, 100)
println("We're $percent % done!")
}
fun main(){
showProgress(146)
// We're 100 % done!
}
코틀린이 원시 타입을 항상 객체로 표현하는 것은 아니다.
실행 시점에 숫자 타입은 가능한 한 가장 효율적인 방식으로 표현된다.
코틀린의 Int 타입은 자바 int 타입과 동일하게 컴파일되는데 이런 컴파일이 불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우뿐이다.
Int 타입을 컬렉션의 타입 파라미터로 넘기면 그 컬렉션에는 Int의 래퍼 타입에 해당하는 java.lang.Integer 객체가 들어간다.(래퍼 객체)
자바 원시 타입에 해당하는 코틀린 타입의 전체 목록
정수 타입 : Byte, Short, Int, Long
부동소수점 숫자 타입: Float, Double
문자 타입: Char
불리언 타입: Boolean
8.1.2 양수를 표현하기 위해 모든 비트 범위 사용: 부호 없는 숫자 타입
코틀린의 부호 없는 숫자 타입
타입
크기
값 범위
UByte
8비트
0 ~ 255
UShort
16비트
0 ~ 65535
UInt
32비트
0 ~ 2^32 - 1
ULong
64비트
0 ~ 2^64 -1
* 부호없는 숫자 타입들은 상응하는 부호 있는 타입의 범위를 시프트한다
* 이를 이용해 같은 크기의 메모리를 사용하면서 더 큰 양수 범위를 표현할 수 있게 해준다.
* 다른 원시 타입과 마찬가지로 코틀린의 부호 없는 수도 필요할 때만 래핑된다.
부호 없는 숫자 타입의 값을 사용하는 진짜 목적은 비트와 바이트 수준에서 데이터를 다루고 싶을 때이다. 그런 경우가 아니라면 보통의 원시 타입을 이용해 음수 범위가 함수에 전달됐는지 검사하는 편이 더 낫다.
원래 JVM 자체는 부호가 없는 수에 대한 원시 타입을 지정하지 않거나 제공하지 않는다.
코틀린은 이를 바꿀 수 없고 다만 기존 부호가 있는 원시 타입 위에 추상화를 제공한다.
코틀린은 인라인 클래스를 통해 이런 추상화를 제공하는데 부호없는 값을 표현하는 각 클래스는 사실 실제로는 인라인 클래스이며, 각 인라인 클래스는 자신에 상응하는 부호 있는 타입을 사용한다.
내부에서 UInt 타입의 값은 실제로는 Int인 것이다.
코틀린 컴파일러는 쉽게 Int같은 타입을 그에 상응하는 JVM 원시 타입으로 쉽게 변환할 수 있다.
반대로 자바 원시 타입의 값은 결코 null이 될 수 없으므로 자바 원시 타입을 코틀린에서 사용할 때도 널이 될 수 없는 타입으로 취급할 수 있다.
8.1.3 널이 될 수 있는 기본 타입: Int?, Boolean? 등
자바에서 null 참조는 자바의 참조 타입의 변수에만 대입될 수 있기 때문에 코틀린에서 널이 될 수 있는 코틀린 타입은 자바의 원시 타입으로 표현할 수 없다.
따라서 코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일된다.
코틀린에서 널이 될 수 있는 원시 타입은 자바의 래퍼 타입으로 컴파일 되기 때문에 널이 될 수 있는 원시 타입(Int?와 같은 형식의 타입)은 null인지 검사 후에야 일반 원시 타입처럼 사용이 가능하다.
data class Person(val name: String, val age: Int? = null){
fun isOlderThan(other: Person): Boolean? {
if(age == null || other.age == null) // age > other.age 바로 비교 불가
return null
return age > other.age // null 검사 후에야 원시 타입처럼 비교 연산 가능
}
}
fun main(){
println(Person("Sam",35).isOlderThan(Person("Amy", 42)))
// false
Println(Person("Sam", 35).isOlderThan(Person("Jane")))
}
제네릭 클래스의 경우 래퍼 타입을 사용한다.
어떤 클래스의 타입 인자로 원시 타입을 넘기면 코틀린은 그 타입에 대한 박스 타입(래퍼 타입)을 사용한다.
이렇게 컴파일하는 이유는 JVM에서 제네릭을 구현하는 방법 때문이다.
JVM은 타입 인자로 원시 타입을 허용하지 않는다. 그렇기 때문에 자바나 코틀린 모두에서 제네릭 클래스는 항상 박스 타입을 사용해야 한다.
8.1.4 수 변환
타입이 정해진 변수의 타입 변환: 변환 함수 호출 필요함.
코틀린과 자바의 가장 큰 차이점 중 하나는 수를 변환하는 방식이다.
코틀린은 자바와 달리 한 타입의 수를 다른 타입의 수로 자동 변환하지 않는다.
결과 타입이 허용하는 수의 범위가 원래 타입의 범위보다 넓다 할지라도 자동 변환은 원칙적으로 불가능하다. => 직접 변환 메서드를 호출해야 한다.
val i = 1
val l: Long = i // ERROR: type mismatch 컴파일 오류 발생
val i = 1
val l: Long = i.toLong()
코틀린은 모든 원시 타입(Boolean은 제외)에 대해 toByte(), toShort(), toChar() 등등의 변환 함수를 제공한다.
변수가 아닌 리터럴의 타입 변환: 변환 함수 호출 필요하지 않음
숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요가 없다.
42나 42.0f 처럼 상수 뒤에 타입을 표현하는 문자를 붙이면 변환이 필요 없다.
직접 변환하지 않더라도 숫자 리터럴을 타입이 알려진 변수에 대입하거나 함수에게 인자로 넘기면 컴파일러가 필요한 변환을 자동으로 넣어준다
산술 연산자는 적당한 타입의 값을 받아들일 수 있도록 이미 오버로드돼 있다.
fun printAlong(l: Long) = println(l)
fun main(){
val b: Byte = 1 // 상수 값은 적절한 타입으로 해석
val l = b + lL // +는 Byte와 Long을 인자로 받을 수 있다.(오버로드, 변환 필요 X)
printAlong(42) // 컴파일러는 숫자 리터럴 42를 Long으로 해석석
}
8.1.5 Any와 Any? : 코틀린 타입 계층의 뿌리
자바에서 Object가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다.
자바에서는 참조 타입만 Object를 정점으로 하는 타입 계층에 포함, 원시 타입은 그런 계층에 포함되지 않는다.
코틀린에서는 Any가 Int 등의 원시 타입을 포함한 모든 타입의 조상 타입이다.
코틀린에서 널을 포함하는 모든 값을 대입할 변수를 선언하려면 Any? 타입을 사용해야 한다.
자바 메서드에서 Object를 인자로 받거나 반환하면 코틀린에서는 Any로 그 타입을 취급한다.
코틀린 함수가 Any를 사용하면 자바 바이트코드의 Object로 컴파일된다.
java.lang.Object에서 toString, equals, hashCode 이외의 다른 메서드는 Any에서 사용할 수 없으므로 그런 메서드를 호출하고 싶다면 java.lang.Object 타입으로 값을 캐스트 해야 한다.
8.1.6 Unit 타입: 코틀린의 void
코틀린 Unit 타입은 자바 void와 같은 기능을 한다.
관심을 가질 만한 내용을 전혀 반환하지 않는 함수의 반환 타입으로 Unit을 쓸 수 있다.
fun f(): Unit { /* ... */}
fun f() { /* ... */ } // 반환 타입 명시 X => 반환 타입이 Unit이다.
코틀린 함수의 반환 타입이 Unit이고 그 함수가 제네릭 함수를 오버라이드 하는 경우가 아니라면, 반환 타입이 Unit인 코틀린 함수는 내부에서 자바 void 함수로 컴파일된다.
그런 코틀린 함수를 자바에서 오버라이드 하는 경우엔 void를 반환 타입으로 해야 한다.
코틀린의 Unit이 자바 void와 다른 점
Unit은 모든 기능을 갖는 일반적인 타입, Unit 타입에 속한 값은 단 하나, 그 이름도 Unit이다.
Unit은 void와 달리 타입 파라미터로 사용 가능
Unit이 반환 타입인 함수는 암시적으로 Unit 값을 반환
Unit이 자바 void와 같은 것처럼 보일 수 있지만, 본질적인 '값 없음' 상태를 나타내는 void와는 완전히 다른 개념인 것이다.
Unit이 반환 타입인 함수는 아무 값도 반환하지 않는 것이 아니다.
함수형 프로그래밍에서 전통적으로 Unit은 '단 하나의 인스턴스만 갖는 타입'을 의미해왔고 바로 그 유일한 인스턴스 유무가 자바 void와 코틀린 Unit을 구분하는 가장 큰 차이다.
interface Processor<T> {
fun processor(): T
}
class NoResultProcessor: Processor<Unit> {
override fun process() { // Unit을 반환하지만 반환 타입 명시할 필요는 없다.
// 명시적으로 return문을 작성할 필요가 없다.
}
}
자바에서는 타입 인자로 '값 없음'을 표현하는 문제를 코틀린과 같이 깔끔하게 해결 할 수 없다.
굳이 해결하자면 별도의 인터페이스를 사용해 값을 반환하는 경우와 값을 반환하지 않는 경우를 분리하는 방법을 사용할 수 있다.
다른 방법으로는 타입 파라미터로 특별히 java.lang.Void 타입을 사용하는 방법도 있다.
하지만 Void를 사용한다 하더라도 Void 타입에 대응할 수 있는 유일한 값인 null을 반환하기 위해 return null을 명시해줘야하기 때문에 진정으로 그 어떤 값도 반환하지 않는 상태를 나타낼 수는 없다.
어쩔 수 없이 프로그래밍 언어에서 관례적으로 사용해 온 Void라는 이름을 사용할 수도 있겠지만 코틀린에서는 아무 값도 반환하지 않는 상태를 나타내기 위한 Nothing이라는 전혀 다른 기능을 하는 타입이 하나 존재한다.
8.1.7 Nothing 타입: 이 함수는 결코 반환되지 않는다.
코틀린에는 결코 성공적으로 값을 돌려주는 일이 없어서 '반환값'이라는 개념 자체가 의미가 없는 함수가 일부 존재한다.
fun fail(message: String) : Nothing {
throw IllegalStateException(message)
}
fun main() {
fail("Error Occured")
// java.lang.IllegalStateException: Error Occured
}
Nothing 타입은 아무 값도 포함하지 않는다.
Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다.
그 외의 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하더라도 그 변수에 아무 값도 저장할 수 없으므로 아무 의미가 없다.
Nothing을 반환하는 함수를 엘비스 연산자으 오른쪽에 사용해서 전제 조건을 검사할 수 있다.
val address = company.address ?: fail("No address")
println(address.city)
8.2 컬렉션과 배열
8.2.1 널이 될 수 있는 값의 컬렉션과 널이 될 수 있는 컬렉션
List< Int? >와 List< Int >?
fun readNumbers(text: String): List<Int?> {
val result = mutableListOf<Int?>()
for (line in text.lineSequence()) {
val numberOrNull = line.toIntOrNull()
result.add(numberOrNull)
}
return result
}
어떤 변수 타입의 널 가능성과 타입 파라미터로 쓰이는 타입의 널 가능성 사이의 차이를 알아둬야 한다.
List< Int? > => 리스트 자체는 항상 null이 아니지만 리스트에 들어있는 각 원소는 null이 될 수 있다.
List< Int >? => 리스트를 가리키는 변수에는 null이 들어갈 수 있지만 리스트 안에는 null이 아닌 값만 들어간다.
널이 될 수 있는 값으로 이뤄진 널이 될 수 있는 리스트를 정의해야할 수도 있다.
코틀린에서는 물음표를 2개 사용해 List< Int? >?로 이를 표현한다.
8.2.2 읽기 전용과 변경 가능한 컬렉션
코틀린 컬렉션과 자바 컬렉션을 나눈는 가장 중요한 특성 중 하나는 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다.
kotlin.collections.Collection 인터페이스 => 컬렉션 안의 원소에 대해 이터레이션하기, 컬렉션의 크기 구하기, 컬렉션 안에 어떤 값이 들어있는지 검사하기, 컬렉션 안의 데이터 읽기를 수행할 수 있지만 원소를 추가하거나 제거하는 메서드가 없다.
kotlin.collections.MutableCollection 인터페이스 => 일반 인터페이스인 kotlin.collections.Collection 인터페이스를 확장하면서 원소를 추가하거나, 삭제하거나, 컬렉션 안의 원소를 모두 지우는 등의 메서드를 좀 더 제공한다.
가능하면 코드에서 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 한다.
코드가 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용한다.
어떤 컴포넌트의 내부 상태에 컬렉션이 포함된다면 그 컬렉션을 MutableCollection을 인자로 받는 함수에 전달할 때는 어쩌면 원본의 변경을 막기 위해 컬렉션을 복사해야 할 수도 있다.
fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
for(item in source){ //
target.add(item)
}
}
fun main() {
val source: Collection<Int> = arrayListOf(3, 5, 7)
val target: MutableCollectin<Int> = arrayListOf(1)
copyElements(source, target)
println(target)
// [1, 3, 5, 7]
}
객체가 실제로는 변경 가능한 컬렉션이라 할지라도 copyElements 함수의 target에 해당하는 인자로 읽기 전용 타입의 값을 넘길 수는 없다.
fun main() {
val source: Collection<Int> = arrayListOf(3, 5, 7)
val target: Collectin<Int> = arrayListOf(1)
copyElements(source, target)
println(target)
// Error: Type mismatch: inferred type is Collection<Int>
// but MutableCollection<Int> was expected
}
코드의 일부분이 가변 컬렉션에 대한 참조를 갖고 있고 다른 부분에서 같은 컬렉션에 대한 '뷰'를 갖고 있다면 후자의 코드는 전자가 컬렉션을 동시에 변경할 수 없다는 가정에 의존할 수 없다.
이런 경우 코드가 컬렉션을 사용하는 도중에 다른 스레드 등에 의해 컬렉션이 변경되는 상황이 생길 수 있고, 이로 인해 ConcurrentModificationException이나 다른 오류가 발생할 수 있다.
읽기 전용 컬렉션이 항상 스레드 안전하지는 않다는 점을 명시해야 한다.
함수가 얻은 컬렉션의 뷰가 실제로는 내부에서 변경 가능한 컬렉션을 가리킬 수 있다.
다중 스레드 환경에서 데이터를 다루는 경우 그 데이터를 적절히 동기화하거나 동시 접근을 허용하는 데이터 구조를 활용해야 한다.
같은 컬렉션 객체를 가지키는 다른 타입(읽기 전용 리스트와 변경 가능 리스트)의 참조들
list에 접근하는 코드는 대상 컬렉션을 변경할 수 없지만 mutableList를 통해 접근하는 코드를 통해 대상 컬렉션을 변경할 수 있다.
8.2.3 코틀린 컬렉션과 자바 컬렉션은 밀접히 연관됨
모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스라는 점은 사실이다.
코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없다. 래퍼 클래스를 만들거나 데이터를 복사할 필요가 없다.
다만 코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능 인터페이스라는 2가지 표현을 제공한다.
코틀린의 읽기 전용 인터페이스와 변경 가능 인터페이스의 기본 구조는 java.util 패키지에 있는 자바 컬렉션 인터페이스의 구조와 같다.
추가로 각 변경 가능 인터페이스는 자신과 대응하는 읽기 전용 인터페이스를 확장한다.
변경 가능한 인터페이스는 java.util 패키지에 있는 인터페이스와 직접적으로 연관되지만 읽기 전용 인터페이스에는 컬력션을 변경할 수 있는 모든 요소가 빠져있다.
자바 ArrayList와 HashSet은 코틀린의 변경 가능 인터페이스를 확장한다.
코틀린은 java.util.ArrayList와 java.util.HashSet 클래스가 각각 코틀린의 MutableList와 MutableSet 인터페이스 상속한 것처럼 취급한다.
컬렉션과 마찬가지로 Map 클래스(Map은 Collection이나 Iterable을 확장하지 않는다.)도 코틀린에서 Map과 MutableMap이라는 2가지 버전으로 나타난다.
* setOf()와 mapOf()는 Set과 Map 읽기 전용 인터페이스의 인스턴스를 반환하지만 내부적으로는 변경 가능한 클래스다.
* 중요한 것은 그 둘이 변경 가능한 클래스라는 사실에 의존하면 안 된다는 것
* setOf()나 mapOf()가 진정한 불변 컬렉션 인스턴스를 반환하게 바뀔 수도 있다.
자바 메서드를 호출하되 컬렉션을 인자로 넘겨야 한다면 따로 변환하거나 복사하는 등의 추가 작업없이 직접 컬렉션을 넘기면 된다.
java.util.Collection을 파라미터로 받는 자바 메서드가 있다면 아무 Collection이나 MutableCollection 값을 인자로 넘길 수 있다.
이 때, 자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로 코틀린에서 읽기 전용Collection으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.
컬렉션을 자바로 넘기는 코틀린 프로그램을 작성한다면 호출하려는 자바 코드가 컬렉션을 변경할 지 여부에 따라 올바른 파라미터 타입을 사용할 책임은 코드 작성자가 져야 한다.
/* 자바 코드 */
// CollectionUtils.java
public classs CollectionUtils {
public static List<String> uppercaseAll(List<String> items) {
for (int i = 0; i < items.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())
}
fun main() {
val list = listOf("a", "b", "c")
printInUppercase(list)
// [A, B, C]
// A
}
8.2.4 자바에서 선언한 컬렉션은 코틀린에서 플랫폼 타입으로 보임
자바의 널 가능성을 코틀린에서 다루는 방식: 자바 코드에서 정의한 타입을 코틀린에서는 플랫폼 타입으로 본다.
플랫폼 타입의 경우 코틀린 쪽에는 널 관련 정보가 없다.
코틀린 컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느 쪽으로든 사용할 수 있도록 허용한다.
타입의 널 가능성과 마찬가지로 자바 쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다.
플랫폼 타입인 컬렉션은 기본적으로 변경 가능성에 대해 알 수 없다.
따라서 코틀린 코드는 그 타입을 읽기 전용 컬렉션이나 변경 가능한 컬렉션 어느 쪽으로든 다룰 수 있다.
컬렉션 타입이 시그니처에 들어간 자바 메서드 구현을 코틀린에서 오버라이드 하려는 경우 읽기 전용 컬렉션과 변경 가능 컬렉션의 차이가 문제가 된다.
플랫폼 타입에서 널 가능성을 다룰 때처럼 이런 경우에도 오버라이드 하려는 메서드의 자바 컬렉션 타입을 코틀린의 어떤 컬렉션 타입으로 표현할지, 읽기 전용 컬렉션 타입으로 할지, 변경 가능 컬렉션 타입으로 표현할지 결정해야 한다.