Kotlin

[Kotlin] 코틀린의 원시 타입

sh1mj1 2024. 1. 18. 17:46

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

 

 

 

원시(Primitive) 타입

자바는 `int` 등의 원시(Primitive) 타입 과  `String` 등의 참조(Reference) 타입을 구분한다.

원시 타입에는 변수에 값이 그대로 들어간다.

참조 타입에는 메모리 상의 객체의 위치가 변수에 들어간다.

 

자바의 원시 타입은 컬렉션에 넣을 수 없다. 

또한 원시 타입의 멤버, 확장 메서드를 호출할 수 없다. 

 

위와 같은 경우에는 `java.lang.Integer` 와 같은 참조 타입이 필요하다. 

참조 타입은 원시 타입 값을 감싸서 사용하는 것이다. (`int` 를 감싼 `Integer` 를 컬렉션에 넣기 : `Collection<Integer>`)

 

하지만 코틀린에서는 원시 타입과 래퍼 타입의 구분이 없다.

아래처럼 항상 같은 타입을 표기한다.

val i: Int = 1
val list: List<Int> = listOf(1,2,3)

또한 원시 타입의 값에 대해 메서드 호출도 가능하다.

 

그런데 코틀린에서 원시 타입과 래퍼 타입을 구분하지 않는다면, 항상 값이 아닌, 객체로 표현하는 것일까?

만약 그렇다면 아주 비효율적일 것이다. 

실제로는 그렇지 않다.

 

런타임에서 숫자 타입은 가능한 가장 효율적인 방식으로 표현된다.

변수, 프로퍼티, 파라미터, 리턴 타입 등의 대부분의 경우, 코틀린의 `Int` 타입은 자바 `int` 타입으로 컴파일된다.

물론 컬렉션과 같은 Generic class 를 사용할 때는 이렇게 컴파일되지 않고 래퍼 타입으로 컴파일된다.

(`Collection<Int>` 의 `Int` 는 `java.lang.Integer` 로 컴파일됨.)

