Kotlin

 

코틀린은 디자인 바이 컨트랙트 (Design By Contract) 접근방식으로, 개발자는 함수나 메소드가 null을 받거나 리턴할 수 있는지 명확하게 표현할 수 있으며, 그 시점도 알 수 있다고 한다.

만약에 참조가 null이 될 수 있다면 참조하고 있는 객체의 속성이나 메소드를 사용할 땐 언제나 null 체크를 하도록 강제한다.

 

게다가 이런 체크는 모두 컴파일 시간에만 이루어지고 바이트코드에는 아무 것도 추가되지 않는다.

 

코틀린의 모든 클래스는 Java의 Object 클래스처럼 Any 클래스에서 상속을 받는다. Any 클래스는 코틀린의 모든 클래스에서 사용 가능한 유용한 메소드를 포함하고 있다.

 

코틀린의 고급 개념 중의 하나는 제네릭 파라미터 타입의 공변성 (Convariance)반공변성 (Contravariance) 이다.

(공변성? 반공변성? 이란 개념이 조금 난해한 것 같다.)

 

Any 와 Nothing 클래스를 통해 null 가능 참조와 연관된 연산자, 스마트 캐스트의 장점을 배울 수 있고, 타입 캐스팅을 안전하게 하는 방법과 타입 안정적으로 확장 가능한 제네릭 함수를 만드는 것이 가능하다.

 

1. Any 와 Nothing 클래스

1.1. Any 클래스

  • 코틀린의 모든 클래스는 Any를 상속받는다.
  • Java의 Object 클래스와 유사하지만 더 많은 메소드들을 제공한다.
  • Any 클래스는 최대한의 유연성을 제공하기에 아주 제한적으로 사용해야한다.
  • equals(), hashCode(), toString(), to(), let(), run(), apply(), also() 등등

 

1.2 Nothing 클래스

  • 코틀린은 표현식이 리턴을 하지 않을때 void 대신에 Unit을 사용한다.
  • 그러나 진짜로 아무것도 리턴하지 않는 경우에는 Nothing 클래스를 리턴한다.

 

fun computerSqrt(n: Double): Double {
    if (n >= 0) {
        return Math.sqrt(n)
    } else {
        throw RuntimeException("No negative please")
    }
}

 

여기서 if문의 리턴 타입은 Double 이고, 예외는 Nothing 타입을 대표한다.

 

2. Null 가능 참조

2.1 null은 에러를 유발한다.

null을 null 불가 참조에 할당하거나 참조타입이 null 불가인 곳에 null 을 리턴하려고 하면 컴파일 오류가 난다.

 

fun nickName(name: String): String {
    if (name == "William") {
        return "Bill"
    }

    return null //ERROR
}

println("Nickname for William is ${nickName("William")}")
println("Nickname for Venkat is ${nickName("Venkat")}")
println("Nickname for null is ${nickName(null)}")

 

해당 코드를 실행 시, 아래와 같은 결과가 나온다.

 

error: null can not be a value of a non-null type String (nonull.kts:6:12)
nonull.kts:6:12: error: null can not be a value of a non-null type String
    return null //ERROR
           ^

 

코틀린은 리턴타입이 String일 경우 null을 리턴하지 못하게 한다.

 

2.2 Null 가능 타입 사용하기

null 가능 타입은 타입 이름 뒤에 ? 가 붙는다.

String 이라면 null 가능 타입은 String? 가 되고, Int는 Int?, List<String> 은 List<String>? Class는 Class? 가 된다.

 

fun nickName(name: String?): String? {
    if (name == "William") {
        return "Bill"
    }

    if (name != null) {
        return name.reversed() //ERROR
    }

    return null
}

println("Nickname for William is ${nickName("William")}")
println("Nickname for Venkat is ${nickName("Venkat")}")
println("Nickname for null is ${nickName(null)}")

 

정상적으로 실행이 된다.

 

Nickname for William is Bill
Nickname for Venkat is takneV
Nickname for null is null

 

좀 더 코드를 다듬어 보도록 한다.

 

2.3 세이프 콜 연산자

? 연산자를 이용하면 메소드 호출 또는 객체 속성 접근과 null 체크를 하나로 합칠 수 있다.

? 연산자를 세이프 콜 연산자 (safe-call operator) 라고 한다.

 

if (name != null) {
    return name.reversed() //ERROR
}

 

해당 코드를 한 줄로 표현이 가능하다.

 

return name?.reversed()

 

이름을 뒤집은 문자열을 모두 대문자로 변경할때 이렇게 작성할 수도 있다. (toUpperCase 는 Deprecated 되었다.)

 

return name?.reversed()?.uppercase()

 

하지만 세이프 콜 연산자 보다 더 좋은 엘비스(Elvis) 연산자가 있다.

 

엘비스 연산자 (Elvis Operator)

