KotlinInAction/코틀린 타입 시스템

코틀린의 원시 타입

webmaster 2022. 8. 7. 15:16
728x90

원시 타입: Int, Boolean 등

자바의 원시 타입(int, double) 변수에는 그 값이 직접 들어가지만, 참조 타입(String)의 변수에는 메모리 상의 객체 위치가 들어간다.

원시 타입의 값을 더 효율적으로 저장하고, 여기저기 전달할 수 있지만, 메서드를 호출하거나 컬렉션에 원시 타입 값을 담을 수는 없다.

자바는 참조 타입이 필요한 경우 특별한 래퍼 타입으로 원시 타입을 감싸서 사용한다.

fun main(args: Array<String>) {
    val i:Int = 1
    val list:List<Int> = listOf(1, 2, 3)
}
  • 코틀린은 원시 타입과 래퍼타입을 구분하지 않고 항상 같은 타입을 사용한다
fun main(args: Array<String>) {
    showProgress(146)
}

fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)
    println("We 're ${percent} % done!")
}
  • 코틀린에서는 숫자 타입 등 원시 타입의 값에 대해 메서드를 호출할 수 있다.
  • 코틀린은 실행 시점에 숫자 타입은 가능한 가장 효율적인 방식으로 표현된다
    • 대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일된다(불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우만 있다)
  • Int와 같은 코틀린 타입에는 널 참조가 들어갈 수 없기 때문에 쉽게 그에 상응하는 자바 원시 타입으로 컴파일이 가능하다.
  • 반대로 자바 원시 타입의 값은 결코 널이 될 수 없으므로 자바 원시 타입을 코틀린에서 사용할 때도 널이 될 수 없는 타입으로 취급 가능하다

널이 될 수 있는 원시 타입: Int?, Boolean? 

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
    }
}
fun main(args: Array<String>) {
    println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
    println(Person("Sam", 35).isOlderThan(Person("Jane")))
}
  • 널이 될 가능성이 있으므로 Int? 타입의 두 값을 직접 비교할 수는 없다.
    • 먼저 두 값이 모두 널이 아닌지 검사한 뒤, 컴파일러는 널 검사를 마친 다음에야 두 값을 일반적인 값처럼 다루는 것을 허용한다.
  • Person 클래스에 선언된 age 프로퍼티 값은 Integer로 저장된다.
fun main(args: Array<String>) {
 	val listOfInts = listOf(1, 2, 3)
}
  • 제네릭 클래스 같은 경우 래퍼 타입을 사용한다.
    • 어떤 클래스의 타입 인자로 원시 타입을 넘기면 코틀린은 그 타입에 대한 박스 타입을 사용한다.
  • 이렇게 컴파일하는 이유는 자바 가상 머신에서 제네릭을 구현하는 방식 때문이다.
    • JVM은 타입 인자로 원시 타입을 허용하지 않는다
    • 따라서 자바, 코틀린 모두 항상 박스 타입을 사용해야 한다.
  • 원시 타입으로 이뤄진 대규모 컬렉션을 효율적으로 저장해야 한다면, 서드 파티 라이브러리를 사용하거나 배열을 사용해야 한다.

숫자 변환

fun main(args: Array<String>) {
    val i = 1
    val l: Long = i
}

발생한 예외

  • 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
    • 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우조차도 자동 변환은 불가능하다
fun main(args: Array<String>) {
    val i = 1
    val l: Long = i.toLong()
}
  • 직접 변환 메서드를 호출해야 변환 가능하다
  • 코틀린은 모든 원시 타입에 대한 변환 함수를 제공한다(Boolean 타입 제외)
    • 함수의 이름은 toByte, toShort, toChar 등과 같다
  • 양방향 변환 함수 모두 제공된다
    • 더 좁은 범위 -> 더 넓은 범위로 변환하는 함수, 더 넓은 범위 -> 더 좁은 범위로 일부를 잘라내는 함수 모두 있다
