조용한 담장

[코틀린] Kotlin : 기본 변수의 타입 살펴보기 - 1 본문

kotlin

[코틀린] Kotlin : 기본 변수의 타입 살펴보기 - 1

iosroid 2019. 12. 31. 11:56

코틀린(kotlin)의 기본적인 변수의 타입들을 설명한 페이지 Basic Types 를 보자.

사실 타입을 알아서 잘 처리해주는 언어이기 때문에 모든 변수에 타입을 다 지정해줄 필요는 없을 것 같지만,

다른 개발자의 이해를 돕기 위해서나 오류를 방지하기 위해 타입을 명확히 지정해야 하는 경우도 있겠다 또는 많겠다.

In Kotlin, everything is an object

코틀린 에선 모든 것이 object 이다.

변수도 단순히 데이터 값만을 가진 변수가 아니라 그 데이터 값을 가진 object가 된다.

일반 데이터 변수처럼 쓰다가도 class처럼 쓸 수 있는 것이다.

Some of the types can have a special internal representation - for example, numbers, characters and booleans can be represented as primitive values at runtime - but to the user they look like ordinary classes.

이 설명에 따르면 특정 타입은 내부적으로 primitive value로 처리되는 것 같다.

Numbers

Byte, Short, Int, Long, Float, Double 타입들이 해당된다.

Long과 Float의 경우 suffix 가 있다.

Type

Size (bits)

class definition

suffix

Doc link

Byte

8

class Byte : Number, Comparable<Byte>

link

Short

16

class Short : Number, Comparable<Short>

link

Int

32

class Int : Number, Comparable<Int>

link

Long

64

class Long : Number, Comparable<Long>

L

link

Float

32

class Float : Number, Comparable<Float>

f, F

link

Double

64

class Double : Number, Comparable<Double>

link

val one = 1 // Int
val threeBillion = 3000000000 // Long
val oneLong = 1L // Long
val oneByte: Byte = 1
val pi = 3.14 // Double
val e = 2.7182818284 // Double
val eFloat = 2.7182818284f // Float, actual value is 2.7182817

진법 표현

10진법 : 123, 123L
16진법: 0x0F
2진법: 0b00001011
8진법은 지원 안 함
Double: 123.5, 123.5e10
Float: 123.5f, 123.5F

밑줄 표현

숫자 사이에 밑줄을 써서 보기 좋게 표현할 수 있다. 표현방법일 뿐 값에는 영향이 없다.

val oneMillion = 1_000_000 // 1000000
val creditCardNumber = 1234_5678_9012_3456L // 1234567890123456
val socialSecurityNumber = 999_99_9999L // 999999999
val hexBytes = 0xFF_EC_DE_5E // 0xFFECDE5E
val bytes = 0b11010010_01101001_10010100_10010010 // 0b11010010011010011001010010010010

Representation

이 부분을 이해하기 위해 Referential equality 를 알고 가자.
=== 는 참조하는 object를 비교하고,
== 는 가지고 있는 value를 비교한다.
Java platform에서는 null 을 가질 수 없는 숫자 변수는 JVM primitive type 으로써 처리된다.
Int 의 문서 설명을 보면 JVM 에서는 primitive type int 로 처리되는 것 같다.

Represents a 32-bit signed integer. On the JVM, non-nullable values of this type are represented as values of the primitive type int.

하지만 Int? 같이 null 을 가질 수 있는 nullable number reference 나 generic 에 포함되는 경우는 boxing 되어 처리된다.
boxing 된 변수는 identity를 잃게 되는 특징이 생긴다.

val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!Prints 'false'!!!

하지만 equality는 유지한다.

println(boxedA == anotherBoxedA) // Prints 'true'

JVM 기반에서 실행되다 보니 nullable, non-nullable의 차이가 생기는 것 같다. (Primitive type, Wrapper class, Boxing...)
여기까지가 공식 문서의 내용이고...
위 예제 코드에서 a 값이 1이면?

val a: Int = 1
val boxedA: Int? = a
val anotherBoxedA: Int? = a
println(boxedA === anotherBoxedA) // !!!Prints 'true'!!!

위의 문서 내용만으론 황당한 결과다.
답은 스택오버플로우에 있다.

Why the boxed Integers are not the same?
This is because Integer only caching values in the range [-128, 127]. the the variable a above is out of the cache range, it will create a new Integer instance for each boxing rather than using a cached value.
https://stackoverflow.com/questions/45139381/kotlin-boxed-int-are-not-the-same

이건 Java와 Boxing을 제대로 몰라서 그런 거였다... 이미 아시는 분들은 패스...
Int a 변수는 Int? 로 boxing 되는데 이는 java의 intInteger 로 변환되는 것 과 같다고 한다.

 

val a:Int = 1   // int a = 1;
val b:Int? = a; // Integer b = a;

Integer 형은 [-128, 127] 범위에서 caching 하기 때문에 범위 내에 값이 있으면 새로운 인스턴스를 만들지 않는다고 한다.
더 자세히 파고들기엔 이제 겨우 문법보는 상황에선 무리...
여튼 이런게 있구나.. Kotlin 과 Java.. 그리고 기본 데이터형을 잘 봐둬야 하는 이유는 알겠다..

 