세이프 콜 연산자는 타깃이 null 일 경우에 null 을 리턴한다. 그런데 null 이 아닌 다른 걸 리턴해주고 싶은 경우에 사용할 수 있다.

 

fun nickName(name: String?): String? {
    if (name == "William") {
        return "Bill"
    }

    return name?.reversed()?.uppercase() ?: "Joker"
}

println("Nickname for William is ${nickName("William")}")
println("Nickname for Venkat is ${nickName("Venkat")}")
println("Nickname for null is ${nickName(null)}")

 

결과는 다음과 같이 나온다. 

 

Nickname for William is Bill
Nickname for Venkat is TAKNEV
Nickname for null is Joker

 

엘비스 연산자는 ?: 으로 사용한다. ('?' 가 헤어스타일, ':' 는 눈이라고 했을 때 엘비스 처럼 보인다고 한다.)

 

확정 연산자 (Not-Null assertion Operator)

확정 연산자는 쓰지 말 것을 권장하고 있다. (C언어의 goto 문법 같은건가..?)

 

return name!!.reversed()?.uppercase() // BAD CODE

 

이때 name 은 null 이 아니라는 것으로 확정을 짓고, 만약 name 이 null 이 되면 NullPointerException 이 발생한다.

NullPointerException 을 발생하지 않게 하는 작업이니 쓸 이유가 없는 이유이기도 하다.

 

when 의 사용

세이프 콜 연산자나 엘비스 연산자 보다 when 을 사용하는 것을 고려해보라고 한다.

 

fun nickName(name: String?): String? {
    if (name == "William") {
        return "Bill"
    }

    return name?.reversed()?.uppercase() ?: "Joker"
}

 

이 코드를 아래와 같이 변경한다.

 

fun nickName(name: String?)= when (name) {
    "William" -> "Bill"
    null -> "Joker"
    else -> name.reversed().uppercase()
}

 

최종적으로 사용해야될 코드는 이렇다. 처음 코드에 비해 매우 깔끔해졌다.

 

3. 타입 체크와 캐스팅

3.1 타입체크

타입 체크하는 것은 필수적이지만, 최소한으로 해야한다. 임의의 타입을 체크하는 행위는 새로운 타입이 추가됐을 때 코드를 부서지기 쉽게 만들고 개방-폐쇄 원칙에도 위배된다고 한다.

실행 시간에 타입 체크를 진행할지 여부는 코드를 짜기 전에 여러 번 생각을 해보는 것이 좋다.

 

3.2 is 사용하기

is 를 사용하여 객체가 참조로 특정 타입을 가리키는지 확인한다.

 

class Animal {
    override operator fun equals(other: Any?) = other is Animal
}

val greet: Any = "hello"
val odie: Any = Animal()
val toto: Any = Animal()

println(odie == greet) //false
println(odie == toto) //true

println(odie::class)
println(toto::class)

 

아래는 결과

 

false
true
class Equals$Animal
class Equals$Animal

 

3.3 스마트 캐스트

코틀린은 참조의 타입이 확인되면 자동 혹은 스마트 캐스팅을 한다.

equals() 메소드 안에서 캐스트 없이 other.age 라고 바로 사용할 수 있다.

 

/*
//Java code
@Override public boolean equals(Object other) {
    if(other instanceof Animal) {
        return age == ((Animal) other).age;
    }

    return false;
}
*/

/* 1차 변경
class Animal(val age: Int) {
    override operator fun equals(other: Any?): Boolean {
        return if(other is Animal) age == other.age else false
    }
}
 */

// 2차 변경
class Animal(val age: Int) {
    override operator fun equals(other: Any?) = other is Animal && age == other.age
}

val odie = Animal(2)
val toto = Animal(2)
val butch = Animal(3)

println(odie == toto) //true
println(odie == butch) //false

 

3.4 when 과 함께 타입 체크와 스마트 캐스트 사용하기

is 연산자로 타입 체크를 하고, dayOfWeek 가 Any 타입의 파라미터지만 is String 조건에서는 String 으로 취급할 수 있다.

 

// activity.kts

fun whatToDo(dayOfWeek: Any) = when (dayOfWeek) {
    "Saturday", "Sunday" -> "Relax"
    in listOf("Monday", "Tuesday", "Wednesday", "Thursday") -> "Work hard"
    in 2..4 -> "Work hard"
    "Friday" -> "Party"
    is String -> "What? you provided a string of length ${dayOfWeek.length}"
    else -> "No clue"
}

println(whatToDo("Sunday")) // Relax
println(whatToDo("Wednesday")) // Work hard
println(whatToDo(3)) // Work hard
println(whatToDo("Friday")) // Party
println(whatToDo("Munday")) // What? you provided a string of length 6
println(whatToDo(8)) // No clue

 

다음에 계속..