throw NullPointerException()
를 호출하는 경우.!!
연산자를 사용하는 경우 (아래에서 설명).null이 아니라고 단언한 경우임 → 근데 Null이 들어갔으니 에러를 발생하는 것이 당연
초기화되지 않은 **this**
가 어딘가에 전달 및 사용되는 경우 ("leaking this").초기화되지 않은 상태
를 사용하는 경우. Kotlin은 "플랫폼 유형(platform types)"이라는 개념을 도입하여 Java와의 상호 운용성을 지원합니다. 플랫폼 유형은 null 안전성에 대한 정보가 충분하지 않은 경우에 사용됩니다. 즉, Kotlin에서 정확히 Nullable 또는 Non-Nullable 여부를 결정할 수 없는 상황에서 발생합니다. 따라서 Kotlin에서 플랫폼 유형에 액세스하면 컴파일러는 null 안전성 검사를 적용하지 않습니다. 이로 인해 NPE가 발생할 수 있으므로 주의해야 합니다.
플랫폼 유형에 대한 작업을 수행할 때는 주의해야 합니다. 주로 Java에서 가져온 코드와 상호 작용하는 경우 발생할 수 있습니다. 이런 경우에는 명시적으로 Nullable 여부를 확인하고 안전하게 처리해야 합니다.
- Java 상호 운용성을 위해 사용되는 제네릭 타입의 Nullable 문제. 예를 들어, Java 코드에서 Kotlin MutableList<String>에 null을 추가하면 Kotlin에서는 MutableList<String?>이 필요할 수 있습니다.
Kotlin의 제네릭 타입과 Java의 제네릭 타입 간의 차이 때문에 이러한 문제가 발생할 수 있습니다. Kotlin에서의 제네릭은 기본적으로 Non-Nullable이며, Java에서의 제네릭은 Nullable이 될 수 있습니다. 따라서 Kotlin에서 Java로부터 제네릭 타입을 사용할 때, Kotlin 컴파일러는 해당 타입을 Nullable로 처리할 수 있습니다.
예를 들어, Java에서 MutableList<String>에 null 값을 추가하면 Kotlin에서는 MutableList<String?>이 필요할 수 있습니다. 이 경우, Kotlin에서는 MutableList에 String이 아닌 Nullable String을 추가할 수 있는 유연성이 필요하므로 Nullable 제네릭 타입을 사용해야 합니다.
이러한 상황에서는 주로 컴파일러의 경고나 에러 메시지를 주의 깊게 확인하고, 필요한 경우 명시적으로 Nullable 또는 Non-Nullable을 지정하여 상호 운용성을 관리해야 합니다. 예를 들어, Kotlin에서 Java로 Java로부터 제네릭 타입을 전달하는 경우 **`as`** 연산자를 사용하여 Nullable 또는 Non-Nullable을 명시적으로 지정할 수 있습니다.
```kotlin
val javaList: MutableList<String> = ArrayList<String>()
val kotlinList: MutableList<String?> = javaList as MutableList<String?>
```
이렇게 상호 운용성 문제를 다루면 Kotlin과 Java 코드가 함께 작동하고 NullPointer 예외를 방지할 수 있습니다. 하지만 주의가 필요하며, 코드를 안전하게 관리하기 위해 명시적인 Nullable 및 Non-Nullable 타입 지정을 사용해야 합니다.
Kotlin은 변수의 타입에 따라 널이 될 수 있는지 여부를 구분합니다.
예를 들어, **`var a: String`**은 널이 될 수 없는 타입으로 선언되어 있으며, **`a = null`**과 같은 시도는 컴파일 오류를 발생시킵니다.
```kotlin
var a: String = "abc" // 기본적인 초기화는 기본적으로 null을 허용하지 않음
a = null // 컴파일 오류
```
반면, **`var b: String?`**은 널이 될 수 있는 타입으로 선언되어 있어, **`b = null`**을 허용합니다.
```kotlin
var b: String? = "abc" // null로 설정 가능
b = null // 허용
```
**`a`**와 같은 변수에서 메소드를 호출하거나 속성에 액세스하면 NPE를 발생시키지 않으므로 다음과 같이 안전하게 사용할 수 있습니다:
```kotlin
val l = a.length
```
그러나 **`b`**와 같은 변수의 동일한 속성에 액세스하려면 안전하지 않으며 컴파일러는 오류를 보고합니다:
```kotlin
val l = b.length // 오류: 변수 'b'는 null일 수 있음
```
널이 될 수 있는 변수에 대한 안전한 접근은 **`?.`** 연산자를 사용하여 수행합니다. 이것은 해당 변수가 널이 아닌 경우에만 속성 또는 메서드를 호출하고, 널인 경우에는 **`null`**을 반환합니다.
```kotlin
var timestamp: Instant? = null
val isoTimestamp = timestamp?.toString() // Returns a String? object which is `null`
if (isoTimestamp == null) {
// Handle the case where timestamp was `null`
}
```
Elvis 연산자 **`?:`** 를 사용하여 "만약 변수가 널이 아니라면 그 값을 사용하고, 널이라면 다른 값을 사용하라"와 같은 조건을 간단하게 표현할 수 있습니다.
```kotlin
val l = b?.length ?: -1
```
```kotlin
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
```
**`!!`** 연산자를 사용하면 널이 될 수 있는 변수를 널이 아닌 타입으로 강제로 변환할 수 있지만, 변수가 널인 경우 NPE (NullPointerException) 예외를 발생시킵니다. 이것은 널에 대한 명시적인 처리를 요구합니다.
예를 들어, **`b!!`**를 사용하면 **`b`**의 non-null 값 (예: 위의 예에서 **`String`**)을 반환하거나 **`b`**가 null인 경우 NPE를 throw합니다.
```kotlin
val l = b!!.length
```
기본적으로 Kotlin은 null 안전성을 제공하여 NPE를 방지하도록 설계되었으며, 가능한한 **`!!`** 연산자를 사용하는 것은 피해야 합니다. 왜냐하면 이 연산자는 NPE를 명시적으로 요청해야 하며, 예외가 예상치 못한 곳에서 발생하지 않도록 합니다.
**`as?`** 연산자를 사용하면 타입 변환 시, 대상 타입으로 변환이 실패하면 **`null`**을 반환하게 됩니다.
또한 안전한 캐스트를 사용할 수 있으며, 이러한 캐스트는 대상 타입이 아닌 경우 null을 반환합니다. 아래는 예시입니다:
```kotlin
val a Int: Int? = a as? Int
```
또한 nullable 타입의 원소로 이뤄진 컬렉션에서 non-nullable 원소를 필터링하려면 **`filterNotNull`**를 사용할 수 있습니다. 아래는 예시입니다:
```kotlin
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
```
Kotlin 표준 라이브러리에는 객체의 컨텍스트 내에서 코드 블록을 실행하는 것이 목적인 여러 함수가 포함되어 있습니다. 이러한 함수를 객체에 람다 표현식
을 제공하여 호출하면 임시 범위
가 형성됩니다. 이 범위 내에서 객체에 대한 이름없이 접근할 수 있습니다. 이러한 함수는 범위 함수
라고 불립니다. 다섯 가지 범위 함수가 있으며, 이들은 let, run, with, apply 및 also입니다.
이러한 함수들은 기본적으로 모두 동일한 작업을 수행합니다. 차이점은 이 객체가 블록 내에서 어떻게 사용 가능해지며 전체 표현식의 결과가 무엇인가입니다.
다음은 범위 함수를 사용하는 전형적인 예제입니다:
Person("Alice", 20, "Amsterdam").let {
println(it)
it.moveTo("London")
it.incrementAge()
println(it)
}
let을 사용하지 않고 동일한 코드를 작성하면 새 변수를 도입하고 사용할 때마다 해당 이름을 반복해서 사용해야 합니다.
val alice = Person("Alice", 20, "Amsterdam")
println(alice)
alice.moveTo("London")
alice.incrementAge()
println(alice)
범위 함수는 새로운 기술적 기능을 도입하지 않지만 코드를 더 간결하고 가독성있게 만들 수 있습니다.
범위 함수 간에 많은 유사점이 있기 때문에 사용 사례에 따라 적절한 함수를 선택하는 것은 까다로울 수 있습니다. 선택은 주로 의도와 프로젝트 내에서의 일관성에 따라 달라집니다. 아래에서 각 범위 함수의 차이점에 대한 자세한 설명을 제공합니다.
함수 선택
사용 목적에 따라 적절한 범위 함수를 선택하는데 도움이 되도록 다음 표를 제공합니다.
함수 | 객체 참조 | 반환 값 | 확장 함수 여부 |
---|---|---|---|
let | it | 람다 결과 | 네 |
run | this | 람다 결과 | 네 |
run | - | 람다 결과 | 아니오 called without the context object |
with | this | 람다 결과 | 아니오 takes the context object as an argument. |
apply | this | 컨텍스트 객체 | 네 |
also | it | 컨텍스트 객체 | 네 |
범위 함수는 본질적으로 유사하므로 그들 사이의 차이점을 이해하는 것이 중요합니다. 각 범위 함수 간의 주요 차이점은 다음 두 가지입니다.
범위 함수의 람다 내에서 컨텍스트 객체는 실제 이름 대신 짧은 참조로 사용할 수 있습니다. 각 범위 함수는 컨텍스트 객체를 참조하는 두 가지 방법 중 하나를 사용합니다: 람다 수신자(this)
또는 람다 인수(it)
. 둘 다 동일한 기능을 제공하므로 각각 다른 사용 사례에 대한 장단점을 설명하고 사용에 대한 권장 사항을 제공합니다.
run, with 및 apply는 컨텍스트 객체를 람다 수신자(this)로 참조합니다. 따라서 람다 내에서 객체는 일반적인 클래스 함수에서와 같이 사용할 수 있습니다. 대부분의 경우, 수신자 멤버에 액세스할 때 this를 생략할 수 있어 코드를 짧게 만들 수 있습니다. 그러나 this를 생략하면 수신자 멤버와 외부 객체나 함수 간의 구분이 어려워질 수 있으므로 주로 객체의 멤버에 대한 함수 호출이나 속성에 값 할당을 주요 작업으로 하는 람다에서 컨텍스트 객체를 수신자로 사용하는 것이 좋습니다.
this를 사용한 예시:
val adam = Person("Adam").apply {
age = 20 // this.age = 20과 동일
city = "London"
}
반면에 let 및 also는 람다 인수(it)로 컨텍스트 객체를 참조합니다. 인수 이름이 지정되지 않으면 기본 이름 it으로 객체에 액세스됩니다. it는 this보다 짧으며 it를 사용한 표현식은 보통 더 읽기 쉽습니다. 그러나 객체의 함수나 속성을 호출할 때 this와 달리 람다 내에서 객체를 암묵적으로 사용할 수 없습니다. 따라서 객체가 주로 함수 호출 인수로 사용되는 람다에서 it를 사용하는 것이 더 좋습니다. 코드 블록에서 여러 변수를 사용하는 경우에도 it을 사용하는 것이 더 효율적입니다.
it을 사용한 예시
fun getRandomInt(): Int {
return Random.nextInt(100).also { it ->
writeToLog("getRandomInt() generated value $it")
}
}
또한 it을 인수로 사용할 때 인수 이름을 지정하여 사용할 수도 있습니다.
fun getRandomInt(): Int {
return Random.nextInt(100).also { value ->
writeToLog("getRandomInt() generated value $value")
}
}
범위 함수는 반환 값에 따라 다릅니다.
코드에서 무엇을 하려는지를 고려하여 반환 값을 신중하게 선택해야 합니다. 이려면 코드에서 다음 단계를 무엇을 하려고 하는지 고려하십시오. 이것은 어떤 범위 함수를 사용할지 선택하는 데 도움이 됩니다.
또한 반환 값을 무시하고 범위 함수를 사용하여 지역 변수에 대한 한정된 범위를 만들어 코드를 더 읽기 쉽게 작성할 수도 있습니다.
참고: 범위 함수를 남용하지 않는 것이 좋습니다. 범위 함수를 과도하게 사용하면 코드가 읽기 어려워지고 오류가 발생할 수 있습니다. 또한 범위 함수를 중첩하거나 연결할 때 현재 컨텍스트 객체와 this 또는 it의 값에 혼동이 생길 수 있으므로 주의해야 합니다.
범위 함수 내에서 pravite 함수나 변수에 대한 접근은 안된다!
let→ null이 아닐때
이름이 가지고 있는 의미를 좀 느껴보셈