primitive type

Yerin·2024년 9월 23일
post-thumbnail

kotlin in action 6-2. 코틀린의 원시타입 을 읽고 정리한 글입니다.


들어가기 전. Java에서의 원시 타입과 참조 타입

코틀린에서는 primitive type과 wrapper type을 구분하지 않는다.

자바를 모르는 사람들을 위해 잠깐 설명한다면..
자바에는 type을 primitive type과 reference type으로 나눌 수 있다.
이걸 깊게 이해하려면 JVM에 대해 알아야하는데.. 일단 그건 넘어가고, 간단히 말하자면
primitive type, 원시타입은 stack에 바로 저장된다. 각 타입은 메모리에서 고정된 크기를 가지고 있다.
reference type, 참조타입은 실제 값 자체가 아닌 객체의 참조 즉, 주소를 heap에 저장한다.

여기서 이제 Wrapper Class가 등장한다.
각 pritive type들은 wrapper class로 객체화 시킬 수 있다.

Primitive Typebyteshortintlongfloatdoublebooleanchar
Wrapper ClassByteShortIntegerLongFloatDoubleBooleanCharacter

이렇게 각 Wrapper class로 객체화 시키는 과정을 boxing이라고 한다.

이렇게 보면 왜 굳이 객체와 시키나 싶지만.. 기본적으로 자바는 객체지향언어이기 때문에 유용하게 쓰인다. Wrapper Class를 사용하는 이유는 primitive 타입이 제공하지 않는 기능과 유연성 때문이라고 생각하면 좋을 듯하다. (컬렉션, 제네릭, null 등… 객체를 필요로 하는 곳이 많다)

그러면 이렇게 Wrapper Class로 박싱하면, 이 객체의 주소값을 참조하는 것이 참조 타입이라고 할 수 있다.


원시 타입 : Int, Boolean 등

말이 좀 길어졌으나.. 본격적으로 다시 코인액을 읽어보겠다. (이제 시작)
아무튼간 코틀린에서는 primitive type과 wrapper type을 구분하지 않는다.
자바에서도 컬렉션에 원시 타입 값을 담을 순 없고, 이를 래퍼 클래스에 박싱하여 사용해야한다.
예를 들어 Collection가 아니라, Collection를 사용해야한다.

허나 코틀린에서는 이 둘을 구분할 필요가 없다. 왜냐? 애초에 없으니까!

그럼 원시타입과 참조 타입이 같다면, 코틀린은 이들을 항상 객체로 표현하는 것일까? → NO
코틀린에서는 실행 시점에 가능한 한 가장 효율적인 방법으로 표현된다.
예를 들어 대부분의 경우 코틀린의 Int 타입은 자바의 int 타입으로 컴파일 된다.
(코틀린에서는 기본적으로 null 불가이기 때문에 자바의 원시타입으로 바로 변환 가능하다 👍)

나머지 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우로, 컬렉션의 타입으로 Int를 사용하면 Int의 래퍼 타입에 해당하는 java.lang.Integer 객체가 들어가게 된다.


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

위에서 잠깐 말했다시피, null은 자바의 참조 타입에만 대입할 수 있기 때문에 null이 될 수 있는 타입은 자바 원시 타입으로 표현할 수 없다. 헉 ~ 그럼 Int?는 어케 컴파일 되는건데!!!!!
→ 코틀린에서 null이 될 수 있는 원시 타입을 사용하는 경우 자바의 래퍼 타입으로 컴파일 된다. (위에서 언급한 컬렉션과 같다.)

이렇게 컴파일 하는 이유는 JVM에서 제네릭을 구현하는 방법 때문이다. JVM은 타입 인자로 원시타입을 허용하지 않기 때문에 자바나 코틀린 모두에서 제네릭 클래스는 항상 박스 타입을 사용해야한다고 하는 것이다.

+) 추가로 원시 타입으로 이루어진 대규모 컬렉션을 사용하고 싶다면 third party libary(ex. trove)를 사용하거나 배열을 이용해야한다.


숫자 변환

자바와 코틀린의 가장 큰 차이 점은 중 하나는 숫자를 변환하는 방식이라고 한다.

코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다. 다음과 같이 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우에도 자동 변환이 불가능하다.

val i = 1
val l: Long = i // error
val l: Long = i.toLong()

