최근 null에 대한 접근 방법은 런타임에 발생하는 NPE를, 컴파일 타임으로 옮기는 것이다.

 

  • 널이 될 수 있음과 없음에 대한 모든 검사는 컴파일 타임에 수행되기 때문에, 실행 시점에는 널이 될 수 있는 타입과 널이 될 수 없는 타입의 객체가 같아진다.
    • 단, ``kt @NotNull`` 애너테이션이나
    • ``java Intrinsics.checkExpressionValueIsNotNull()``
    • ``java Intrinsics.checkParameterIsNotNull()``같은 체크가 추가된다.
  • 런타임에 ``kt null``을 가져와 non-null type에 집어 넣거나, ``kt !!``를 잘못 사용하는 경우 프로그램 실행 도중 다음 예외가 발생한다.
    • ``java java.lang.IllegalStateException: ??? must not be null``
    • ``kt kotlin.TypeCastException: null cannot be cast to non-null type``
  • 그러나... non-null type에 ``kt null``이 들어갔음에도 예외가 발생하지 않는 경우가 있으므로 주의해야 한다. 
    • 예를 들어 리플렉션으로 강제로 넣은 경우. 코틀린은 이러한 프로세스에 관여할 수 있는 방법이 없다.
    • 게다가 이런 경우 런타임 에러가 객체 초기화 시점에 발생하는게 아니라, 멤버에 접근하는 시점에 발생하게 된다.

 

안전한 호출 연산자 ?.    /    엘비스 연산자 ?:

```kt

s?.toUpperCase()    // 다음 구문과 동일

if (s != null) s.toUpperCase() else null    // s==null이면 결과 타입도 null이 된다는 점 유의.

```

연쇄해서 사용한다면 코드를 굉장히 줄일 수 있다. 

엘비스 연산자 ``kt ?:``와 연계해서 사용하면 이런 모습이다.

```kt

class Address(val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

 

fun Person.countryName(): String = 

         this.company?.address?.country ?: "Unknown"

 

>>> val p = Person("umbum", null)

>>> println(p.countryName())

```

코틀린에서는 ``kt return, throw``등의 연산도 식이다. 따라서 엘비스 연산자의 우항에 넣을 수 있다.

또는, ``kt Nothing`` 타입 함수를 엘비스 연산자의 우항에 넣고 사용하면 좋다.

```kt

val address = person.company?.address ?: throw IllegalArgumentException("No address")

```

 

안전한 캐스트 as?

```kt
o as? Person    // 다음 구문과 동일
if (o is Person) o as Person else null    // 역시 결과 타입이 null이 된다는 점.
```
 
이 것도 연산 수행 결과가 ``kt null``이 될 수 있기 때문에 엘비스 연산자와 연계해서 사용한다.
```kt
o as? Person ?: return false
```

 

널 아님 단언 !!

  • 우선은 !!를 바로 쓰기 보다는 ``kt require(), check()`` 를 사용해서 스마트 캐스트 가능하도록 만드는 것을 우선 검토해보면 좋고
  • 스마트 캐스트가 동작하지 않는다거나, require, check를 쓰는게 더 지저분해지는 등 여의치 않은 경우에 !! 쓰는 것이 좋다.
    • ``kt var``이거나, 커스텀 접근자를 사용하는 프로퍼티에 대한 null check 수행 이후. (스마트 캐스트가 동작하지 않음)
    • 액션 API에서 액션이 호출되었을 때, 어떤 값이 필연적으로 존재해야만 이 액션이 호출될 수 있는 경우. 즉, 그 값이 ``kt null``일 수가 없음이 자명한 경우.
  • ``kt !!``를 사용했는데 ``kt null``값이 들어오면 NPE가 발생한다.

```kt

fun f1(v: String?) {

    val str = v ?: return

    f2(str)    // 여기서 !!는 필요 없다.

}

 

fun f2(str: String) {

    println(str)

}

```

 

널이 될 수 있는 타입 확장

직접 정의할 수도 있고, 그냥 가져다 사용할 수도 있음.
널이 될 수 있는 타입에 대해 확장을 정의했다면, 널이 될 수 있는 값에 대해서 그 확장을 호출할 수 있다. 
``kt this``가 널이 될 수 있으므로 정의할 때 명시적으로 널 체크 해주어야 한다.
```kt
>>> var s:String? = null
>>> s.isNullOrBlank()
true
```

 

플랫폼 타입

자바의 타입은 코틀린에서 플랫폼 타입, 즉 널 관련 정보를 알 수 없는 타입이 된다.

단, 자바 원시 타입의 값은 널이 될 수 없으므로 널이 될 수 없는 타입으로 취급된다.

 

플랫폼 타입은 널이 될 수 있는 타입으로 처리해도 되고 널이 될 수 없는 타입으로 처리해도 되기 때문에, 이 값이 널이 될 수 있는지 없는지 잘 생각해보고 널이 될 수 없다면 그냥 사용하면 되고, 널이 될 수 있다면 널 체크를 수행해주어야 한다.

아무튼, 자바 API를 사용할 때는 널을 반환할지 아닐지를 잘 생각해보아야 한다.

```kt

>>> val p = JavaPerson("umbum")

>>> println(p.name.capitalize())

Umbum

>>> val np = JavaPerson(null)

>>> println(np.name.capitalize())

java.lang.IllegalStateException: np.name must not be null

```

 

플랫폼 타입은 ``kt Type!``으로 표시된다. 직접 선언할 수는 없고 자바에서 가져온 것만 이렇게 표기된다.

플랫폼 타입은 널이 될 수 있는 타입이 될 수도, 널이 될 수 없는 타입이 될 수도 있기 때문에 둘 다 대입할 수 있다.

```kt

>>> val i: Int = np.name

error: type mismatch: inferred type is String! but Int was expected

>>> val kp: String? = p.name

>>> val kp: String = p.name

```

그러나, 이미 ``kt null``이 들어 있는 상태에서 널이 될 수 없는 타입에 대입하려고 하면 Exception이 발생한다.

```kt

>>> val kp: String? = np.name

>>> val kp: String = np.name

java.lang.IllegalStateException: np.name must not be null

```

 

아무튼, 그냥 널이 될 수 있는지 없는지만 신경쓰면 된다.

자바 클래스나 인터페이스를 코틀린에서 상속 또는 구현하는 경우에도 널이 될 수 있는지 없는지만 신경써서 붙여주면, 나머지는 컴파일러가 알아서 해준다.

 

*** 자바에도 Optional 있는데 왜 코틀린을 써야해?

라고 말하는 사람들이 종종 있어서... [Effective Java] 8장 메서드 ( null 체크, Optional )