Published on

Kotlin IN ACTION | 타입 시스템, 연산자 오버로딩과 기타 관례

Authors
  • avatar
    Name
    이건창
    Twitter

Introduction

코틀린은 가독성을 위해 타입 시스템 방법을 도입했어. null 여부에 따라 어떤 행동을 하는지를 간단하게 선언 할 수 있지.

코틀린에서 제공하는 타입 시스템을 간단하게 설명해볼게.

6장 : 코틀린 타입 시스템

Nullable

코틀린은 null 타입을 신중하게 그리고 다채롭게 관리할 수 있어. 어떻게 사용할 수 있는지 알아보자.

String?

코틀린은 다음처럼 변수를 선언 할 수 있어.

val message: String = "hello"

그런데 자바와 달리 null 입력되면 NullPointerException이 발생하게 되지

// NullPointerException!!!
val message: String = null

null 을 입력하고 싶다면 타입 뒤에 ? 키워드를 입력하면 돼.

val message: String? = null

이렇게 구분하는 이유는 null 가능성에 따라 검증할지 안할지를 사용자가 집적 선택 할 수 있게 돼. 무슨 말이냐면은 null 가능성이 있는 곳 한 곳만 검증하고 다른 곳들은 type safe 하게 활용 할 수 있다는 의미야. 중복으로 검증할 필요가 없어져.

string?.length()

호출한 변수가 제공하는 메서드를 호출 할 때 변수가 null 이면 NPE의 쓴 맛을 봐야했어. 하지만 코틀린은 변수를 선언하기 전에 ? 키워드를 사용하면 NPE가 아닌 null을 반환받게 돼.

var message: String? = null
println(message?.length) // null

string?:"empty"

엘비스 연산자(?:)는 null 값이 들어오면 우항 값을 반환하도록 만들어진 친구야.

var message: String? = null
println(message ?: "hello") // hello

엘비스 연산자는 다음처럼 활용 할 수 있어.

fun Person.countryName() = company?.address?.country ?: "Unknown"

string as? Empty

as?를 활용하면 안전하게 캐스트도 가능하지.

fun getType(s: Any?) = s as? String ?: "no string"

string!!

!! 키워드를 활용하면 null 아님을 단언 할 수도 있어.

val message: String? = null
println(message!!) // NPE!!

!! 키워드를 사용한 이유는 더 나은 방법을 찾아보라는 표현이라고 해. 만약 사용한다면 더 좋은 방법이 있는지 고민해보자.

string?.let

코틀린은 null이 아닌 값을 받을 경우만 동작하도록 도와주는 친구가 있어. 예상한 것처럼 null이 반환되면 아무런 동작을 수행하지 않지.

fun printHello(message: String?){
    message?.let {
        println(message)
     }
}

late initialized

객체를 사용할 때 초기화하고 싶다면 lateinit 키워드를 사용해야 해. lateinit 키워드를 활용하면 null 타입 검사를 하지 않아도 되지.

class Hello {
    private lateinit var message: String

    @Befor fun setUp() {
        message = "hello"
    }

    @Test fun test() {
//        assert(message!! == "hello") -> null 인지 확인하지 않아도 된다.
        assert(message == "hello")
    }
}

lateinit 키워드를 사용할 때 프로퍼티는 var로 선언되어야 해.

코틀린의 원시 타입

원시 타입 : Int, Boolean

자바에는 원시 티입이 스택에 실제 값이 저장되지만 참조 타입은 스택에 참조 주소가 들어가고 있어. 코틀린도 마찬가지야. 원시 타입과 관련된 키워드는 없지만 원시 타입으로 저장되는 참조 타입을 사용하면 원시 타입 처럼 값을 관리하게 돼.

윈시 타입은 다음과 같아.

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

반대로 null 이 될 수 있는 Int? 처럼 ? 키워드가 포함되면 참조 타입으로 관리가 돼.

컬렉션을 관리할 때는 참조 타입으로 관리하게 돼. 그 이유는 JVM에서 타입 인자를 참조 타입만 허용하기 때문이야. 만약 원시 타입으로 이루어진 효율적인 컬렉션을 활용하기 위해서는 서드 파티를 활용해야 해.

숫자 간 변환

코틀린에서는 숫자 변환이 자동으로 이뤄지지 않아.

val i = 0
val l: Long = i.toLong()

하지만 산술 연산자는 적당한 타입의 값을 받을 수 있게 오버로드 되어 있어.

val byte:Byte = 1
val l = byte + 1L
println(l)

함수에 인자로 넘기는 경우도 자동으로 변환해줘.

fun foo(l: Long) = println(l)
foo(42)

Any, Any?

자바에서는 Object가 최상위 클래스라면 코틀린에서는 Any가 최상위 타입이야. 즉, 자바와 코틀린은 내부에서 Any 타입과 Object로 대응하게 돼.

val answer: Any = 42

Unit 타입

Unit은 자바의 Void 같은 역할 같아. 만약 제네릭 함수를 반환해야 한다면 다음처럼 간단하게 구현 할 수 있어.

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