대신 위와 같이 toLong() 같은 변환함수를 호출해야한다.
toByte(), toShort(), toChar() 등 양방향 변환 함수가 모두 제공된다.

코틀린은 개발자의 혼란을 피하기 위해 타입 변환을 명시하기로 했다.
두 박스 타입 간의 equals 메서드는 그 안의 값이 아니라 박스 타입 객체를 비교한다.
자바에서 new Integer(42).equals(new Long(42)) false 인 것 과 같이, 코틀린에서도 타입이 다르면 값이 같아도 false를 반환한다.
숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요가 없다. → 123L, 0.12, 123.4f …
또한, 산술 연산자는 적당한 타입의 값을 받아들일 수 있게 이미 오버로드 되어있다.


Any, Any?: 최상위 타입

자바에서 Object가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any 타입이 모든 null이 될 수 없는 타입의 조상 타입이다. (물론 참조 타입만 Object를 정점으로 한다. 원시타입은 객체가 아니잔슴 ~)

코틀린에서는 Any가 Int 등의 원시 타입을 포함한 모든 타입의 조상 타입이다.
내부적으로 Any 타입은 java.lang.Object에 대응한다. 즉, 코틀린 함수가 Any를 사용하면 자바 바이트 코드의 Object로 컴파일된다.

모든 코틀린 클래스에는 toString, equals, hashCode라는 세 메서드가 들어있다. 이 세 메서드는 Any에 정의된 메서드를 상속한 것이다.


Unit 타입 : 코틀린의 void

코틀린의 Unit은 자바의 void와 동일한 기능을 한다. return 값이 없는 함수의 반환 타입으로 Unit을 사용할 수 있다.
코틀린 함수의 반환 타입이 Unit 이고, 그 함수가 제네릭 함수를 오버라이드 하지 않는다면, 내부적으로 자바 void 함수로 컴파일 된다.

그럼 Unit과 void는 동일한 것인가?
Unit은 모든 기능을 갖는 일반적인 타입으로, void와 달리 Unit을 타입 인자로 쓸 수 있다.
아래 예싱하 같이 제네릭 파라미터를 반환하는 함수를 오버라이드하면서 반환 타입으로 Unit을 쓸 때 유용하다

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

class NoResultProcessor: Processor<Unit> {
    override fun process() {
        // something
    }
}

타입 인자로 ‘값 없음'을 표현하는 문제를 자바에서 어떻게 코틀린과 같이 깔끔하게 해결할 수 있을지 생각해보면 별도의 인터페이스(Callable과 Runnable 등과 비슷하게)를 사용해 을 반환하는 경우와 값을 반환하지 않는 경우를 분리하는 방법도 있다.
다른 방법으로는 타입 파라미티로 특별히 java.lang.Void 타입을 사용하는 방법도 있다.
후자를 택한다 해도 여전히 void 타입에 대응할 수 있는 유일한 값인 null을 반환하기 위해 return null을 명시해야 한다.


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

코틀린에서는 반환값이라는 개념 자체가 의미없는 함수들이 존재한다.
테스트 후 fail을 throw 한다거나, 무한 루프를 도는 경우를 생각해볼 수 있다.

그런 함수를 호출하는 코드를 분석하는 경우 함수가 정상적으로 끝나지 않는다는 사실을 알면 매우 유용하지 않겠는가? 그런 경우는 표현하기 위해 Nothing 타입이 존재한다.

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

Nothing 타입은 아무 값도 포함하지 않는다.
따라서 Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸수 있다.
그외의 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하터라도 그 변수에 아무 값도 저장할 수 없으므로 아무 의미도 없다.

val address = company.address ?: fail("No Address")
prinln(adress.city)

위의 예시는 Nothing을 반환하는 함수를 엘비스 연산자의 우항에 사용해서 전제 조건을 검사하는 코드이다. company.address가 null인경우 예외가 발생한다는 사실을 알고 address값이 null이 아님을 추론할 수 있다.
이와 같이 컴파일러는 Nothing이 반환타입인 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 분석할 때 사용한다.

profile
𝙸 𝚐𝚘𝚝𝚝𝚊 𝚕𝚒𝚟𝚎 𝚖𝚢 𝚕𝚒𝚏𝚎 𝙽𝙾𝚆, 𝙽𝙾𝚃 𝚕𝚊𝚝𝚎𝚛 !

0개의 댓글