Explicit conversions

작은 데이터 타입의 변수를 더 큰 데이터 타입으로 변환(implicit conversion) 을 할 수 없다.
예를들어 Byte 변수의 값을 Int 에 바로 대입할 수 없다.
각각이 별개지 subtype 의 개념이 아니기 때문에 변수의 타입 변환 대신 값 자체를 변환하여 전달해야 한다.
만약 implicit conversion 이 된다고 가정하면 어떨지 아래 코드를 보자.

// Hypothetical code, does not actually compile:
val a: Int? = 1 // A boxed Int (java.lang.Integer)
val b: Long? = a // implicit conversion yields a boxed Long (java.lang.Long)
print(b == a) // Surprise! This prints "false" as Long's equals() checks whether the other is Long as well

위에 Representation 에서 본 것 처럼 변수 a, b 가 각각 primitive type 이 아닌 boxed Long, boxed Int 이면 a == bfalse 가 되는 문제가 생길것이다.
== 가 equals() 를 통해 동작을 수행하게 되는데 Longequals() 가 비교대상의 타입이 Long 인지를 확인하게 되기 때문이다.
참조 : Why doesn't Kotlin perform automatic type-casting?
identity 를 잃는 특성도 문제가 된다.
이렇게 implicit conversion 으로 인해 생길 수 있는 문제들을 만들지 않기 위해 지원 자체를 하지 않는다고 한다.
대신 explicit conversion 을 하는 방법은 있다.

val b: Byte = 1 // OK, literals are checked statically
// val i: Int = b // ERROR
val i: Int = b.toInt() // OK: explicitly widened

모든 숫자 타입은 아래의 변환 함수가 존재한다.

  • toByte(): Byte
  • toShort(): Short
  • toInt(): Int
  • toLong(): Long
  • toFloat(): Float
  • toDouble(): Double
  • toChar(): Char

코드 연산 구문에 따라 결과 값의 타입을 지정해주는 기능과 수 연산자가 오버로드 되어 변환되기 때문에 implicit conversion 같은 동작이 이루어지기도 한다.

val l = 1L + 3 // Long + Int => Long

Bitwise operation

bitwise 연산을 위한 함수들이 있다. Int, Long 만 적용가능.

  • shl(bits) – signed shift left
  • shr(bits) – signed shift right
  • ushr(bits) – unsigned shift right
  • and(bits) – bitwise and
  • or(bits) – bitwise or
  • xor(bits) – bitwise xor
  • inv() – bitwise inversion
println(1 shl 2 and 0x000FF000) // 0

Floating point numbers comparison

floating point 비교 연산에 대한 특성이 있다.

  • Equality checks: ​a == b and a != b
  • Comparison operators: a < b, a > b, a <= b, a >= b
  • Range instantiation and range checks: a..b, x in a..b, x !in a..b

위와 같이 비교하는 변수 a, b 가 Float, Double 로 타입이 명확한 경우 Floating point 표준(IEEE 754)에 따른 연산 동작을 한다.

 

하지만 불명확한 타입을 사용하는 경우에 대한 예외가 있다.

이는 Float, Double 처럼 명확하고 고정된 타입이 아니지만 부동 소수점 값을 가질 수 있는 형태의 타입을 사용하게 되는 generic 을 사용하는 경우에 대해 total ordering 을 지정하기 위한 것이다.

예를 들어 Any, Comparable<...>, type parameter(generic 에서 사용) 들은 Float, Double 타입이 아니지만 floating point 값을 가질 수 있게 되는데, 이때 비교 연산을 하게되면 Float, Doubleequals()compareTo() 함수를 사용하게 된다.

 

이 경우에 아래와 같이 표준과는 다른 방식으로 total ordering 이 정의되어 비교 연산이 동작되게 된다.

  • NaN is considered equal to itself:
    • NaN 이 NaN 자신과 동등한 것으로써 비교가 가능하다.
  • NaN is considered greater than any other element including POSITIVE_INFINITY:
    • NaN 은 POSITIVE_INFINITY 을 포함해 다른 어떤 것보다 크다.
  • -0.0 is considered less than 0.0:
    • -0.0 이 0.0 보다 작은것으로 간주된다.

참조:

The normal comparison operations, however, treat NaNs as unordered and compare −0 and +0 as equal.
https://en.wikipedia.org/wiki/IEEE_754
A comparison with a NaN always returns an unordered result even when comparing with itself. ... The equality and inequality predicates are non-signaling so x = x returning false can be used to test if x is a quiet NaN.
https://en.wikipedia.org/wiki/NaN#Comparison_with_NaN

Why Kotlin considers negative zero less than positive zero - stackoverflow
Comparing NaN in Kotlin - stackoverflow

fun compare (a:Any, b:Any) {
    println(a == b)
}

fun main() {
    println(Double.NaN == Double.NaN) // false
    compare(Double.NaN, Double.NaN) // true
    println(-0.0 == 0.0) // true
    compare(-0.0, 0.0) // false
}
Comments