fun main(args: Array<String>) {
    val x = 1
    val list = listOf(1L, 2L, 3L)
    //println(x in list) //false가 반환된다(묵시적 타입 변환으로 인해)
    println(x.toLong() in list) //명확하게 타입지정이 되야한다
}
  • 코틀린은 개발자의 혼란을 피하기 위해 타입 변환을 명시하기로 결정하였고, 박스 타입을 비교하는 경우 두 박스 타입 간의 equals 메서드는 그 안에 들어있는 값이 아니라 박스 타입 객체를 비교한다.
  • x in list는 묵시적 변환으로 인해서 컴파일되지 않는다
  • 코틀린에서는 타입을 명시적으로 변환해서 같은 타입을 값으로 만든 후, 비교해야 한다

TIP : 원시 타입 리터럴

코틀린이 허용하는 숫자 리터럴

  • L 접미사가 붙은 Long 타입 리터럴: 123L
  • 표준 부동 소수점 표기법을 사용한 Double 타입 리터럴 : 0.12
  • f 나 F 접미사가 붙은 Float 타입 리터럴 : 123.4f, 123.4F
  • 0x, 0X 접두사가 붙은 16진 리터럴 : 0xCAF
  • 0b, 0B 접두사가 붙은 2진 리터럴 : 0b001

코틀린 1.1부터는 숫자 리터럴 중간에 밑줄을 넣을 수 있다: 1_234

문자 리터럴 같은 경우 자바와 마찬가지 구문 사용

fun main(args: Array<String>) {
    val b: Byte = 1 //상수 값은 적절한 타입으로 해석
    val l = b + 1L //+는 byte와 long을 인자로 받을 수 있다
    foo(42) //컴파일러는 42를 Long값으로 해석
}
fun foo(s: String?) {
    val t: String = s ?: ""//s 가 null 이면  결과는 "" 이다.
}
  • 숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요 없다
  • 직접 변환하지 않아도 숫자 리터럴 타입이 알려진 변수에 대입하거나 함수에게 인자로 넘기면 컴파일러가 필요한 변환을 자동으로 넣어준다
  • 산술 연산자는 적당한 타입의 값을 받아들일 수 있게 이미 오버로드 돼있다
    • 코틀린 산술 연산자에서도  숫자 연산 시 값 넘침이 발생할 수 있다
    • 코틀린은 값 넘침을 검사하느라 추가 비용을 들이지 않는다

TIP : 문자열 -> 숫자로 변환

코틀린 표준 라이브러리는 문자열을 원시 타입으로 변환하는 여러 함수를 제공한다(toInt, toByte, toBoolean).

이런 함수는 원시 타입을 표기하는 문자열로 파싱 하는데 오류가 발생하면 NumberFormatException이 발생한다.

최상위 타입(Any, Any?)

자바에서는 object가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any타입이 모든 널이 될 수 없는 타입의 조상이다.

하지만 자바에서는 참조 타입만 Object를 정점으로 하는 타입 계층에 포함되며, 원시 타입은 그런 계층에 들어있지 않는데 이는 자바에서 object 타입의 객체가 필요할 경우 원시 타입을 래퍼 타입으로 감싸야한다는 의미이다. 

코틀린에서는 Any가 Int 등의 원시 타입을 포함한 모든 타입을 조상으로 감쌀 필요가 없다

fun main(args: Array<String>) {
    val answer: Any = 42 //Any가 참조 타입이기 때문에 42 박싱
}
  • Any가 널이 될 수 없는 타입에 유의
    • Any 타입에 널이 들어갈 수 없으므로 널을 포함하는 모든 값을 대입할 변수를 선언하려면 Any?를 사용해야 한다
  • 내부에서 Any 타입을 Object에 대응한다.
    • 코틀린 함수가 Any를 사용하면 자바 바이트코드의 Object로 컴파일된다.
  • toString, equals, hashCode는 모두 Any에 정의된 메서드를 상속한 것이지만 Object에 있는 다른 메서드 (wait, notify 등)은 Any에서 사용할 수 없고, 호출하고 싶다면 object 타입으로 값을 캐스팅해야 한다

