[Kotlin in Action] 6.2 코틀린의 원시 타입
코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다. 이번 장에서는 코틀린 내부에서 어떻게 원시 타입에 대한 래핑이 작동하는지 알아보자. 또한 Object, Void 등의 자바 타입과 코틀린 타입 간의 대응 관계에 대해서도 살펴본다.
6.2.1 원시 타입: Int, Boolean 등
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용한다.
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
래퍼 타입을 따로 구분하지 않으면 편리하다. 더 나아가 코틀린에서는 숫자 타입 등 원시 타입의 값에 대해 메소드를 호출할 수 있다. 예를 들어, 다음 코드는 표준 라이브러리 함수 coerceIn을 사용해 값을 특정 범위로 제한한다.
fun showProgress(progress: Int) [
val percent = progress.coerceIn(0, 100)
println("We're ${percent}% done!")
}
>>> showProgress(146)
We're 100% done!
원시 타입과 참조 타입이 같다면 코틀린은 항상 객체로 표현하는 것일까? 그렇게 한다면 너무 비효율적이지 않을까? 실제로도 항상 객체로 표현한다면 비효율적이겠지만, 코틀린은 그러지 않는다.
실행 시점에 숫자 타입은 가능한 한 가장 효율적인 방식으로 표현된다. 대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일 된다. 이런 컴파일이 불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우 뿐이다. 예를 들어 Int 타입을 컬렉션의 타입 파라미터로 넘기면 그 컬렉션에는 Int의 래퍼 타입에 해당하는 java.lang.Integer 객체가 들어간다.
Int와 같은 코틀린 타입에는 null 참조가 들어갈 수 없기 때문에 쉽게 그에 상응하는 자바 원시 타입으로 컴파일 할 수 있다. 마찬가지로 반대로 자바 원시 타입의 값은 결코 null이 될 수 없으므로 자바 원시 타입을 코틀린에서 사용할 때도 (플랫폼 타입이 아니라) null이 될 수 없는 타입으로 취급할 수 있다.
6.2.2 null 이 될 수 있는 원시 타입: Int?, Boolean? 등
null 참조를 자바의 참조 타입의 변수에만 대입할 수 있기 때문에 null이 될 수 있는 코틀린 타입은 자바 원시 타입으로 표현할 수 없다. 따라서 코틀린에서 null이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 된다.
// null이 될 수 있는 원시 타입
data class Person(val name: String, val age: Int? = null) {
fun isOlderThan(other: Person): Boolean? {
if (age == null || other.age == null
return null
return age > other.age
}
}
>>> println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
false
>>> println(Person("Sam", 35).isOlderThan(Person("Jane")))
null
null이 될 가능성이 있는 Int? 타입의 두 값을 직접 비교할 수는 없다. 먼저 두 값이 모두 null인지 아닌지 검사해야한다. 컴파일러는 null 검사를 마친 당므에야 두 값을 일반적인 값처럼 다루게 허용한다.
6.2.3 숫자 변환
코틀린과 자바의 가장 큰 차이점 중 하나는 숫자를 변환하는 방식이다. 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다. 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우조차도 자동 변환은 불가능하다. 예를 들어 코틀린 컴파일러는 다음 코드를 거부한다.
val i = 1
val l: Long = i // "Error: type mismatch" 컴파일 오류 발생
대신 직접 변환 메소드를 호출해야 한다.
val i = 1
val l: Long = i.toLong()
코틀린은 모든 원시 타입에 대한 변환 함수를 제공한다. 그런 변환 함수의 이름은 toByte(), toShort(), toChart() 등과 같다. 양방향 변환 함수가 모두 제공된다.
Int와 Long을 비교해도 안된다. 코틀린에서는 타입을 명시적으로 변환해서 같은 타입의 값으로 만든 후 비교해야한다.
6.2.4 Any, Any?: 최상위 타입
코틀린에서는 Any 타입이 모든 null이 될 수 없는 타입의 조상 타입이다.
Any가 null이 될 수 없는 타입임에 유의하라. 코틀린에서 null을 포함하는 모든 값을 대입할 변수를 선언하려면 Any? 타입을 사용해야 한다.
6.2.5 Unit 타입: 코틀린의 void
코틀린 Unit 타입은 자바 void와 같은 기능을 한다.
그렇다면 코틀린의 Unit이 자바 void와 다른 점은 무엇일까? Unit은 모든 기능을 갖는 일반적인 타입이며, void와 달리 unit을 타입 인자로 쓸 수 있다. Unit 타입에 속한 값은 단 하나뿐이며, 그 이름도 Unit이다. Unit 타입의 함수는 Unit 값을 묵시적으로 반환한다. 이 두 특성은 제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환 타입으로 Unit을 쓸 때 유용하다.
interface Processor<T> {
fun process(): T
}
class NoResultProcessor : Processor<Unit> {
override fun process() { // Unit을 반환하지만 타입을 지정할 필요는 없다.
// 업무 처리 코드
} // 여기서 return을 명시할 필요가 없다. 컴파일러가 묵시적으로 return Unit을 넣어준다.
}
6.2.6 Nothing 타입: 이 함수는 결코 정상적으로 끝나지 안흔다
코틀린에는 결코 성공적으로 값을 돌려주는 일이 없으므로 '반환 값'이라는 개념 자체가 의미 없는 함수가 일부 존재한다. 예를 들어 테스트 라이브러리들은 fail이라는 함수를 제공하는 경우가 많다. fail은 특별한 메시지가 들어있는 예외를 던져서 현재 테스트를 실패시킨다. 다른 예로 무한 루프를 도는 함수도 결코 값을 반환하지 않으며, 정상적으로 끝나지 않는다.
그런 함수를 호출하는 코드를 분석하는 경우 함수가 정상적으로 끝나지 않는다는 사실을 알면 유용하다. 그런 경우를 표현하기 위해 코틀린에는 Nothing이라는 특별한 반환 타입이 있다.
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
>>> fail("Error occurred")
java.lang.IllegalStateException: Error occurred
Nothing 타입은 아무 값도 포함하지 않는다. 따라서 Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다. 그 외의 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하더라도 그 변수에 아무 값도 저장할 수 없으므로 아무 의미도 없다.
Nothing을 반환하는 함수를 엘비스 연산자의 우항에 사용해서 전제 조건을 검사할 수 있다.
val address = company.address ?: fail("No address")
println(address.city)
컴파일러는 Nothing이 반환 타입인 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용한다. 앞의 예제에서 컴파일러는 company.address가 null인 경우 엘비스 연산자의 우항에서 예외가 발생한다는 사실을 파악하고 address의 값이 null이 아님을 추론할 수 있다.