Kotlin ‐ 기본 타입, 기본 타입의 리터럴과 연산[Effective Kotlin Item 4] - dnwls16071/Backend_Summary GitHub Wiki

기본 타입, 기본 타입의 리터럴과 연산

개요

  • 코틀린에서는 기본적으로 모든 값이 객체로 간주되기 때문에(=원시 타입이 없다) 모든 값은 메서드를 제공하며, 값의 타입을 제네릭 타입 인수로 사용할 수 있다.
  • 자바와 달리 코틀린은 원시 타입(primitive type)과 래퍼 타입(wrapper type)을 구분하지 않는다. 하지만 내부적으로는 성능 최적화를 위해 가능한 경우 JVM의 원시 타입으로 컴파일된다.

수 (Numbers)

  • 코틀린에는 수를 표현할 때 사용하는 타입이 다양하게 준비되어 있다.
  • 크게 소수점이 없는 정수 타입과 소수점이 있는 부동소수점 수 타입으로 구분할 수 있다.

정수 타입

  • 정수를 표현할 때는 Int, Long, Byte, Short를 사용한다.
타입 크기 (비트) 최솟값 최댓값
Byte 8 -128 127
Short 16 -32,768 32,767
Int 32 -2,147,483,648 2,147,483,647
Long 64 -9,223,372,036,854,775,808 9,223,372,036,854,775,807
// 정수 리터럴
val decimal = 100           // Int (기본)
val long = 100L             // Long (L 접미사)
val byte: Byte = 127        // Byte (명시적 타입 지정)
val short: Short = 1000     // Short (명시적 타입 지정)

// 코드에서 소수점이 없는 수는 기본적으로 Int로 간주
val defaultInt = 42         // 타입 추론: Int
val explicitLong = 42L      // 타입 추론: Long

부동소수점 타입

  • 부동소수점 수를 표현할 때는 Float, Double을 사용한다.
타입 크기 (비트) 유효 자릿수
Float 32 6-7 자리
Double 64 15-16 자리
// 부동소수점 리터럴
val double = 3.14                    // Double (기본)
val explicitDouble = 3.14159265358979 // Double
val float = 3.14f                    // Float (f 또는 F 접미사 필요)
val floatUpper = 3.14F               // Float

// 코드에서 소수점이 있는 수는 기본적으로 Double로 간주
val defaultDouble = 2.71828          // 타입 추론: Double
val scientificNotation = 1.23e10     // 과학적 표기법: 1.23 × 10^10
val negativeExponent = 1.5e-5        // 0.000015

수 리터럴 표기법

수에서 밑줄 사용

  • 수 리터럴에서 자릿수 사이에 밑줄(_)을 넣을 수 있다.
  • 밑줄은 무시되지만 큰 수의 가독성을 높이고자 할 때 사용된다.
// 큰 수의 가독성 향상
val oneMillion = 1_000_000
val billion = 1_000_000_000L

// 신용카드 번호처럼 그룹화
val creditCardNumber = 1234_5678_9012_3456L

// 이진수에서도 사용 가능
val bytes = 0b11010010_01101001_10010100_10010010

// 16진수에서도 사용 가능
val hexBytes = 0xFF_EC_DE_5E

// 부동소수점에서도 사용 가능
val pi = 3.141_592_653_589_793

다양한 진법 표기

  • 코틀린은 10진수 외에도 2진수, 16진수 리터럴을 지원한다.
// 10진수 (Decimal)
val decimal = 255

// 16진수 (Hexadecimal) - 0x 또는 0X 접두사
val hexadecimal = 0xFF
val hexUpper = 0XFF
println(hexadecimal)  // 255

// 2진수 (Binary) - 0b 또는 0B 접두사
val binary = 0b11111111
val binaryUpper = 0B11111111
println(binary)  // 255

// 8진수는 지원하지 않음
// val octal = 0377  // 컴파일 에러! 

// 진법 표기 조합 예제
val flags = 0b0000_1111  // 15
val colorRed = 0xFF_00_00  // 16711680
val permissions = 0b111_101_101  // 493

Number와 변환 함수

  • 숫자를 나타내는 모든 기본 타입은 Number 타입의 서브 타입이다.
  • 기본적인 숫자라면 to{NewType} 함수를 사용해 다른 기본 타입으로 변환할 수 있다.
  • 이런 함수를 **변환 함수(conversion function)**라고 한다.
