[Kotlin] Nullable 처리
📚 Nullable 처리
📔 널러블 여부
보통 변수에 객체가 할당되지 않거나 null이 할당된 경우에 메서드나 연산자 등을 사용하면 널 포인터 예외가 발생한다. 이를 방지하기 위해 항상 널을 체크한 후에 연산자나 메서드 처리를 수행한다.
📖 널러블 자료형 처리 규칙
- 기존 자료형에 물음표를 붙이면 널을 처리할 수 있는 자료형이 만들어짐
- 널러블 자료형은 항상 널이 불가능한 자료형보다 상위 자료형이다. 그래서 널이 불가능한 자료형의 변수를 널러블 자료형의 변수에 할당할 수 있다. 하지만 반대로 널러블 자료형은 널이 불가능한 자료형의 변수에 할당할 수 없다
- 널에 대한 체크는 컴파일 타임에 확정하지만, 실제 실행할 때는 처리하지 않는다. 그래서 널에 대한 처리는 전부 컴파일 처리할 때 예외를 발생시킨다
📖 널러블 처리 연산자
- 안전 연산자(?.): 널 값이 들어오면 그다음 메서드를 처리하지 않는다. 반면 실제 객체가 들어올 경우에는 안전하게 메서드를 실행한다
- 엘비스 연산자(?:): 널 값이면 다음에 들어오는 값으로 변환한다. 안전 연산자와 엘비스 연산자를 같이 사용하면 조건문을 사용하는 것보다 편리하다
- 널 단언 연산자(!!): 널 값이 들어올 수 없다고 확신할 경우에만 사용한다. 널 값이 들어오면 예외를 발생시킨다
val ss = "100"
println(ss!!.length)
val ss1: String? = null
try {
ss1!!.length // 널 값이 아니어야 함. 널 값이 들어오면 예외 처리
} catch (e: Exception) {
println("예외: ${e.message}")
}
val d: String? = "문자열"
println(d?.length) // 널인 경우 널 처리. 그렇지 않다면 뒤의 메서드 실행
val e: String? = null
println(e?.length)
val f: String? = null
println(e?.length ?: 0) // 널 값이 라면 뒤의 0을 디폴트 값으로 처리
var g = if (f != null) f?.length else 0
println(g)
3
예외: null
3
null
0
0
📖 널러블 자료형 지정
변수나 함수 반환을 처리할 때 널이 가능한 자료형인 널러블 자료형으로 변환해서 널이 들어오는 것을 명기할 수 있다.
import kotlin.reflect.full.isSubclassOf
val a1: Any? = null // nullable 타입 지정
val a2: Any = Any()
val a3: Any? = Any()
var s1: String? = null
var s2: String = "문자열"
//s2 = s1 <- 상위 타입에 하위 타입 처리 불가
s1 = s2 // 상위 타입에 하위 타입 처리 가능
println(s1)
s2 = s1 as String // 명확한 타입으로 저장
println(s2)
var s3: String = "문자열"
var s4: String? = "문자열2"
println((s3::class))
println((s4!!::class))
println((s3::class).isSubclassOf(s4!!::class))
println((s4!!::class).isSubclassOf(s3::class))
문자열
문자열
class kotlin.String
class kotlin.String
true
true
📖 널러블 자료형 점검한 후에 처리
변수에 널러블 자료형이 할당되면 널 값이 들어올 수 있다는 것을 알 수 있다. 들어온 널 값을 예외 없이 처리하려면 널 값을 체크한 다음에 널 값이 아닌 경우만 메서드나 속성 등을 처리한다.
val b: String? = "Kotlin" // 널러블 문자열에 문자열 할당
if (b != null) println("문자열: $b")
else println("널값")
val c: String? = null // 널러블 문자열에 널 할당
if (c == null) println("널값")
else println("문자열: $c")
문자열: Kotlin
널값
📖 널러블 연산자로 처리
안전 연산자(?.)를 사용하면 널 값일 경우 null로 처리하고 널이 아니면 그다음에 오는 속성이나 메서드를 실행한다.
fun <T: Any> T?.mapCheck(f: (T) -> T): T? = // 널러블 처리
when(this) {
null -> null // 널 값
else -> f(this) // 원래 타입을 처리
}
val n1: Any? = null // 널러블 타입을 지정하면 널값 할당 가능
val n2 = 100
println(n1?.mapCheck({ it }))
println(n2.mapCheck({ it * 3 }))
null
300
📖 내장 클래스의 널을 처리하는 메서드
문자열이나 리스트 등은 널러블로 사용할 경우 내부적으로 널 값을 처리하는 메서드가 있다.
val ff: String? = null
println(ff?.getOrNull(0))
val nullableList: List<Int?> = listOf(1, 2, null, 4) // 리스트 내 원소
println(nullableList.filterNotNull()) // 리스트 내 널 원소 제거
val s1: String? = null
println(s1.isNullOrEmpty())
println(s1.isNullOrBlank())
val s2 = " "
println(s2.isNullOrEmpty())
println(s2.isNullOrBlank())
val s3: String = "\t\n"
println(s3.isNullOrEmpty())
println(s3.isNullOrBlank())
null
[1, 2, 4]
true
true
false
true
false
true