자바 원시 타입에 해당하는 타입

  • 정수 타입:                 : `Byte', `Short`, `Int`, `Long`
  • 부동소수점 수 타입    : `Float`, `Double`
  • 문자 타입                  : `Char`
  • 불리언 타입              : `Boolean`

위와 같은 코틀린 타입에는 널 참조가 들어갈 수 없기 때문에 쉽게 그에 맞는 자바 원시 타입으로 컴파일할 수 있다.

nullable 원시 타입

자바의 null 은 참조 타입에만 대입 가능하다. 

코틀린의 `Int?`, `Boolean?` 과 같은 nullable 원시 타입은 자바의 원시 타입으로 표현이 불가능하다.

이 때는 자바의 래퍼 타입으로 컴파일된다.

 

nullable 원시 타입을 가진 `Person`

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
    }
}

assertFalse(Person("Sam", 35).isOlderThan(Person("Amy", 42))!!)
assertNull(Person("Sam", 35).isOlderThan(Person("Jane")))

nullable 인 두 `Int?` 타입의 값을 직접 비교할 수 없다.

둘 다 null-check 를 해야 하며, 그 이후에 compiler 가 두 값을 일반적인 값처럼 다루도록 허용해준다.

`Person` 클래스의 `age` 프로퍼티의 값은 `java.lang.Integer` 로 저장된다.

 

제네릭 클래스의 경우는 래퍼 타입, 그 타입에 대한 Boxed 타입을 사용한다.

JVM 은 타입 인자로 원시 타입을 허용하지 않기 때문이다.

만약 원시 타입으로 이루어진 대규모 컬렉션을 효율적으로 저장해야 한다면, 배열을 사용하거나, 서드 파디 라이브러리(ex: 트로프4J) 를 사용해야 한다.

숫자 변환

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

심지어 숫자 타입의 범위가 더 넓어지더라도 그렇다.

이는 개발자의 혼란을 막기 위해 파일 변환을 명시하기로 결정한 것이다.

val i = 1
val l: Long = i // [COMPILE ERROR] Type mismatch: inferred type is Int but Long was expected
val k: Long = i.toLong() // 이렇게 명시적으로 변환

 이런 식으로 `toLong()`, `toChar()` 등으로 쓰면 된다.

이런 식의 변환 함수는 `Boolean` 을 제외한 모든 원시 타입에 제공된다.

좁은 범위의 타입(`Byte`)에 대해 더 넓은 범위(`Long`)로 변환하는 것도 가능하고,

넓은 범위에서 좁은 범위로 변환하여 값을 잘라내는 것도 가능하다.

원시 타입 리터럴
* L 접미사 -> Long 타입: 123L
* 표준 부동소수점 표기법은 Double 타입: 0.12, 2.0, 1.2e10, 1.2e-10
* f, F 접미사 -> Float 타입: 123.4f, 456F, 1e3f
* 0x, 0X 접두사 -> 16진수: 0XCAFE, 0xbcd1
* 0b, 0B 접두사 -> 2진수 : 0b000000101
fun foo(l: Long) = println(l)

var b: Byte = 1 // 상수 값은 적절한 타입으로 해석됨.
foo(42) // 컴파일러는 42 를 Long 값으로 해석
val l = b + 1L // + 는 Byte 와 Long 을 인자로 받기 가능
  • 숫자 리터럴 타입이 알려진 변수에 대입하거나,
  • 함수에 인자로 넘기면 컴파일러가 변환을 자동으로 넣어준다.
  • 산술 연산자는 적당한 타입값을 받아들일 수 있도록 이미 오버로드되어 있다.
  • 코틀린 산술 연산자에서도 자바와 똑같이 숫자 연산 시 overflow(값 넘침)이 발생할 수 있다. 코틀린에서 이것을 따로 검사하지 않는다.
아래처럼 문자도 숫자로 변환할 수 있다.
`println("42".toInt())`
문자열의 내용을 각 원시 타입을 표기하는 문자열로 파싱한다.
만약 파싱에 실패하면 NumberFormatException 이 발생한다.

최상위 타입 Any, Any? 

자바에서는 `Object`(`java.lang.Object`) 가 모든 참조 타입의 최상위 타입이다.

원시 타입은 포함되지 않는다.

만약 원시 타입을 `Object` 타입의 객체로 사용하고 싶다면, 래퍼 타입으로 감싸야 한다. (`int` -> `java.lang.Integer`)

 

코틀린에서는 `Any`모든 not-null 타입의 최상위 타입이다.

이 때는 `Int` 와 같은 원시타입도 포함된다.

val answer: Any = 42 // Any 가 참조 타입이기 때문에 42 가 박싱된다.

 만약 코틀린에서 모든 nullable 값을 대입할 변수를 선언하려면 `Any?` 타입으로 써야 한다.

 

내부에서 `Any` 는 `Object` 와 대응된다.

자바의 `Object` 타입은 코틀린에서 `Any!` 가 된다.

그 반대로 코틀린의 `Any` 타입은 자바의 `Object` 가 된다.

 

`Any` 에는 `toString`, `equals`, `hashCode` 라는  세 메서드가 들어있다.

자바 `Object` 에 들어 있는 `wait` 나 `notify` 등의 다른 메서드는 `Any` 에서 사용할 수 없다.

만약 그런 메서드를 호출하고 싶다면 `Object` 타입으로 캐스팅해야 한다.

Unit 타입: 코틀린의 void

// The type with only one value: the `Unit` object. This type corresponds to the `void` type in Java.
public object Unit {
    override fun toString() = "kotlin.Unit"
}

코틀린의 `Unit` 은 자바와 같은 기능을 한다.

fun foo(): Unit { ... }

fun foo() { ... } // 리턴 타입을 생략해도 됨

 리턴 타입이 `Unit` 인 함수가 제네릭 함수를 오버라이드 하지 않았다면, 내부에서 자바 void 함수로 컴파일된다.

 

`Unit` 은 모든 기능을 갖는 일반적인 타입이다. 

`Unit` 을 타입 인자로 사용할 수 있다.

`Unit` 타입에 속한 클래스는 `Unit` 뿐이며 인스턴스도 유일하게 Unit 이다. 즉, 싱글톤이다.

`Unit` 타입 함수는 `Unit` 타입을 암시적으로 리턴한다.

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

class NoResultProcessor : Processor<Unit> {
    override fun process() { // Unit 을 리턴하지만, 타입을 명시할 필요없다.
        // 처리 코드
        // 여기서 return 을 명시할 필요 없다. 컴파일러가 암시적으로 return Unit 을 넣어준다
    }
}

 

자바에서 타입 인자로 '값 없음' 을 표현할 때 `java.lang.Void` 타입을 사용하는 방법이 있다.

이 때도 `Void` 타입에 대응할 수 있는 유일한 값 null 을 리턴하기 위해 `return null` 을 명시해야 한다.

Unit? 을 리턴하지 말라

`Unit?` 을 리턴한다면 그 이유는 무엇일까?

마치 `Boolean` 이 `true` 또는 `false` 를 갖는 것처럼 `Unit?` 은 `Unit` 또는 `null` 이라는 값을 가질 수 있다.

따라서 `Boolean` 과 `Unit?` 타입은 서로 바꿔서 사용할 수 있다.

 

`Boolean` 을 사용한 샘플 코드

fun keyIsCorrect(key: String): Boolean = /* ... */

if (!keyIsCorrect("key")) return

 아래처럼 바꾸어보자.

 

`Unit?` 을 사용한 샘플 코드

fun verifyKey(key: String): Unit? = /* ... */

verifyKey("key") ?: return

이러한 트릭은 코딩할 때는 멋있어 보이지만, 읽을 때는 그렇지 않다.

`Unit?` 으로 참/거짓 을 표현하는 것은 오해의 소지가 있으며 예측하기 어려운 오류를 만들 수 있다.

 

그러므로 기본적으로 `Unit?` 을 리턴하지 않는 게 좋으며, 

리턴하더라도, 이를 기반으로 연산하지 않는 것이 좋다.

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

/**
 * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
 * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
 */
public class Nothing private constructor()

 `Nothing` 타입은 값을 성공적으로 리턴하는 일이 없는, 리턴값이라는 개념 자체가 없는 함수의 리턴으로 사용된다.

`Nothing` 타입은 아무 값도 포함하지 않아서, 리턴 타입이나 리턴 타입으로 쓰일 타입 파라미터로만 사용 가능하다.

(ex: 테스트 라이브러리들의 `fail` 함수: 특별한 msg 를 가진 예외를 던져서 테스트를 실패시킴, 무한 loop 도는 함수)

 

`Nothing` 타입의 함수 만들기

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

fail("ERROR OCCURRED") // java.lang.IllegalStateException: ERROR OCCURRED

코드를 분석 시, 타입을 보고 정상 리턴을 하지 않는 함수라는 것을 알기 쉬워서 유용하게 쓸 수 있다.

 

`Nothing` 을 리턴하는 함수를 엘비스 연산자의 우항에 사용해서 전제 조건을 검사할 수도 있다.

val address = company.address ?: fail("No address")
pritnln(address.city)