class NoResultProcessor: Processor<Unit>{
    override fun process() { // 반환 타입 지정 X
        TODO("Not yet implemented") // return 값 반환 X
    }
}

자바였다면 Void 반환 함수라고 지정하고 반환 값도 지정해야 하지만 코틀린은 그럴 필요가 없지!

Nothing 타입

성공적인 결과가 아닌 경우 예외를 전파해야 하는데, 예외를 전파하는 경우를 표현하기 위해 Nothing 이라는 타입을 활용할 수 있어.

fun fail(message: String) {
    throw IllegalArgumentException(message)
}

컬렉션과 배열

코틀린 컬렉션

코틀린은 다음처럼 활용하면 돼.

컬렉션 타입읽기 전용 타입변경 가능 타입
ListlistOfmutableListOf, arrayListOf
SetsetOfmutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
MapmapOfmutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

컬렉션은 자바와 유사해. 그래서 어떤 확장 함수를 제공하는지만 파악하면 걱정할 일이 없어.

7장 : 연산자 오버로딩과 기타 관례

산술 연산자 오버로딩

산술 연산자

연산자를 오버로딩 하는 함수 앞에는 operator가 꼭 필요해.

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

println(Point(10, 20) + Point(20, 30)) // result : Point(30, 50)

자바 클래스에도 확장 함수를 활용해서 산술 연산자를 쉽게 활용 가능해.

operator fun Point.plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
}

그런데 두 피 연산자가 다르면 어떻게 될까. 첫 번째 연산자의 결과에 따라 값이 변경 돼.

data class Point(val x: Int, val y: Int)

operator fun Point.times(scale: Double): Point {
        return Point((x * scale).toInt(), (y * scale).toInt())
}

println(Point(10, 20) * 1.5) // result : Point(15, 30)

코틀린은 교환 법칙을 지원하지 않아. 만약 1.5 * POINT 였다면 Double 에 Point 를 곱하는 연산자를 추가 해야 해.

함수 식 정리

산술 연산자를 커스텀하기 위해서 필요한 함수 이름을 정리해봤어.

함수 이름
a * btime
a / bdiv
a % brem
a + bplus
a - bminus

비트 연산은 산술 연산자 처럼 특별한 연산자 함수를 제공하지 않아. 대신 중위 연산자 표기법을 지원하지.

중위 연산자 표기법설명
shl<<
shr>>
ushr>>>
and&
or|
xor^
inv~

그리고 복합 대입 연산자도 지원해줘.

함수 이름
a *= btimeAssign
a /= bdivAssign
a %= bremAssign
a += bplusAssign
a -= bminusAssign

단항 연산자도 지원해줘.

함수 이름
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
--a, a--dec

비교 연산자 오버로딩

equals

동등성 연산자(==)로 비교하려면 equals 메서드를 오버라이딩 하면 돼.

data class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        if (other === this) return true
        if (other !is Point) return false
        return other.x == x && other.y == y
    }
}

compareTo

산술 연산자와 큰 차이는 operator 키워드가 아닌 override 키워드를 사용한다는 점이야. Any 클래스에 operator 키워드로 선언되어 있기 때문이지.

순서 연산자(>, <, >=, <=)로 비교하려면 compareTo 메서드를 오버라이딩 하면 돼.

data class Point(val x: Int, val y: Int) : Comparable<Point> {
    override fun compareTo(other: Point): Int {TODO("Not yet implemented") }
}

컬렉션과 범위에 쓸 수 있는 관례

get, set

코틀린은 컬렉션 원소에 접근 할 때 배열에 접근하는 방법처럼 접근할 수 있어.

val map: Map<String, String> = mapOf("A" to "B", "C" to "D")
println(map["A"]) // B

코틀린은 인덱스 연산자도 관례를 따르게 돼. get은 다음처럼 선언 할 수 있어.

data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else -> throw RuntimeException()
    }
}

println(Point(10, 20)[0]) // 10

set 은 다음처럼 선언 할 수 있어.

data class Point(val x: Int, val y: Int)

operator fun Point.set(index: Int, value: Int) {
    return when(index) {
        0 -> x = value
        1 -> y = value
        else -> throw RuntimeException()
    }
}
val point = Point(10, 20)
point[0] = 42
println(point) // Point(42, 20)

in

컬렉션에 원소가 포함되는지 확인하는 연산을 제공 해.

data class Point(val x: Int, val y: Int)

operator fun Point.contains(i: Int): Boolean {
    return x ==  i || y == i
}

println(3 in Point(3, 2)) // true

rangeTo

범위를 만드려면 rangeTo 연산을 확장하면 돼. rangeToComparable 인터페이스를 구현하면 정의할 필요가 없어.

구조 분해 선언

복합적인 값

구조 분해를 사용하면 복합적인 값을 한 번에 초기화 가능해.

data class Point(val x: Int, val y: Int)
val (x, y) = Point(3, 2)

내부적으로는 componentN이라는 메서드를 사용하지.

data class Point(val x: Int, val y: Int)
val p = Point(3, 2)
val (x, y) = p.component1() to p.component2()

복합적인 값을 한 번에 선언하는 건 최대 다섯 개까지만 되니 주의해야 해.