val intValue: Int = 100

// 다양한 타입으로 명시적 변환
val byteValue: Byte = intValue.toByte()      // 100
val shortValue: Short = intValue.toShort()   // 100
val longValue: Long = intValue.toLong()      // 100
val floatValue: Float = intValue. toFloat()   // 100. 0
val doubleValue: Double = intValue.toDouble()  // 100.0

// 자동 변환은 지원하지 않음 - 명시적 변환 필요
val int: Int = 42
// val long: Long = int  // 컴파일 에러!
val long: Long = int.toLong()  // OK

// 오버플로우 예제
val largeInt: Int = 130
val smallByte: Byte = largeInt.toByte()
println(smallByte)  // -126 (오버플로우 발생)

// 부동소수점을 정수로 변환 (소수점 버림)
val pi = 3.14159
val piInt = pi.toInt()
println(piInt)  // 3

변환 함수 목록

  • 모든 수 타입은 다음 변환 함수를 제공한다:
val number = 100

// 정수 타입으로 변환
number.toByte()    // Byte
number.toShort()   // Short
number.toInt()     // Int
number.toLong()    // Long

// 부동소수점 타입으로 변환
number.toFloat()   // Float
number.toDouble()  // Double

// 문자로 변환 (ASCII 코드)
number.toChar()    // 'd' (ASCII 100)

수의 산술 연산

  • 코틀린은 수 타입에 대해 표준 산술 연산자를 제공한다.

기본 산술 연산자

val a = 10
val b = 3

// 덧셈
println(a + b)   // 13

// 뺄셈
println(a - b)   // 7

// 곱셈
println(a * b)   // 30

// 나눗셈 (정수 나눗셈 - 소수점 버림)
println(a / b)   // 3

// 나머지 (모듈로 연산)
println(a % b)   // 1

// 단항 연산자
val positive = +a  // 10
val negative = -a  // -10

정수 나눗셈 vs 부동소수점 나눗셈

// 정수 나눗셈 - 결과가 정수
val intResult = 10 / 3
println(intResult)  // 3

// 부동소수점 나눗셈 - 결과가 소수
val doubleResult = 10.0 / 3.0
println(doubleResult)  // 3.3333333333333335

// 정수를 부동소수점으로 변환하여 나눗셈
val mixedResult = 10 / 3.0
println(mixedResult)  // 3.3333333333333335

val convertedResult = 10. toDouble() / 3
println(convertedResult)  // 3.3333333333333335

복합 대입 연산자

var count = 10

count += 5  // count = count + 5  -> 15
count -= 3  // count = count - 3  -> 12
count *= 2  // count = count * 2  -> 24
count /= 4  // count = count / 4  -> 6
count %= 5  // count = count % 5  -> 1

println(count)  // 1

증가 및 감소 연산자

var x = 10

// 후위 증가/감소
println(x++)  // 10 출력 후 증가
println(x)    // 11

println(x--)  // 11 출력 후 감소
println(x)    // 10

// 전위 증가/감소
println(++x)  // 증가 후 11 출력
println(x)    // 11

println(--x)  // 감소 후 10 출력
println(x)    // 10

산술 연산 우선순위

val result1 = 2 + 3 * 4      // 14 (곱셈 먼저)
val result2 = (2 + 3) * 4    // 20 (괄호 먼저)
val result3 = 10 - 5 - 2     // 3 (왼쪽에서 오른쪽)
val result4 = 2 + 3 * 4 / 2  // 8 (*, / 먼저, 왼쪽에서 오른쪽)

// 복잡한 연산
val complex = (10 + 5) * 3 - 8 / 2 + 1
println(complex)  // 42

비트 연산

  • 코틀린은 정수 타입(Int, Long)에 대해 비트 단위 연산을 지원한다.

비트 논리 연산

val x = 0b1010  // 10 (2진수)
val y = 0b1100  // 12 (2진수)

// AND - 두 비트가 모두 1일 때 1
val and = x and y
println(and. toString(2))  // 1000 (8)

// OR - 두 비트 중 하나라도 1일 때 1
val or = x or y
println(or. toString(2))   // 1110 (14)

// XOR - 두 비트가 다를 때 1
val xor = x xor y
println(xor.toString(2))  // 0110 (6)

// NOT - 비트 반전 (inv 함수 사용)
val inv = x.inv()
println(inv)  // -11 (2의 보수)

