6.1. 널 가능성
6.1.1. 널이 될 수 있는 타입
- 코틀린과 자바의 가장 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 지원한다는 점이다. 자바에서는, 어떤 변수가 널이 될 수 있다면 그 변수에 대해 메소드를 호출할 시 NullPointerException이 발생할 수 있으므로 안전하지 않다. 코틀린은 그런 메소드 호출을 금지함으로써 많은 오류를 방지한다. (NPE의 경우 런타임 시 발생하지만, 코틀린에서는 컴파일 시 오류를 발생시킨다.)
- strLen 함수의 경우 파라미터 s의 타입인 String이 널로 넘어오지 못하게 컴파일 오류를 발생시킨다. 이 함수가 널과 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다.
fun strLenSafe(s:String?) = ...
type? = type 또는 null. 물음표가 없는 타입은 그 변수가 null 참조를 저장할 수 없다는 뜻이다. 따라서 모든 타입은 기본적으로 널이 될 수 없는 타입이다.
- null이 될 수 있는 타입의 변수는 그에 대해 수행할 수 있는 연산이 제한된다. 예를들어, 변수.메소드()처럼 메소드를 직접 호출할 수 없다.
- null이 될 수 있는 타입의 가장 중요한 일은 바로 null과 비교하는 것이다. 일단 null과 비교한 후, 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
fun strLenSafe(s: String?) : Int =
if(s!=null) s.length else 0
6.1.3. 안전한 호출 연산자: ?.
- ?.은 null 검사와 메소드 호출을 한 번의 연산으로 수행한다. 호출하려는 값이 null이 아니라면 ?.은 일반 메소드 호출처럼 작동한다. 호출하려는 값이 null이면 이 호출은 무시되고 null이 결과 값이 된다.
class Employee(val name:String, val manager: Employee?)
fun managerName(employee: Employee): String? = employee.manager?.name
>>> val ceo = Employee("boss", null)
>>> val developer = Employee("bob", ceo)
>>> println(managerName(developer)
boss
>>>println(managerName(ceo))
null
6.1.4. 엘비스 연산자: ?:
- 코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공한다. 이를 엘비스 연산자라고 한다.
fun strLenSafe(s: String?): Int = s?.length?:0
>>>println(strLenSafe("abc")
3
>>>println(strLenSafe(null))
0
6.1.5. 안전한 캐스트: as?
- 자바 타입 캐스트와 마찬가지로 대상 값을 as로 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생한다. as를 사용할 때마다 is로 미리 변환 가능한지 검사할 수 있으나, as? 연산자를 이용해 어떤 값을 지정한 타입으로 캐스트 하되, 대상 타입으로 변환할 수 없으면 null을 반환하도록 할 수 있다.
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()
}
6.1.6. 널 아님 단언: !!
- !!를 사용하면 어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다. 실제 널에 대해 !!를 적용하면 NPE가 발생한다.
fun ignoreNulls(s:String?){
val sNotNull:String= s!!
println(sNotNull.length)
- 어떤 함수가 값이 널인지 검사한 다음, 다른 함수를 호출한다고 해도 컴파일러는 호출된 함수 안에서 그 값을 사용할 수 있음을 인식할 수 없다. 하지만 이런 경우 호출된 함수가 언제나 다른 함수에서 널이 아닌 값을 전달받는다는 사실이 분명하다면 굳이 널 검사를 다시 수행하고 싶지 않을 것이다. 이 경우 널 아님 단언문을 쓸 수 있다.
6.1.7. let 함수
- let 함수를 안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해 한꺼번에 처리할 수 있다.
fun sendEmailTo(email:String){
println("Sending email to $email")
}
>>>val email:String? = "yellow@example.com"
>>>email?.let {sendEmailTo(it)}
Sending email to yellow@example.com
>>>email = nul
>>>email?.let {sendEmailTo(it)}
6.1.8. 나중에 초기화할 프로퍼티
- 객체 인스턴스를 일단 생성한 다음에 나중에 초기화하는 프레임워크가 많다. 하지만 코틀린에서는 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화할 수는 없다. 일반적으로는 생성자에서 모든 프로퍼티를 초기화하며, 만약 프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 초기화해야 한다. 그럴 수 없다면 널이 될 수 있는 타입을 제공해야 하거, 이 경우 모든 프로퍼티 접근에 널 검사를 넣거나 !!를 넣어야하므로 보기 나쁜 코드가 된다.
- lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.
class MyService{
fun performAction():String = "action"
}
class MyTest{
private lateinit var myService:MyService //초기화하지 않고 널이 될 수 없는 프로퍼티로 선언
@Before fun setUp(){
myService = MyService() //진짜 초깃값 지정
}
@Test fun testAction(){
Assert.assertEquals("action", myService.performAction()) //널 검사를 수행하지 않고 프로퍼티 사용
}
}
- 나중에 초기화하는 프로퍼티는 항상 var여야 한다. val의 경우 final 필드로 컴파일되며, 생성자 안에서 반드시 초기화 해야 한다.
6.1.9. 널이 될 수 있는 타입 확장
fun verifyInput(input:String?){
if(input.isNullOrBlank()){ //안전한 호출을 하지 않아도 된다.
println("Please fill in the required fields")
}
}
>>>verifyInput(" ")
Please fill in the rquired fields
>>>verifyInput(null)
Please fill in the rquired fields
- 널이 될 수 있는 타입의 값에 대해 널이 될 수 있는 타입의 확장 함수를 호출할 시, 안전한 호출(?.)을 사용하지 않고도 호출 가능하다.
6.1.10. 타입 파라미터의 널 가능성
- 코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다. 따라서 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다.
fun <T> printHashCode(t:T){
println(t?.hashCode()) //t가 널이 될 수 있으므로 안전한 호출 사용.
}
- 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한을 지정해야 한다. 이를 지정하면 널이 될 수 있는 값을 거부하게 된다.
fun <T> printHashCode2(t:T){ //이제 T는 널이 될 수 없는 타입.
println(t.hashCode())
}
printHashCode2(null)에 대해 Null can not be a value of a non-null type TypeVariable(T)와 같은 컴파일 오류가 발생한다.
- 타입 파라미터는 널이 될 수 있는 타입을 표시하려면 반ㄷ시 ?를 타입 이름 뒤에 붙여야 한다는 규칙의 유일한 예외다.
6.1.11. 널 가능성과 자바
- 널 가능성을 지원하지 않는 자바를 코틀린과 조합해서 사용할 시 상호운용성을 어떻게 향상시킬 수 있을까?
- 첫번째로 자바 코드에 애노테이션으로 표시된 널 가능성 정보를 활용한다. @Nullable String은 String?과, @NotNull String은 String과 같다.
- 애노테이션이 소스 코드에 없는 경우 자바의 타입은 코틀린의 플랫폼 타입이 된다. 플랫폼 타입은 코틀린이 널 관련 정보를 알 수 없는 타입을 말한다. 그 타입을 널이 될 수 있는 타입으로 처리하든, 널이 될 수 없는 타입으로 처리하든 개발자의 몫이며 컴파일러는 모든 연산을 허용한다. 자바 api를 다룰 때는 자바 메소드가 널을 반환할 지 알아내고 널을 반환하는 메소드에 대한 널 검사를 추가해야 한다.(모든 자바 타입을 널이 될 수 있는 타입으로 다루지 않은 이유는 결코 널이 될 수 없는 값에 대해서 불필요한 널 검사가 들어가지 않도록 하기 위해서다.)
- 코틀린에서는 플랫폼 타입을 선언할 수 없다. 자바에서 가져온 타입만 플랫폼 타입이 된다.
'3-1기 스터디 > Kotlin' 카테고리의 다른 글
[3-2, 1주차] 연산자 오버로딩과 기타 관례(1) (0) | 2022.04.06 |
---|---|
[12주차] 코틀린 타입 시스템(2) (0) | 2022.01.30 |
[10주차] 람다로 프로그래밍(4) (0) | 2022.01.16 |
[9주차] 람다로 프로그래밍(3) (0) | 2022.01.09 |
[8주차] 람다로 프로그래밍(2) (0) | 2022.01.03 |
댓글