(Kotlin) Nullability 관련 연산자
최근 null에 대한 접근 방법은 런타임에 발생하는 NPE를, 컴파일 타임으로 옮기는 것이다.
- 널이 될 수 있음과 없음에 대한 모든 검사는 컴파일 타임에 수행되기 때문에, 실행 시점에는 널이 될 수 있는 타입과 널이 될 수 없는 타입의 객체가 같아진다.
- 단,
@NotNull
애너테이션이나 Intrinsics.checkExpressionValueIsNotNull()
Intrinsics.checkParameterIsNotNull()
같은 체크가 추가된다.
- 단,
- 런타임에
null
을 가져와 non-null type에 집어 넣거나,!!
를 잘못 사용하는 경우 프로그램 실행 도중 다음 예외가 발생한다.java.lang.IllegalStateException: ??? must not be null
kotlin.TypeCastException: null cannot be cast to non-null type
그러나… non-null type에
null
이 들어갔음에도 예외가 발생하지 않는 경우가 있으므로 주의해야 한다.- 예를 들어 리플렉션으로 강제로 넣은 경우.코틀린은 이러한 프로세스에 관여할 수 있는 방법이 없다.
- 게다가 이런 경우 런타임 에러가 객체 초기화 시점에 발생하는게 아니라, 멤버에 접근하는 시점에 발생하게 된다.
안전한 호출 연산자 ?. / 엘비스 연산자 ?:
1
2
s?.toUpperCase() // 다음 구문과 동일
if (s != null) s.toUpperCase() else null // s==null이면 결과 타입도 null이 된다는 점 유의.
연쇄해서 사용한다면 코드를 굉장히 줄일 수 있다. 엘비스 연산자 ?:
와 연계해서 사용하면 이런 모습이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
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())
코틀린에서는 return, throw
등의 연산도 식이다. 따라서 엘비스 연산자의 우항에 넣을 수 있다. 또는, Nothing
타입 함수를 엘비스 연산자의 우항에 넣고 사용하면 좋다.
1
val address = person.company?.address ?: throw IllegalArgumentException("No address")
안전한 캐스트 as?
1
2
o as? Person // 다음 구문과 동일
if (o is Person) o as Person else null // 역시 결과 타입이 null이 된다는 점.
이 것도 연산 수행 결과가 null
이 될 수 있기 때문에 엘비스 연산자와 연계해서 사용한다.
1
o as? Person ?: return false
널 아님 단언 !!
- 우선은 !!를 바로 쓰기 보다는
require(), check()
를 사용해서 스마트 캐스트 가능하도록 만드는 것을 우선 검토해보면 좋고 스마트 캐스트가 동작하지 않는다거나, require, check를 쓰는게 더 지저분해지는 등 여의치 않은 경우에 !! 쓰는 것이 좋다.
var
이거나, 커스텀 접근자를 사용하는 프로퍼티에 대한 null check 수행 이후. (스마트 캐스트가 동작하지 않음)- 액션 API에서 액션이 호출되었을 때, 어떤 값이 필연적으로 존재해야만 이 액션이 호출될 수 있는 경우. 즉, 그 값이
null
일 수가 없음이 자명한 경우.
!!
를 사용했는데null
값이 들어오면 NPE가 발생한다.
1
2
3
4
5
6
7
8
9
fun f1(v: String?) {
val str = v ?: return
f2(str) // 여기서 !!는 필요 없다.
}
fun f2(str: String) {
println(str)
}
널이 될 수 있는 타입 확장
직접 정의할 수도 있고, 그냥 가져다 사용할 수도 있음. 널이 될 수 있는 타입에 대해 확장을 정의했다면, 널이 될 수 있는 값에 대해서 그 확장을 호출할 수 있다. this
가 널이 될 수 있으므로 정의할 때 명시적으로 널 체크 해주어야 한다.
1
2
3
>>> var s:String? = null
>>> s.isNullOrBlank()
true
플랫폼 타입
자바의 타입은 코틀린에서 플랫폼 타입, 즉 널 관련 정보를 알 수 없는 타입 이 된다. 단, 자바 원시 타입의 값은 널이 될 수 없으므로 널이 될 수 없는 타입으로 취급된다.
플랫폼 타입은 널이 될 수 있는 타입으로 처리해도 되고 널이 될 수 없는 타입으로 처리해도 되기 때문에, 이 값이 널이 될 수 있는지 없는지 잘 생각해보고 널이 될 수 없다면 그냥 사용하면 되고, 널이 될 수 있다면 널 체크를 수행해주어야 한다. 아무튼, 자바 API를 사용할 때는 널을 반환할지 아닐지를 잘 생각해보아야 한다.
1
2
3
4
5
6
>>> 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
플랫폼 타입은 Type!
으로 표시된다. 직접 선언할 수는 없고 자바에서 가져온 것만 이렇게 표기된다. 플랫폼 타입은 널이 될 수 있는 타입이 될 수도, 널이 될 수 없는 타입이 될 수도 있기 때문에 둘 다 대입할 수 있다.
1
2
3
4
>>> 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
그러나, 이미 null
이 들어 있는 상태에서 널이 될 수 없는 타입에 대입하려고 하면 Exception이 발생한다.
1
2
3
>>> 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 )