널 가능성(nullability)은 NullPointerException(NPE) 오류를 피하는데 도움이 되는 코틀린 타입 시스템의 특성이다.
null이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 미리 null 문제를 감지하여 실행 도중의 NPE 발생 가능성을 줄일 수 있다.
코틀린의 타입 시스템은 자바와 달리 null이 될 수 있는 타입과 없는 타입을 명시적으로 구분한다.
var nonNullStr: String = "abc" // null 불가능한 타입
var nullableStr: String? = "abc" // null 가능한(non-null) 타입
둘은 같은 문자열이지만 위에 변수엔 null을 저장할 수 없고, 아래는 가능하다.
nonNullStr = null // 컴파일 에러!
이처럼 어떤 타입이든 뒤에 ?를 붙이면 변수에 그 타입 객체와 null을 저장할 수 있으며, nullable 타입이라고 한다.
nullable 타입엔 제약 사항이 있다. 일반적인 방식으로 프로퍼티나 함수를 호출할 수 없고, non-null 타입의 변수나 파라미터에 대입할 수도 없다.
따라서, 아래의 코드는 모두 컴파일러가 허락하지 않는다.
// 컴파일 에러1
fun strLen1(str: String?) = str.length
// 컴파일 에러2
val x: String? = "abc"
val y: String = x // non-null 타입에 nullable 변수 값 할당 불가능
// 컴파일 에러3
fun strLen2(str: String) = str.length
strLen2(x) // non-null 파라미터에 nullable 인자 전달 불가능
이렇게 제약이 많은데 nullable로 대체 뭘 할 수 있을까?
우선, if문 검사를 통과한 변수는 특정 범위 안에서 non-null로 사용할 수 있다.
fun strLenSafe(str: String?): Int =
if (str != null) str.length else 0
그런데 nullable 타입을 다루는 도구가 if문 뿐이면 다루기 번거롭고 코드가 지저분해질 것이다. 당연하고 다행히도 코틀린은 nullable 타입을 다루는 여러 도구를 지원한다.
nullability를 다루는 가장 유용한 연산자는 safe call이라고 불리는 ?.이다. ?.는 객체와 메서드(프로퍼티) 사이에 위치하는데, null 검사와 메서드 호출을 함께 수행한다.
fun printAllCaps(s: String?) {
val caps: String? = s?.toUpperCase()
println(caps)
}
s?.upUpperCase()에서 safe call이 사용되었다. 이것은 훨씬 더 긴 if (s != null) s.toUpperCase() else null과 같은 동작을 한다. s가 null이 아니면 일반 메서드 호출처럼 동작하고, null이면 메서드를 무시하고 결과 값이 null이 된다.
>>> printAllCaps("abc")
ABC
>>> printAllCaps(null)
null
safe call은 null이 될 수 있는 중간 객체가 여러 개 있을 때 연쇄 호출해서 편하게 null 처리를 할 수 있다.
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val address: Address?)
class Person(val name: String, val company: Company?)
fun Person.countryName(): String {
val country = this.company?.address?.country
return if (country != null) country eles "Unknown"
}
>>> println(Person("홍길동", null).countryName())
Unknown
위에서 val country = this.company?.address?.country를 보면 ?.를 연쇄하여 추가 검사 없이 한 줄로 null을 처리하고 있다.
코틀린은 어떤 식이 null일 경우 대신 사용할 default 값을 지정할 수 있는 연산자를 제공한다. 그것을 엘비스 연산자라고 하며 ?:로 생겼다.
fun strLenSafe(s: String?): Int = s?.length ?: 0
>>> println(strLenSafe("abc"))
3
>>> println(strLenSafe(null))
0
s?.length ?: 0에서 s?.length가 null이 아니면 s?.length이 결과 값이 되고, null이면 ?: 뒤의 0이 전체 식의 결과가 된다.
참고로 ?:를 시계 방향으로 90도 돌리면 엘비스 프레슬리의 헤어스타일과 비슷하다는 이유로 엘비스 연산자라고 한다. (ㅋ)
엘비스 연산자를 이용하면 위에서 봤던 Person.countryName 함수를 한 줄로 표현할 수도 있다.
fun Person.countryName(): String =
this.company?.address?.country ?: "Unknown"
?:는 return, throw와 함께 함수의 전제 조건을 검사할 때 유용하다.
fun printAddress(person: Person) {
val address = person.company?.address ?: throw IllegalArgumentException("No address")
println(address)
}