오늘도 더 나은 코드를 작성하였습니까?

코틀린 타입 시스템(코틀린 원시 타입) 본문

Kotlin in Action/코틀린 기초

코틀린 타입 시스템(코틀린 원시 타입)

hik14 2020. 8. 20. 16:21

코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다

 

원시 타입은 변수에 할당한 그 값이 직접 메모리에 들어간다.

참조 타입은 그 객체에 대한 주소 값이 메모리에 들어간다.

 

자바의 경우 참조 타입이 필요한 경우 원시 타입을 래퍼 클래스로 감싸서 사용한다.

예를 들면 Collection <int>가 아닌 Collection <Integer>처럼 말이다.

 

하지만 코틀린은 그렇지 않다 래퍼타입과 원시 타입을 구분하지 않으면 편리하다.

또한 원시타입에 대한 메서드 호출이 가능하다.

*coerceIn은  값을 특정 범위로 한정한다.

fun showProgress(progress: Int){

    val percent = progress.coerceIn(0,100)
    println("$percent done! ")
}

fun main() {
    showProgress(1414)
}

 원시타입과 참조 타입이 같다면 항상 객체로 표현되는 것인가? 그렇다면 매우 비효율적이다.

코틀린은 실행시점에 가능한 가장 효율적인 방법으로 표현한다.

 

변수, 프로퍼티, 파라미터 반환 타입 등은 코틀린의 Int는 자바의 int로 컴파일이 된다.

컬렉션과 제네릭스 클래스를 사용하는 경우는 코틀린의 Int는 자바의 Integer 클래스로 컴파일된다.

 

*자바의 int 같은 원시 타입은 null을 참조할 수 없기 때문에 코틀린에서는 널이 될 수 없는 타입으로 취급된다.

 

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

자바의 원시 타입은 널이 될 수없기 때문에 널이 될 수있는 코틀린의 원시타입은 자바의 래퍼 클래스로 컴파일이 된다.

 

예시

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() {

    val person = Person("Sam", 15)
    val person2 = Person("Amy", 42)
    val person3 = Person("Jane")

    println(person.isOlderThan(person2))
    println(person.isOlderThan(person3))
}

두 사람의 나이를 비교할 때 나이는 널이 될 수 있는 타입이다 그래서 2개의 나이를 널이 아님을 검사하면

컴파일러는 널이 검사를 마친 이 2개의 나이를 일반적인 값으로 다룬다.

Person 클래스의 선언된 age는 Integer로 저장된다.

 

하지만 이러한 자세한 사항은 java에서 선언한 클래스를 사용할 때 고려하면 되고

코틀린에서 적절한 타입은 변수나 프로퍼티에 널을 허용 하냐 아니냐 만 고려하면 된다.

 

제네릭 클래스의 경우 래퍼 타입을 이용한다.

val list = listOf(1, 2, 3, 4, 5)

1 2 3 4 5는 Integer 타입이다.

 

Jvm은 타입 인자(<T>)로 원시 타입을 허용하지 않는다. 따라서 자바나 코틀린 모두 제네릭스 클래스는 항상  래퍼 타입을 사용해야 된다. 

 

숫자 변환

 

코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.

결과 타입이 허용 한 숫자의 범위가 원래 타입의 범위보다 넓은 경우도 허용하지 않는다.

 

코틀린은 개발자의 혼란을 피하기 위해 타입 변환을 명시하도록 하였다.

fun main() {
    val num: Int = 1
    val l: Long = num.toLong()
}

숫자 리터럴을 직접 사용할 때는 변환이 필요 없다.

 

원시 타입 리터럴

 

Long     = 1L 2L 3L

Double  0.1 0.2 0.3

Float     0.1f 0.2f 0.3f

16진수  0 xABCD

2진수  0 b101

 

숫자의 리터럴을 타입이 알려진 변수에 대입 또는 파라미터로 넘길 때는 컴파일러가 필요한 변환을 자동으로 해준다.

 

산술 연산자는 적당한 타입의 값을 받도록 이미 오버로드 되어있다.

fun foo (l: Long) = println(l)

fun main() {

    val b: Byte = 1 // 

    val l = b + 1L // 서로 다른 숫자 타입을 받을수 있도록 오버로딩 되어있다.

    foo(42) // 42는 long타입으로 해석한다.
}

overflow가 존재하며

 

String.toInt() 경우 NumberFormatException이 발생할 수 있다.

Any, Any? : 최상위 타입 

자바의 Object 타입과 대응되지만 널이 될 수 있는가 아닌가에 따라 2개로 나뉜다.

코틀린의 Any는 Int 등 원시 타입을 포함한 모든 타입의 조상이다.

 

정확히는 Object 타입은 Any! 플랫폼 타입으로 취급한다. 

 

Any는 toString equals hashCode  3개의 메서드를 가지고 있고 wait notify는 없다

그러므로  Object의 타입으로 캐스트를 해야 사용 가능하다.

 

Unit 타입 : 코틀린의 void

코틀린의 Unit 타입은 자바의 void와 같은 기능을 하지만 조금 다르다.

관심을 가질 만한 내용을 전혀 반환하지 않는 함수의 타입으로 Unit을 쓸 수 있다.

 

코틀린의 Unit 반환하는 함수가 제네릭 함수를 오버라이드 한 것이 아니라면 void 리턴하는 함수로 컴파일이 되며

자바에서 오버라이드 한 경우도 void로 반환 타입을 해야 된다.

 

차이점은 Unit은  모든 기능을 갖은 일반적인 타입이며 void와 달리 Unit을 타입 인자(<T>) 로 사용할 수 있다.

Unit 타입의 함수는  Unit을 묵시적으로 반환한다.

 

interface Processor<T>{
    fun process(): T
}

class NoResultProcessor: Processor<Unit>{
    
    override fun process() {  // Unit 타입을 반환하지만 명시 할필요가없다.
        
        // return unit을 하지 않아도 컴파일러가 해준다.
    }

}

 

함수형 프로그래밍에서 Unit은 단 하나의 인스턴스만 갖는 타입을 의미해왔고 바로 그 유일한 인스턴스의 유무가 자바의 void와 코틀린의 Unit을 구분하는 가장 큰 차이다.

 

Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다.

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

 

많은 테스트 라이브러리 들은 fail이라는 함수를 제공하는데.

 

fail 이라는 함수는 특별한 메시지가 들어있는 예외를 던져 현재 테스트를 실패시킨다.

무한루프를 도는 함수 역시 결코 값을 반환하지 않는다.

 

이러한 함수들을 호출하는 코드를 분석함에 있어 "함수가 정상적 종료를 하지 못함"을 사실을 알면 유용하다.

 

코틀린은 그래서 Nothing이라는 특별한 반환 타입이 이 역할을 한다.

우항의 값이 null임을 정상적 종료가 안 되는 함수를 호출함으로써 알 수 있다.

fun fail(message: String): Nothing{
    throw IllegalStateException(message)
}

fun main() {
    val address = company.address ?: fail("Error occurred")
}

Nothing 타입은 어떤 값도 포함하지 않는다.

함수의 반환 타입, 반환 타입으로 쓰일 타입 파라미터 <T> 로만 사용될 수 있다.