숫자

숫자 (Numbers)

Kotlin의 숫자 타입은 정수 값과 부동소수점 값을 나타내요.

숫자 타입은 산술 계산, 카운터, 측정, 기타 계산 같은 데서 숫자 데이터를 저장하고 처리하는 데 쓰여요.

출처: Kotlin 공식 문서

본문

숫자 타입 고르기

대부분의 경우 아래 규칙만 참고해도 작업에 맞는 숫자 타입을 정할 수 있어요.

  • 정수는 Int를 사용해요.
  • Int 범위를 벗어나는 정수는 Long을 사용해요.
  • 소수는 Double을 사용해요.
  • 더 낮은 정밀도로 충분하거나 그것이 요구될 때는 Float을 사용해요.
  • API나 데이터 형식이 요구할 때는 ByteShort를 사용해요.

Kotlin은 또한 부호 없는 정수 타입을 Beta 기능으로 제공해요.

정수 타입

Kotlin은 크기와 값 범위가 서로 다른 네 가지 정수 타입을 제공해요.

타입 크기(bits) 최솟값 최댓값
Byte 8 -128 127
Short 16 -32768 32767
Int 32 -2,147,483,648 (-2^31) 2,147,483,647 (2^31 - 1)
Long 64 -9,223,372,036,854,775,808 (-2^63) 9,223,372,036,854,775,807 (2^63 - 1)

정수 값 선언하기

Kotlin은 정수 값에 대해 다음과 같은 리터럴 형태를 지원해요.

  • 십진수: 123
  • 16진수: 0x0F
  • 2진수: 0b00001011

Kotlin은 8진수 리터럴을 지원하지 않아요.

숫자 값을 선언할 때는 타입을 명시적으로 지정해요.

val one: Int = 1

// 가독성을 높이려면 밑줄을 사용
val oneBillion: Long = 1_000_000_000
val hexBytes: Int = 0x7F_EC_DE_5E
val bytes: Int = 0b01010010_01101001_10010100_10010010

val oneByte: Byte = 1
val oneShort: Short = 1

또한 L 접미사를 붙여서 Long 값을 선언할 수도 있어요.

val oneLong = 1L

숫자 타입을 명시적으로 선언하면 컴파일러는 그 값이 해당 타입의 범위에 들어가는지 확인해요.

// 값이 Byte에 들어감
val oneByte: Byte = 1

// 오류: 값이 Byte에 들어가지 않음
val tooBig: Byte = 128

숫자 타입을 지정하지 않으면 값이 Int 범위에 들어올 때 Kotlin이 Int로 추론해요. 그렇지 않으면 Long으로 추론하죠.

val million = 1_000_000 // Int
val threeBillion = 3_000_000_000 // Long

값이 없을 수도 있다면 nullable 타입을 사용해요.

val maybeAbsent: Int? = null

부동소수점 타입

소수 부분이 있는 숫자를 위해 Kotlin은 FloatDouble을 제공해요.

부동소수점 타입은 IEEE 754 표준을 따르는데, Float은 단정밀도(single precision)를, Double은 배정밀도(double precision)를 반영해요.

부동소수점 타입은 크기와 정밀도가 서로 달라요.

타입 크기(bits) 유효 숫자 bits 지수 bits 십진수 자릿수
Float 32 24 8 6-7
Double 64 53 11 15-16

부동소수점 값 선언하기

부동소수점 리터럴을 선언하려면 소수점(.)을 포함하거나 지수 표기법을 사용해요.

val pi = 3.14
val avogadro = 6.02214076e23

기본적으로 Kotlin은 부동소수점 리터럴을 Double로 추론해요. Float을 선언하려면 f 또는 F 접미사를 붙여요.

val pi = 3.14 // Double
val eFloat = 2.7182817f // Float

Float이 저장할 수 있는 것보다 더 큰 정밀도를 가진 Float 리터럴은 Kotlin이 반올림해요.

값이 없을 수도 있다면 nullable 타입을 사용해요.

val maybeAbsent: Double? = null

산술 연산

Kotlin은 숫자에 대해 표준 산술 연산을 지원해요: +, -, *, /, %.

이 연산자들을 써서 일반적인 계산을 수행할 수 있어요.

fun main() {
//sampleStart
    println(1 + 2) // 3
    println(2_500_000_000L - 1L) // 2499999999
    println(3.14 * 2.71) // 8.5094
    println(10.0 / 3) // 3.3333333333333335
//sampleEnd
}

결과 타입은 피연산자들의 타입에 따라 달라져요. 자세한 내용은 혼합 숫자 표현식을 참고하세요.

사용자 정의 숫자 클래스에서 이 연산자들을 오버로드할 수도 있어요. 자세한 내용은 연산자 오버로딩을 보세요.

정수 나눗셈

정수 값끼리의 나눗셈은 항상 정수 결과를 반환해요. 컴파일러가 소수 부분을 버리죠.

fun main() {
//sampleStart
    val intValue = 5 / 2
    println(intValue) // 2
    
    val longValue = 5L / 2
    println(longValue) // 2
//sampleEnd
}