비트 시프트 연산

val num = 0b00001010  // 10

// 왼쪽 시프트 (<<) - 2를 곱하는 효과
val leftShift1 = num shl 1
println(leftShift1. toString(2))  // 10100 (20)

val leftShift2 = num shl 2
println(leftShift2.toString(2))  // 101000 (40)

// 오른쪽 시프트 (>>) - 2로 나누는 효과 (부호 유지)
val rightShift1 = num shr 1
println(rightShift1.toString(2))  // 101 (5)

val rightShift2 = num shr 2
println(rightShift2.toString(2))  // 10 (2)

// 부호 없는 오른쪽 시프트 (>>>)
val negative = -16
val signedShift = negative shr 2
val unsignedShift = negative ushr 2
println(signedShift)    // -4 (부호 유지)
println(unsignedShift)  // 1073741820 (부호 무시)

BigDecimal, BigInteger

  • 코틀린의 모든 기본 타입은 표현할 수 있는 크기와 정확도 측면에서 한계가 있기 때문에 특정 상황에서 정확하지 않거나 잘못된 결과를 만들 수 있다.

JVM에서는:

  • 크기에 제한이 없는 정수가 필요할 때는 BigInteger
  • 크기에 제한이 없고 정확해야 하는 부동소수점 수가 필요하면 BigDecimal을 사용해야 한다.

BigInteger 사용

import java.math.BigInteger

// 기본 타입의 한계
val maxLong = Long.MAX_VALUE
println("Long 최댓값: $maxLong")  // 9223372036854775807
// val overflow = maxLong + 1  // 오버플로우 발생! 

// BigInteger로 해결
val bigInt1 = BigInteger("123456789012345678901234567890")
val bigInt2 = BigInteger("987654321098765432109876543210")

// 산술 연산
val sum = bigInt1 + bigInt2
val difference = bigInt2 - bigInt1
val product = bigInt1 * bigInt2
val quotient = bigInt2 / bigInt1

println("합: $sum")
println("차: $difference")
println("곱: $product")
println("몫: $quotient")

// 기본 타입에서 BigInteger로 변환
val fromInt = 1000. toBigInteger()
val fromLong = 1000000L.toBigInteger()
val fromString = "999999999999999999". toBigInteger()

// BigInteger에서 기본 타입으로 변환
val toInt = fromInt.toInt()
val toLong = fromInt.toLong()

BigDecimal 사용

import java.math.BigDecimal
import java.math.RoundingMode

// Double의 정확도 문제
val d1 = 0.1
val d2 = 0. 2
println(d1 + d2)  // 0.30000000000000004 (부정확!)

val d3 = 1.0
val d4 = 3.0
println(d3 / d4)  // 0. 3333333333333333 (반올림됨)

// BigDecimal로 정확한 계산
val bd1 = BigDecimal("0.1")
val bd2 = BigDecimal("0.2")
println(bd1 + bd2)  // 0.3 (정확!)

// 금융 계산 예제
val price = BigDecimal("19.99")
val quantity = BigDecimal("3")
val taxRate = BigDecimal("0. 1")  // 10%

val subtotal = price * quantity  // 59.97
val tax = subtotal * taxRate     // 5.997
val total = subtotal + tax       // 65.967

// 반올림 설정
val roundedTax = tax.setScale(2, RoundingMode.HALF_UP)      // 6.00
val roundedTotal = total. setScale(2, RoundingMode.HALF_UP)  // 65.97

println("소계: $$subtotal")
println("세금: $$roundedTax")
println("합계: $$roundedTotal")

// 비교 연산
val value1 = BigDecimal("10.00")
val value2 = BigDecimal("10.0")
println(value1 == value2)                    // false (스케일이 다름)
println(value1.compareTo(value2) == 0)       // true (값이 같음)

불 (Boolean)

  • true, false 두 가지 값만 가능한 Boolean도 기본 타입으로 제공한다.

불 리터럴

val isTrue: Boolean = true
val isFalse: Boolean = false

// 타입 추론
val autoTrue = true    // Boolean
val autoFalse = false  // Boolean

논리 연산자

val a = true
val b = false

// AND (&&) - 둘 다 true일 때만 true
println(a && b)   // false
println(true && true)   // true

// OR (||) - 하나라도 true면 true
println(a || b)   // true
println(false || false)  // false