코틀린의 void(Unit 타입)

자바의 Void와 같은 기능을 한다. 관심을 가질 만한 내용을 전혀 반환하지 않는 함수의 반환 타입으로 사용할 수 있다

//반환 타입을 명시하지 않은 함수와 같다
fun f(): Unit {
    //logic
}
fun f(){
    //logic
}
  • Unit 반환 타입은 반환 타입 선언 없이 정의한 블록이 본문인 함수와 같다
  • 코틀린 함수의 반환 타입이 Unit이고 그 함수가 제네릭 함수를 오버라이드 하지 않는다면 그 함수는 내부에서 자바 void 함수로 컴파일된다.
    • 그런 코틀린 함수를 자바에서 오버라이드 하는 경우 void를 반환 타입으로 해야 한다
  • Unit과 void 차이가 무엇일까?
    • Unit은 모든 기능을 갖는 일반적인 타입이면 void와 달리 Unit타입 인자로 쓸 수 있다
    • Unit타입에 속한 값은 다 하나뿐이며 이름도 unit이다
    • Unit 타입의 함수는 unit 값을 묵시적으로 반환한다
interface Processor<T>{
    fun process(): T
}
class NoResultProcessor: Processor<Unit> {
    override fun process() {
        //logic
        //리턴을 명시할 필요가 없다
    }
}
  • 인터페이스의 시그니처는 process함수가 어떤 값을 반환하라고 요구하기 때문에 Unit타입도 값을 제공하므로 Unit 값을 반환하는데 아무 문제가 없다
  • 또한 명시적으로 return시 Unit을 반환할 필요가 없다
    • 컴파일러가 묵시적으로 return Unit을 넣어준다
  • 타입 인자로 '값없음'을 표현하는 문제를 자바에서는 별도의 인터페이스를 사용해 값을 반환하는 경우와 값을 반환하지 않는 경우를 분리하는 방법이 있고, Void타입을 사용하는 방법이 있다
    • Void 같은 경우 메서드의 마지막에 항상 return null을 명시해야 한다.

이 함수는 절대 정상적으로 끝나지 않는다(Nothing 타입)

코틀린에서는 결코 성공적으로 값을 돌려주는 일이 없어 '반환 값'이라는 개념 자체가 의미 없는 함수가 일부 존재한다.

그런 함수를 호출하는 코드를 분석하는 경우 함수가 정상적으로 끝나지 않는다는 사실을 알면 유용하다. 그런 경우를 표현하기 위해 코틀린에서는 Nothing이라는 특별한 반환 타입이 있다

fun main(args: Array<String>) {
    fail("Error occurred")
}
fun fail(message: String): Nothing{
    throw IllegalStateException(message)
}
  • Nothing 타입은 아무 값도 포함하지 않는다. 따라서 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다
    • 그 외 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하더라도 그 변수에 아무 값도 저장할 수없으므로 의미가 없다'
fun main(args: Array<String>) {
    val address = company.address?: fail("No address")
    println(address.city)
}

fun fail(message: String): Nothing{
    throw IllegalStateException(message)
}
  • Nothing을 반환하는 함수를 엘비스 연산자의 우항에 사용해 전제 조건을 검사할 수 있다
  • 컴파일러는 Nothing 이 반환 타입인 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용한다.
    • company.address가 널인 경우 엘비스 연산자의 우항에서 예외가 발생한다는 사실을 파악하고 address의 값이 널이 아님을 추론 가능하다
728x90

'KotlinInAction > 코틀린 타입 시스템' 카테고리의 다른 글

컬렉션과 배열  (0) 2022.08.07
널 가능성(2)  (0) 2022.08.06
널 가능성(1)  (0) 2022.08.06