부동소수점 결과를 얻으려면 적어도 하나의 피연산자를 Float이나 Double로 만들어요.

fun main() {
//sampleStart
    val a = 5 / 2.0
    println(a) // 2.5
    
    val b = 5 / 2.toDouble()
    println(b) // 2.5
//sampleEnd
}

타입 변환

숫자 타입들은 서로의 하위 타입이 아니에요. Kotlin은 조용한 데이터 손실과 예상치 못한 동작을 피하기 위해 명시적 변환을 요구해요.

예를 들어 Double을 기대하는 함수는 변환 없이 IntFloat 값을 받을 수 없어요.

fun main() {
//sampleStart
    fun printDouble(x: Double) { 
        print(x) 
    }

    val x = 1.0
    val xInt = 1
    val xFloat = 1.0f
    val one: Double = 1 // 오류: 초기화 타입 불일치

    printDouble(x) // OK
    printDouble(xInt) // 오류: 인자 타입 불일치
    printDouble(xFloat) // 오류: 인자 타입 불일치
//sampleEnd
}

모든 숫자 타입은 다른 숫자 타입으로의 변환을 지원해요. 숫자를 다른 타입으로 변환하려면 명시적 변환 함수를 사용해요.

  • toByte()
  • toShort()
  • toInt()
  • toLong()
  • toFloat()
  • toDouble()

예를 들어 다음 코드는 Int 값을 Double로 변환해요.

fun main() {
//sampleStart
    val intValue: Int = 1
    val doubleValue = intValue.toDouble()
    
    println(doubleValue) // 1.0
//sampleEnd
}

부동소수점 값을 정수 타입으로 변환하면 컴파일러가 소수 부분을 버려요.

fun main() {
//sampleStart
    val d: Double = 1.5
    val l: Long = d.toLong()
    
    println(l) // 1
//sampleEnd    
}

혼합 숫자 표현식

Kotlin은 할당이나 함수 인자에 대해 암시적 변환을 지원하지 않아요. 하지만 산술 표현식에서 서로 다른 숫자 타입을 결합할 수는 있어요. 이런 경우 Kotlin이 피연산자 타입을 바탕으로 결과 타입을 정하고, 산술 연산자가 변환을 자동으로 처리해요.

val intNumber: Int = 1
val longNumber: Long = 1000
val result = intNumber + longNumber // 1001, Long

결과를 더 작은 타입에 할당하려고 하면 컴파일러가 오류를 보고해요.

val intNumber: Int = 1
val longNumber: Long = 1000
val result: Int = intNumber + longNumber 
// 오류: 초기화 타입 불일치

정수 리터럴 타입

타입 추론 중에 Kotlin은 접미사가 없는 정수 리터럴을 특별한 정수 리터럴 타입 (ILT)으로 취급하는데, 주변 문맥이 특정 타입을 결정할 때까지 그래요.

//sampleStart
fun List<Any>.log() {
    println(joinToString(" | ") { it::class.simpleName ?: "Unknown" })
}

fun main() {
    listOf(1, 2).log()
    // Int | Int
    
    listOf(1L, 2L).log()
    // Long | Long
    
    // 컴파일러가 1을 ILT로 해석하고 Long으로 결정함
    listOf(1, 2L).log()
    // Long | Long
    
    // .toInt()가 리터럴을 Int로 변환함
    listOf(1.toInt(), 2L).log()
    // Int | Long
}
//sampleEnd

이것은 IntLong 값이 런타임에서 같은 문자열 표현을 갖기 때문에 특히 놓치기 쉬워요. 이를 피하려면 기대 타입을 명시하거나 값을 명시적으로 변환해요.

//sampleStart
fun List<Any>.log() {
    println(joinToString(" | ") { it::class.simpleName ?: "Unknown" })
}

fun main() {
    val longValues: List<Long> = listOf(1, 2L)
    longValues.log()
    // Long | Long

    val numberValues: List<Number> = listOf(1.toInt(), 2L)
    numberValues.log()
    // Int | Long
}
//sampleEnd

명시적 타입을 사용해 의도하지 않은 타입 추론을 잡을 수도 있어요.

fun main() {
//sampleStart
    val intValues: List<Int> = listOf(1, 2L)
    // 오류: 초기화 타입 불일치
//sampleEnd
}

정수 리터럴 타입에 대해 더 알아보기.

데이터 오버플로

숫자 타입은 정의된 범위 안의 값만 표현할 수 있어요. 연산 결과가 그 범위를 벗어나면 오버플로가 발생해요. 값을 더 작은 숫자 타입으로 변환하면 변환된 값이 원래 숫자 값을 보존하지 못할 수도 있어요.

이 동작은 컴파일러가 받아들인다고 해도 코드 결과에 영향을 줄 수 있어요.

연산에서의 오버플로

각 정수 타입은 정의된 범위 안의 값만 저장할 수 있어요. 산술 연산의 결과가 그 범위를 초과하면 데이터 오버플로가 발생해요.

