코틀린의 타입 시스텀은 널을 참조할 수 있는 가능성을 제거하는데 초점이 맞춰져 있습니다. 많은 프로그래밍 언어들을 사용하는데 가장 흔히 발생하는 에러중에 하나는 NullPointerException 입니다.
코틀린에서 NullPointerException이 발생하는 경우는 아래와 같습니다.
명시적으로 'throw NullPointerException()'을 호출 할 때.
null을 참조하는 변수에 !! 연산자를 사용해서 접근할 때.
생성자에서 초기화 되지 않은 this가 인자로 들어와서 어딘가에서 사용될 때.
슈퍼클래스 생성자가 구현 되지 않은 파생 클래스의 멤버를 호출할 때.
코틀린에서 타입 시스템은 null을 가질 수 있는 참조와 가질 수 없는 참조를 구분합니다. 예를 들어, 일반적인 String 타입의 변수는 null을 가질 수 없습니다.
var a: String = "abc"
a = null// Compile Error
변수가 널을 가질 수 있게 하기 위해서는 변수를 nullable 타입으로 선언해야 합니다.
var b: String? = null // can be null
변수 a의 메소드나 프로퍼티를 호출할 때, NPE가 발생하지 않는 다는것을 보증할 수 있습니다. 따라서 이런 호출은 안전합니다.
println(a.length) // 출력: 3
하지만 b에서 같은 메소드나 프로퍼티를 호출한다면 그 호출은 안전하지 않습니다.
println(b.length) // Compile Error
변수 b가 널인지 아닌지 명시적으로 확인할 수 있습니다.
val l = if (b != null) b.length else -1
컴파일러는 널체크를 수행한 정보를 추적하고 length 프로퍼티 호출을 허용해줍니다.
val b: String? = "Kotlin"
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
이런 방식은 널 체크 시기와 b 사용되는 시기 사이에 b에 들어있는 값이 변하지 않을 때만 정상적으로 작동합니다. 따라서 b가 변수(var)일 때는 null 체크와 사용 시기 사이에 b가 다시 null이 될 가능성이 있으므로 안전하지 않습니다.
nullable 변수의 프로퍼티나 메소드에 접근하는 또 다른 방법은 안전한 호출 연산자(?.)를 사용하는 것입니다.
val a = "Kotlin"
val b: String? = null
println(b?.length)
println(a?.length) // Unnecessary safe call
b?.length는 만약 b가 null이 아니라면 b.length값을 반환합니다. 그렇지 않으면 null을 반환합니다.
안전한 호출은 연쇄 호출에서 유용합니다. 예를 들어, Bob은 어떤 부서에 속했을수도 있고 아닐수도 있는 직원입니다. 그 부서에는 Bob의 상사가 있을수도 있고 없을수도 있습니다. Bob이 속한 부서에 있는 상사의 이름을 알고 싶으면 아래와 같이 표현할 수 있습니다.
bob?.department?.head?.name
이런 호출에서 하나의 프로퍼티가 null이라면 반환값은 null이 됩니다.
어떠한 작업에 대해서 null이 아닌 값에 대해서만 수행하고 싶다면 let을 ?.과 함께 사용할 수 있습니다.
val listWithNulls: List<String?> = listOf("Kotlin", null)
for (item in listWithNulls) {
item?.let { println(it) } // prints Kotlin and ignores null
}
안전한 호출(?.)은 대입의 좌항에서도 존재할 수 있습니다. 이런 경우 하나의 호출이라도 null이면 우항은 실행되지 않습니다.
// If either `person` or `person.department` is null, the function is not called:
person?.department?.head = managersPool.getManager()
엘비스 연산자(?:)를 사용하면 널체크를 보다 간결하게 할 수 있다.
fun main() {
val b: String? = null
val l = b?.length ?: -1
print(l) // -1
}
엘비스 연산자의 좌항이 null이 아니라면 엘비스 연산자는 그 값을 반환합니다. 우항이 null이라면 우항의 값을 반환합니다.
우항의 식은 오직 좌항이 null일 때에만 계산됩니다.
코틀린에서 throw나 return은 식이기 때문에 엘비스 연산자의 우항에서 사용할 수 있습니다.
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
널 아님을 단언하는 연산자(!!)는 모든 유형을 non-null 타입으로 변환하고 만약 null인 경우 예외를 던집니다. b!!는 non-null 타입의 b를 반환합니다. 하지만 b가 null이라면 NullPointerException을 throw합니다.
var b: String? = "abc"
val r = b!!.length // 3
b = null
val l = b!!.length // Error
일반적인 캐스팅에서 타겟 타입으로 객체를 변환하지 못하는 경우 ClassCastException이 발생한다. 하지만 안전한 캐스트(as?)를 사용하면 변환하지 못하는 경우 null을 반환한다.
fun main() {
val a = "14"
val aInt: Int? = a as? Int
print(aInt) // null
}
컬렉션의 요소가 nullable타입이라면, filterNotNull을 사용하여 null이 아닌 인자만 추출할 수 있다.
fun main() {
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
print(intList) // [1, 2, 4]
}