📖 7.1 NullPointerException을 피하고 값이 없는 경우 처리: 널 가능성
코틀린을 포함한 최신 언어에서 null에 대한 접근 방법은 가능한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것
널이 될 수 있는지 여부를 타입 시스템에 추가하으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서 실행 시점에 발생할 수 있는 예외의 가능성을 줄일 수 있다.
📖 7.2 널이 될 수 있는 타입으로 널이 될 수 있는 변수 명시
코틀린과 자바의 첫 번째이자 가장 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점이다.
fun strLen(s: String) = s.length
null이 인자로 들어올 수 없다면 위와 같이 정의 가능
fun main() {
strLen(null) // error
}
컴파일 시, 오류가 발생한다.
fun strLenSafe(s: String?) = s.length() // error
타입 이름 뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있다.
널이 될 수 있는 타입 값의 메서드를 직접 호출할 수는 없다.
fun main() {
val x: String? = null
var y: String = x // error
}
널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다.
fun main() {
val x: String? = null
strLen(x) // error
}
널이 될 수 있는 타입의 값을 null이 아닌 타입의 파라미터를 받는 함수에 전달할 수 없다.
fun strLenSafe(s: String?): Int = if (s != null) s.length else 0
null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 null이 아닌 타입의 값처럼 사용할 수 있다.
📖 7.3 타입의 의미 자세히 살펴보기
타입
가능한 값의 집합과 그런 값들에 대해 수행할 수 있는 연산의 집합
자바의 타입 시스템은 null을 제대로 다루지 못한다.
ex. String 타입의 변수에는 null, String 모두 들어갈 수 있지만 완전히 다르다. 심지어, instanceof 연산자도 null이 String이 아니라고 답한다.
코틀린의 널이 될 수 있는 타입은 이런 문제에 대해 종합적인 해법을 제공한다.
📖 7.4 안전한 호출 연산자로 null 검사와 메서드 호출 합치기: ?.
?.는 null 검사와 메서드 호출을 한 연산으로 수행
class Employee(val name: String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name
fun main() {
val ceo = Employee("Da Boss", null)
val developer = Employee("Bob Smith", ceo)
println(managerName(developer))
println(managerName(ceo))
}
Employee로 프로퍼티 접근 시 안전한 호출을 사용하는 방법
class Address(val streetAddress: String, val zipCode: Int, 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 {
val country = this.company?.address?.country
return if (country != null) country else "Unknown"
}
fun main() {
val person = Person("Dmitry", null)
println(person.countryName())
// Unknown
}
null 검사를 한줄로 연쇄적으로 할 수 있다!
📖 7.5 엘비스 연산자로 null에 대한 기본값 제공: ?:
엘비스 연산자
null 대신 사용할 기본값을 지정할 때 편리하게 사용할 수 있는 연산자
null 복합 연산자라고도 부름
fun greet(name: String?) {
val recipient: String = name ?: "unnamed"
println("Hello, $recipient")
}fun strLenSafe(s: String?): Int = s?.length ?: 0
한줄로 줄여쓸 수 있다.
class Address(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)
fun printShippingLabel(person: Person) {
val address = person.company?.address
?: throw IllegalArgumentException("No address")
with(address) {
println(streetAddress)
println("$zipCode $city, $country")
}
}
코드가 매우 간결해진다!
📖 7.6 예외를 발생시키지 않고 안전하게 타입을 캐스트하기: as?
코틀린에서 대상 값을 as로 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생
as를 사용할 때마다 is로 변환 가능한 타입인지 검사할 수 있지만 너무 귀찮다.
class Person(val firstName: String, val lastName: String) {
override fun equals(o: Any?): Boolean {
val otherPerson = o as? Person ?: return false
return otherPerson.firstName == firstName && otherPerson.lastName == lastName
}
override fun hashCode(): Int = firstName.hashCode() * 37 + lastName.hashCode()
}
as? 연산자는 어떤 값을 지정한 타입으로 변환한다. 변활할 수 없으면 null을 반환한다.
하나의 식으로 해결 가능해진다.
📖 7.7 널 아님 단언: !!
널 아님 단언은 코틀린에서 널이 될 수 있는 타입의 값을 다룰 때 사용할 수 있는 도구 중에서 가장 단순하면서도 무딘 도구다.
!!을 사용하면 어떤 값이든 널이 아닌 타입으로 바꿀 수 있다.
null에 대해 !!를 사용하면 NPE 발생
fun ignoreNulls(str: String?) {
val strNotNull: String = str!! // 예외는 이 지점을 가리킨다.
println(strNotNull.length)
}
예외가 가리키는 지점은 단언문이 위치한 곳을 가리킨다.
결국 !!는 컴파일러에게 null이 아님을 알고 있으며, 예외가 발생해도 감수하겠다는 것을 말하는 것이다.
굳이 느낌표 두개를 선택한 것은 더 나은 방법을 찾아보라는 코틀린 설계자들의 의도이다.
기호가 못생기고 무례함
class SelectableTextList(
val contents: List<String>,
var selectedIndex: Int? = null
)
class CopyRowAction(val list: SelectableTextList) {
fun isActionEnabled(): Boolean = list.selectedIndex != null
fun executeCopyRow() {
val index = list.selectedIndex!!
val value = list.contents[index]
}
}
!!를 사용하여 발생하는 에러 스택 트레이스에는 몇번째 줄인지에 대한 정보가 들어있지 않다.
여러 !! 단언문을 한 줄에 함께 쓰는 일을 피하는 것이 좋다.
📖 7.8 let 함수
fun sendEmailTo(email: String) {
println("Sending email to $email")
}
fun main() {
var email: String? = "yole@gmail.com"
email.let { sendEmailTo(it) }
email = null
email?.let { sendEmailTo(it) }
}
let을 사용하는 가장 흔한 용례는 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우이다.
let 함수는 자신의 수신 객체를 인자로 전달받은 람다에 넘긴다.
아주 긴 식이 있고 그 값이 null이 아닐 때 수행해야 하는 로직이 있을 때 let을 쓰면 훨씬 더 편하다.
📖 7.9 직접 초기화하지 않는 널이 아닌 타입: 지연 초기화 프로퍼티
class MyService {
fun performAction(): String = "Action Done!"
}
class MyTest {
private var myService: MyService? = null
@BeforeAll fun setUp() {
myService = MyService()
}
@Test fun testAction() {
assertEquals("Action Done!", myService!!.performAction())
}
}
이 코드는 보기 나쁘다.
코틀린에서는 클래스 안의 널이 아닌 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메서드 안에서 초기화할 수는 없다. 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다.
프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 초기화해야 한다.
class MyTest {
private lateinit var myService: MyService
@BeforeAll fun setUp() {
myService = MyService()
}
@Test fun testAction() {
assertEquals("Action Done!", myService.performAction())
}
}
lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.
지연 초기화 프로퍼티는 항상 var여야한다.
📖 7.10 안전한 호출 연산자 없이 타입 확장: 널이 될 수 있는 타입에 대한 확장
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) {
println("Please fill in the required fields")
}
}
fun main() {
verifyUserInput(" ")
verifyUserInput(null) // 예외 발생하지 않음.
}
메서드 호출이 null을 수신 객체로 받고 내부에서 null을 처리하게 할 수 있다.
확장 함수에서만 가능
일반 멤버 호출은 객체 인스턴스를 통해 디스패치되므로 그 인스턴스가 null인지 여부를 검사하지 않는다.
널이 될 수 있는 타입의 확장 함수는 자신의 수신 객체가 null일 때 어떻게 해야 하는지 스스로 안다.
안전한 호출 없이도 호출 가능
fun sendEmailTo(email: String) {
println("Sending email to $email")
}
fun main() {
val recipient: String? = null
recipient.let { sendEmailTo(it) } // 안전한 호출을 사용하지 않아 널이 될 수 있는 타입으로 취금
}
let은 this가 null인지 검사하지 않는다.
let을 사용할 때 수신 객체가 null이 아닌지 검사하고 싶다면 안전한 호출 연산인 ?.를 사용해야 한다.
📖 7.11 타입 파라미터의 널 가능성
타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.
fun <T> printHashCode(t: T) {
println(t?.hashCode()) // 안전한 호출 사용
}
fun main() {
printHashCode(null)
// null
}
printHashCode 호출에서 타입 파라미터 T에 대해 추론한 타입은 널이 될 수 있는 Any? 타입이다.
타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상계를 지정해야 한다.
fun <T: Any> printHashCode(t: T) {
println(t.hashCode()) // 안전한 호출 사용
}
fun main() {
printHashCode(null) // error
}
타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반드시 물음표를 타입 이름 뒤에 붙여야 한다는 규칙의 유일한 예외이다.
📖 7.12 널 가능성과 자바
🔖 7.12.1 플랫폼 타입
플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입
널이 될 수 있는 타입 or 널이 될 수 없는 타입으로 처리해도 된다.
컴파일러는 모든 연산을 허용
자바 API를 다룰 때는 조심해야 한다.
대부분의 라이브러리는 널 관련 어노테이션을 쓰지 않는다.
코틀린에서 플랫폼 타입을 선언할 수는 없다.
자바에서 가져온 널 값을 널이 될 수 없는 코틀린 변수에 대입하면 실행 시점에 대입이 이뤄질 때 예외가 발생
🔖 7.12.2 상속
코틀린에서 자바 메서드를 오버라이드할 때 그 메서드의 파라미터와 반환 타입을 널이 될 수 있는 타입으로 선언할지 널이 될 수 없는 타입으로 선언할지 결정해야 한다.
자바 클래스나 인터페이스를 코틀린에서 구현할 경우 널 가능성을 제대로 처리하는 것이 중요
코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어준다.