fun main(){
//sampleStart
    val intNumber: Int = 2147483647
    // 최대 Int 값은 2147483647
    println(intNumber + 1) // -2147483648
//sampleEnd    
}

여기서 결과가 감싸지는 이유는 값이 더 이상 Int에 들어가지 않기 때문이에요. 정수 오버플로가 발생해도 컴파일러가 자동으로 오류를 만들지는 않아요.

부정에서의 오버플로

오버플로는 부정(negation) 중에도 발생할 수 있어요. 예를 들어 Int.MIN_VALUE의 양수 대응 값을 Int로는 표현할 수 없어요.

fun main(){
//sampleStart
    val min = Int.MIN_VALUE
    println(-min) // -2147483648
//sampleEnd    
}

축소 변환

값을 더 작은 정수 타입으로 변환하면 결과가 원래 숫자 값을 보존하지 못할 수 있어요.

fun main() {
//sampleStart
    val large: Int = 130
    val narrowed: Byte = large.toByte()

    println(narrowed) // -126
//sampleEnd
}

한편 부동소수점 타입은 IEEE 754 표준을 따르기 때문에, 아주 큰 결과는 Infinity가 될 수 있어요.

fun main() {
//sampleStart
    println(Double.MAX_VALUE * 2) // Infinity
//sampleEnd    
}

비트 연산

Kotlin은 IntLong에 대해 비트 연산을 제공해요. 이러한 연산은 일련의 중위 함수inv()로 표현돼요.

fun main() {
//sampleStart
    val x = 1
    
    println(x shl 2) // 4
    println(x and 0x000FF000) // 0
//sampleEnd
}

비트 연산에는 다음이 포함돼요.

  • shl() – 부호 있는 왼쪽 시프트
  • shr() – 부호 있는 오른쪽 시프트
  • ushr() – 부호 없는 오른쪽 시프트
  • and() – 비트 AND
  • or() – 비트 OR
  • xor() – 비트 XOR
  • inv() – 비트 반전

부동소수점 숫자 비교

Kotlin에서 부동소수점 비교는 피연산자의 정적 타입에 따라 달라져요.

피연산자가 정적으로 Float이나 Double로 알려져 있으면, 숫자와 그들이 이루는 범위에 대한 연산은 부동소수점 산술에 대한 IEEE 754 표준을 따르죠.

하지만 일반적인 사용 사례(예: Any, Comparable<...>, Collection<T>)에서는 부동소수점 숫자로 정적으로 타입이 정해지지 않은 피연산자에 대해 동작이 달라져요. 이런 경우 Kotlin은 FloatDoubleequals()compareTo() 구현을 사용해요.

그 결과:

  • NaN은 자기 자신과 같은 것으로 간주돼요.
  • NaNPOSITIVE_INFINITY를 포함해 다른 어떤 요소보다 큰 것으로 간주돼요.
  • -0.00.0보다 작은 것으로 간주돼요.

다음 예시는 부동소수점 숫자로 정적으로 타입이 정해진 피연산자와 제네릭 타입을 통해 사용된 피연산자의 차이를 보여줘요.

//sampleStart  
fun generalizedEquals(a: Any, b: Any): Boolean {
    return a == b
}

fun main() {
    // 부동소수점 숫자로 정적으로 타입이 정해진 피연산자
    println(Double.NaN == Double.NaN) // false
    println(0.0 == -0.0) // true

    // 부동소수점이 아닌 정적 타입을 통해 사용된 피연산자
    println(generalizedEquals(Double.NaN, Double.NaN)) // true
    println(generalizedEquals(0.0, -0.0)) // false
}
//sampleEnd

JVM에서 숫자 박싱과 캐싱

JVM에서는 non-nullable 숫자 값이 대개 int, long, double 같은 프리미티브 타입으로 저장돼요. 하지만 제네릭 타입이나 Int? 같은 nullable 숫자 타입을 사용하면 값이 박싱되어 객체로 표현돼요.

JVM은 작은 숫자에 대해 메모리 최적화 기법을 적용해 박싱된 표현을 캐싱해요. 그 결과 같은 값을 가진 박싱된 숫자는 참조적으로 동일할 수 있어요.

예를 들어 JVM은 -128부터 127까지의 박싱된 Integer 값을 캐시해요. 따라서 다음 코드는 true를 반환해요.

fun main() {
//sampleStart
    val score: Int = 100
    val savedScore: Int? = score
    val displayedScore: Int? = score
    
    println(savedScore === displayedScore) // true
//sampleEnd
}

캐시 범위 밖의 값에 대해서는 박싱된 값이 서로 다른 객체예요. 그 경우 값이 구조적으로 동일하더라도 참조적으로 동일하지 않아요. 이런 이유 때문에 숫자 값을 비교할 때는 ==를 사용해요.

fun main() {
//sampleStart
    val score: Int = 10000
    val savedScore: Int? = score
    val displayedScore: Int? = score

    println(savedScore === displayedScore) // false
    println(savedScore == displayedScore) // true
//sampleEnd
}

더 알아보기 (Learn more)