// NOT (!) - 반전
println(!a)       // false
println(! b)       // true

// 복합 논리 연산
val result = (a && b) || (! a && !b)
println(result)   // false

비교 연산자

비교 연산자는 Boolean 결과를 반환한다.

val x = 10
val y = 20

// 동등 비교
println(x == y)   // false (같음)
println(x != y)   // true (다름)

// 크기 비교
println(x < y)    // true (작음)
println(x > y)    // false (큼)
println(x <= y)   // true (작거나 같음)
println(x >= y)   // false (크거나 같음)

// 문자열 비교
val str1 = "abc"
val str2 = "abc"
val str3 = "def"

println(str1 == str2)  // true
println(str1 == str3)  // false
println(str1 < str3)   // true (사전순)

단락 평가 (Short-circuit Evaluation)

fun expensive(): Boolean {
    println("expensive() 호출됨")
    return true
}

// AND 연산 - 첫 번째가 false면 두 번째 평가 안 함
val and = false && expensive()
// "expensive() 호출됨" 출력 안 됨
println(and)  // false

// OR 연산 - 첫 번째가 true면 두 번째 평가 안 함
val or = true || expensive()
// "expensive() 호출됨" 출력 안 됨
println(or)  // true

// 두 번째도 평가하는 경우
val and2 = true && expensive()
// "expensive() 호출됨" 출력됨
println(and2)  // true

문자열

  • 삼중 따옴표 문자열에 trimIndent 함수를 사용하면 문자열을 더 깔끔하게 표현할 수 있다.
  • 해당 함수는 들여쓰기용으로 쓰인 공백들을 모든 줄에서 일관되게 잘라준다.

리터럴 상수와 const

val vs const val

// val - 런타임 상수 (실행 시 값 결정)
val runtimeConstant = System.currentTimeMillis()
val calculated = 10 * 20

// const val - 컴파일 타임 상수 (컴파일 시 값 결정)
// 최상위 레벨 또는 object 내에서만 선언 가능
// 기본 타입과 String만 가능
const val COMPILE_TIME_CONSTANT = 100
const val PI = 3.141592653589793
const val APP_NAME = "My Application"

// const val은 표현식 사용 불가
// const val INVALID = 10 * 20  // 컴파일 에러! 

// 사용 예제
const val MAX_RETRY_COUNT = 3
const val DEFAULT_TIMEOUT = 5000L
const val API_VERSION = "v1"

fun connectWithRetry() {
    for (i in 1..MAX_RETRY_COUNT) {
        println("연결 시도 $i/$MAX_RETRY_COUNT")
    }
}

숫자 표현의 한계와 주의사항

정수 오버플로우

// 오버플로우 예제
val maxInt = Int.MAX_VALUE
println("Max Int: $maxInt")  // 2147483647

val overflow = maxInt + 1
println("Overflow: $overflow")  // -2147483648 (오버플로우!)

// 안전한 처리
val safeLong = maxInt.toLong() + 1
println("Safe: $safeLong")  // 2147483648

부동소수점 정확도 문제

// 부동소수점 계산의 부정확성
println(0.1 + 0.2)  // 0. 30000000000000004
println(1.0 / 3.0)  // 0.3333333333333333

// 비교 시 주의
val a = 0.1 + 0.2
val b = 0.3
println(a == b)  // false! 

// 해결 방법 1: 임계값 사용
val epsilon = 0.0000001
println(kotlin.math.abs(a - b) < epsilon)  // true

// 해결 방법 2: BigDecimal 사용
val bd1 = BigDecimal("0.1")
val bd2 = BigDecimal("0.2")
val bd3 = BigDecimal("0.3")
println(bd1 + bd2 == bd3)  // false (스케일 다름)
println((bd1 + bd2).compareTo(bd3) == 0)  // true

NaN과 무한대

// 0으로 나누기
val divByZero = 1. 0 / 0.0
println(divByZero)  // Infinity

val negDivByZero = -1. 0 / 0.0
println(negDivByZero)  // -Infinity

// NaN (Not a Number)
val nan = 0.0 / 0. 0
println(nan)  // NaN

// NaN 확인
println(nan. isNaN())  // true
println(nan == nan)   // false (NaN은 자기 자신과도 같지 않음!)

// 무한대 확인
println(divByZero.isInfinite())  // true
println(divByZero.isFinite())    // false