6.1 널 가능성
NullPointerException ?
Null 값을 가지고 있는 변수를 사용해서 객체의 필드나 메소드에 접근하려고 하면 발생하는 오류.
runtime에 발생하는 exception 입니다.
코틀린에서는 "null이 될 수 있는 타입" 이란 것을 지원함으로써,
null에 접근할 때를 가능한한 실행시점에서(runtime) 컴파일 시점으로 옮깁니다.
이것은 null 될 수 있는지 여부를 타입시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지하게 한다는 것입니다.
6.1.1 널이 될 수 있는 타입
코틀린의 변수는 기본적으로 null이 될 수 없는 타입입니다.
null이 될 수 있는 변수를 인자로 받는 메소드를 호출하는것을 금지합니다.
fun strLen(s: String) = s.length
코틀린에서 다음과 같이 함수를 정의했을때, strLen 함수에 넘기는 인자인 s가 null이거나 null이 될 수 있는 인자라면 컴파일 시 오류가 발생합니다.
이 함수가 널을 인자로 받을수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 합니다.
fun strLensafe(s: String?) = ...
어떤 타입이든 타입 이름뒤에 물음표를 붙이면 그 타입의 변수나 프로퍼티에 null 참조를 저장할 수 있습니다.
이렇게 코틀린에서는 명시적으로 널이 될 수 있는 타입을 지원합니다.
널이 될 수 있는 타입의 변수는 수행할 수 있는 연산이 제한됩니다.
먼저, 메소드를 직접 호출할 수 없습니다.
fun strLensafe(s: String?) = s.length()
→ 에러: s는 ?이 붙거나 !!이 붙은 호출만 허용된다.
널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없습니다.
val x: String? = null var y: String = x
→ 에러: Type mismatch
널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다.
fun strLensafe(s: String) = ...
val x: String? = null
strLen(x)
// strLen은 널이 될수 없는 타입의 파라미터를 받는데 x는 널이 될 수 있는 타입인듯?
→ 에러: Type mismatch
null이 될 수 있는 타입의 값으로 뭘 할 수 있을까?
null과 비교하는 것.
일단 null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용할 수 있다.
fun strLenSafe(s: String?): Int =
if (s != null) s.length else 0
//if로 null 검사를 했기때문에 s.length를 s?.length로 쓰지 않아도 된다.
6.1.2 타입의 의미
타입이란 무엇이고 왜 변수에 타입을 지정해야하는 걸까?
→ 타입은 분류이고, 타입은 어떤 값들이 가능한지와 그 타입에 대해 수행할 수 있는 연산의 종류를 결정한다. (위키피디아 글)
해당 타입에 속한 값이라면 해당 타입에 대한 연산을 적용시킬수 있고 그 연산이 성공적으로 실행된다는 사실을 확신할 수 있다.
자바의 타입?
자바에서는 해당 타입에 속한 값 외에 null 값도 들어갈 수 있다.
하지만 해당 타입과 null 값, 각각에 실행할 수 있는 연산이 완전히 다르다.
이것은 자바의 타입 시스템이 null을 제대로 다루지 못한다는 뜻이다.
변수에 선언된 타입이 있지만 널 여부를 추가로 검사하기 전에는 그 변수에 대해 어떤 연산을 수행할 수 있을지 알 수 없다.
프로그램을 작성하면서 널 검사를 생략하는 경우가 자주 있고, 그러다 NullPointerException이 발생되며 오류로 프로그램이 종료될수 있다.
NullPointerException 오류를 다루는 다른 방법
요약 : 자바에서 NPE 문제를 해결하는 여러 방법들이 있지만 완벽하지 않고 해결하는 다른 방법으로는 null값을 코드에서 절대 쓰지 않는 방법이 있다.
코틀린의 널이 될 수 있는 타입을 명시적으로 선언하는 방법은 각 타입에 값에 대해 어떤 연산이 가능할지 명확히 이해할 수 있고, 실행 시점에 예외를 발생시킬 수 있는 연산을 판단할 수 있다.
따라서 그런 연산을 아예 금지할 수도 있다.
노트
널이 될 수 있는 타입의 검사는 컴파일 시점에 수행되고 별도의 실행 시점 부가 비용이 들지 않는다.
6.1.3 안전한 호출 연산자: ?.
?.은 null 검사와 메소드 호출을 한 번의 연산으로 수행한다.
호출하려는 값이 null이 아니라면 ?.은 일반 메소드 호출처럼 작동한다.
예를 들어
s?.toUpperCase() //이것은
if (s!=null) s.toUpperCase() else null //얘랑 같다
호출하려는 값이 null이면 이 호출은 무시되고 null이 결과 값이 된다.
리스트 6.3 안전한 호출 연쇄
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 "Unkown"
}
널 검사가 들어간 호출이 연달아 있는 경우를 자바 코드에서 자주 볼 수 있다.
하지만 코틀린에서는 훨씬 간결하게 널 검사를 할 수 있다.
그리고 소스의 맨 마지막 줄에 if문도 없앨 수 있다.
6.1.4 엘비스 연산자: ?:
코틀린은 null 대신 사용할 디폴트 값을 지정할 때 편리하게 사용할 수 있는 연산자를 제공한다.
그 연산자는 엘비스 연산자라고 한다.
엘비스 연산자 사용 방법
fun foo(s: String?) { val t: String = s ?: "" // "s"가 null이면 결과는 빈 문자열("")이다. }
fun foo(s: String?) {
val t: String = s ?: ""
// "s"가 null이면 결과는 빈 문자열("")이다.
}
엘비스 연산자는 이항 연산자로, 좌항을 계산한 값이 널인지 검사하고,
널이 아니면 좌항 값이, 널이면 우항 값이 결과 값이 된다.
객체가 널인 경우
널을 반환하는 안전한 호출 연산자와 (.?)
널인 경우를 대비한 값을 지정하는 경우 (:?)
fun strLensafe(s: String?) : Int = s?.length ?: 0
fun Person.countryName() = company?.address?.country ?: "Unknown"
코틀린에서는 return이나 throw 등의 연산도 식이다.
따라서 엘비스 연산자의 우항에 return, throw 등의 연산을 넣을 수 있다.
val address =
person.company?.address ?: throw IllegalArgumentException("No address")
// 주소가 없으면 예외를 발생시킨다.
// NullPointerException 대신 의미 있는 오류.
with (address) {
// 앞에서 검사했기때문에 address는 null이 아니라는 것을 컴파일러는 기억하고
// null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입의 값처럼 사용
println(streetAddress)
println("$zipCode $city, $country")
}
6.1.5 안전한 캐스트: as?
as 연산자는 대상 값을 as로 지정한 타입으로 바꿀 수 없으면 ClassCastException이 발생한다.
as 대신 as?를 사용할 수 있다.
as? 연산자는 어떤 값을 지정한 타입으로 캐스트한다.
as?는 지정한 타입으로 캐스트할 수 없으면 null을 반환한다.
여기에 엘비스 연산자를 같이 사용하면 유용함.
val otherPerson = o as? Person ?: return false
// 타입이 서로 일치하지 않으면 false를 반환한다.
return otherPerson.firstName == firstName && otherPerson.lastName == lastName
// otherPerson이 Person으로 안전한 캐스트를 하고나면 Person 타입으로 스마트 캐스트 됨.
6.1.6 널 아님 단언: !!
!!은 어떤 값이든 널이 될 수 없는 타입으로 강제 변환할 수 있다.
6.1.7 let 함수
let 함수를 안전한 호출 연산자와 함께 사용하면
원하는 식을 평가해서 결과가 널인지 검사하고
결과를 변수에 넣는 작업을 간단하게 처리할 수 있다.
흔한 용례.
fun sendemailTo(email: String) {}
//널이 아닌 값만 인자로 받는 함수
val email: String? = ..
//널이 될 수 있는 값
sendEmailTo(email)
// 에러
if (email != null) sendEmailTo(email)
// 인자를 넘기기전에 널 검사 필요하지만 let 함수를 통해 가능하게 함
foo?.let { .. }
// foo가 null이 아닐 경우 람다 안에서 널이 아닌 값으로 사용된다.
// foo가 null일 경우 아무 일도 일어나지 않는다.
email?.let { email -> sendEmailTo(email) }
email?.let { sendEmailTo(it) }
6.1.8 나중에 초기화할 프로퍼티
코틀린에서는 기본적으로 생성자 안에서 프로퍼티를 초기화하고,
클래스 안의 널이 될 수 없는 프로퍼티는
반드시 생성자 안에서 널이 아닌 값으로 초기화 해야 한다.
객체 인스턴스를 일단 생성한 다음에 나중에 초기화하고 싶다면?
생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화 하고 싶다면?
초기화 값에 null을 주고 싶다면 널이 될 수 있는 타입을 사용할 수 밖에 없다.
널이 될 수 있는 타입을 사용한다면 모든 프로퍼티 접근에 널 검사를 넣거나 !! 연산자를 써야한다.
class Myservice { fun performAction(): String = "foo" }
class MyTest {
private var myService: MyService? = null
// null로 초기화하기 위해 널이 될 수 있는 타입 사용
@Before fun setUp() {
myService = MyService()
// setUp 메소드 안에서 진짜 초깃값을 지정한다.
}
@Test fun testAction() {
Assert.assertEquals("foo", myService!!.performAction())
// 반드시 !!나 ?를 써야 한다.
}
}
널이 될 수 있는 타입을 사용하지 않고도
lateinit 변경자를 사용해서 프로퍼티를 나중에 초기화 할 수 있다.
class Myservice {
fun performAction(): String = "foo"
}
class MyTest {
private lateinit var myService: MyService
// lateinit을 사용해 나중에 초기화
@Before fun setUp() {
myService = MyService()
}
@Test fun testAction() {
Assert.assertEquals("foo", myService.performAction())
// 널 검사를 수행하지 않고 myservice 사용.
}
}
lateinit 프로퍼티는 항상 var 여야 한다.
val은 final 필드로 컴파일 되고, 반드시 생성자 안에서 초기화해야 한다.
6.1.9 널이 될 수 있는 타입 확장
널이 될 수 있는 타입의 확장 함수는 안전한 호출(.?) 없이도 호출 가능하다.
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) {
// 안전한 호출(.?) 없이도 가능
println("Please fill in the required fields")
}
}
또한 null을 수신 객체로 전달해도 아무런 예외가 발생하지 않는다.
verifyUserInput(null)
// null을 수신 객체로 전달해도 예외 발생 안함
// Please fill in the required fields
자바에서 메소드가 정상 실행 된다면 그 메소드의 this는 항상 널이 아니다.
fun String?.isnullOrBlank() : Boolean = // 널이 될 수 있는 String의 확장. this == null || this.isBlank() // this가 null이 될 수 있다.
fun String?.isnullOrBlank() : Boolean =
// 널이 될 수 있는 String의 확장.
this == null || this.isBlank()
// this가 null이 될 수 있다.
하지만 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서는 this가 널이 될 수 있다.
6.1.10 타입 파라미터의 널 가능성
fun <T> printHashCode(t: T) {
// null이 될 수 있는 타입 T
println(t?.hashCode())
}
<T>에 대해 추론한 타입은 널이 될 수 있는 Any? 타입이다.
<T: Any?>와 같음
fun <T: Any> printHashCode(t: T) {
// null이 될 수 없는 타입 T
println(t.hashCode())
}
: Any 를 붙여주면 널이 될 수 있는 값을 넣었을때 컴파일 되지 않는다
6.1.11 널 가능성과 자바
자바 타입은 코틀린에서 널 가능성을 어떻게 다룰까?
→자바의 애노테이션을 코틀린으로 가능
자바 @Nullable String는 코틀린의 String?와 같게 취급한다.
자바의 @NotNull String는 코틀린의 String와 같게 취급한다.
코틀린이 이해 할 수 있는 널 가능성 애노테이션
-JSR-305 표준
-안드로이드
-젯브레인스 도구들이 지원하는 애노테이션
그 외 코틀린이 널 관련 정보를 알 수 없는 자바의 타입은 플랫폼 타입이 된다.
플랫폼 타입에 대해 수행하는 모든 연산의 책임은 우리에게.
플랫폼 타입의 값은 경고를 표시하지 않는다.
널 가능성 애노테이션이 없는 자바 클래스에
널 검사 없이 널인 자바 클래스에 접근한다면?
NullPointerException이 아니라 좀 더 자세한 예외가 발생한다.
파라미터 값 검사가 함수 내부에서 파라미터를 사용하는 시점이 아니라 함수 호출 시점에 바로 이루어지기 때문.
잘못된 인자로 함수를 호출해도 그 인자가 여러 함수에 전달되어 엉뚱한 위치에서 예외가 발생하는게 아니라 함수를 호출하자마자 예외가 발생하므로 가능한한 빨리 예외가 발생해서 원인을 파악하기가 쉽다.
널 검사를 통해 자바 클래스 접근?
fun yellAtSafe(person: Person) {
println((person.name ?: "Anyone").toUpperCase() + "!!!")
}
→널을 반환할수도 있는 메소드에 대한 널 검사를 추가하자. ( ?: )
코틀린이 플랫폼 타입을 도입한 이유.
모든 자바 타입을 널이 될 수 있는 타입으로 다루고 검사를 전부 하게 된다면,
필연적으로 결코 널이 될 수 없는 값들에 대해서도 불필요한 널 검사가 들어가게 되는데
특히 제네릭을 다룰때 이런 경우가 더 많으므로 널 안정성보다 검사에 드는 비용이 더 크다고 판단해 자바의 타입을 가져온 프로그래머에게 그 타입을 제대로 처리 할 책임을 부여했다..!
플랫폼 타입은 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느쪽으로든 사용할 수 있다.
val s: String = person.name
val s1: String? = person.name
//자바 프로퍼티를 어느 타입으로도 볼 수 있음
상속
코틀린에서 자바 메소드를 오버라이드 할 때
자바 클래스나 인터페이스를 코틀린에서 구현할 경우
널 가능성을 제대로 처리하는 일이 중요하다.
→ 널이 될 수 있는 타입? 널이 될 수 없는 타입?
널이 될 수 없는 타입으로 선언할 경우 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어준다.
자바코드가 그 메소드에게 널 값을 넘기면 이 단언문(!!)이 발동 되어 그대로 실행되기 때문에 예외가 발생한다.
6.2 코틀린의 원시(Primitive) 타입
자바는 원시(Primitive) 타입과 참조 타입을 구분한다.
원시 타입에는 메소드를 호출하거나 컬렉션에 원시 타입 값을 담을 수 없다.
자바는 참조 타입이 필요한 경우 특별한 래퍼 타입으로 원시 타입 값을 감싸서 사용한다
ex)
int → Integer
Collection<Integer>
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용한다.
Collection<Int>
코틀린에서는 숫자 타입 등 원시 타입의 값에 대해 메소드를 호출 할 수 있다.
코틀린에서 원시 타입과 참조 타입을 항상 객체로 표현하지는 않는다.
실행 시점에 숫자 타입은 가능한 한 가장 효율적인 방식으로 표현된다.
대부분의 경우 코틀린의 Int 타입은 자바 int 타입으로 컴파일 된다.
이런 컴파일이 불가능한 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우뿐이다.
예를 들어 Int 타입을 컬렉션의 타입 파라미터로 넘기면 그 컬렉션에는 Int의 래퍼 타입에 해당하는 Integer 객체가 들어간다.
6.2.2 널이 될 수 있는 원시 타입: Int?, Boolean? 등
자바에서는 원시 타입의 변수에 null을 대입할 수 없다.
때문에 코틀린에 널이 될 수 있는 타입은 자바 원시 타입으로 표현할 수 없다.
따라서 코틀린에서 널이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 된다.
널이 될 수 있는 타입의 두 값은 직접 비교할 수 없다.
컴파일러는 먼저 두 값이 널이 아닌지 검사해야 비교를 허용한다.
if (age == null || other.age == null) return null
// 선 검사
return age > other.age
// 후 비교
6.2.3 숫자 변환
코틀린에서는 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
대신 직접 변환 메소드를 호출 해야한다.
코틀린에서는 모든 원시 타입에 대한 변환 함수를 제공한다.
(toLong() , toShort(), toChar(), toByte() 등등)
equals 메소드는
자바에서는 객체를 비교하고
코틀린에서는 안에 들어 있는 값을 비교한다.
6.2.4 Any, Any?: 최상위 타입
코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다.
Any? 는 널이 될 수 있는 타입의 조상 타입.
자바는 참조 타입만 Object가 최상위 타입이고 원시 타입은 이러한 계층에 들어있지 않다.
하지만 코틀린에서의 Any는 Int 등의 모든 타입의 조상 타입이다
6.2.5 Unit 타입: 코틀린의 void
코틀린의 Unit 타입은 자바 void 와 같은 기능을 한다.
void와 Unit의 차이점은?
Unit은 타입 인자로 쓸 수 있다
interface Processor<T> {
fun process(): T
}
class NoResultProcessor : Processor<Unit> {
override fun process() { }
}
6.2.6 Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다.
Nothing 타입은 아무 값도 포함하지 않는다.
따라서 Nothing은 함수의 반환 타입이나, 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다.
함수의 반환 타입에 Nothing을 사용하면 정상적으로 끝나지 않는다는 사실을 알려준다.
6.3 컬렉션과 배열
6.3.1 널 가능성과 컬렉션
컬렉션의 타입 인자로 쓰인 타입에도 ?를 사용해 널 여부를 명시할 수 있다.
-List<Int?>는 리스트 안의 각 값이 null이 될 수 있다.
-List<Int>?는 리스트를 가르키는 변수에 널이 들어갈 수 있고 리스트 안에는 널이 아닌 값만 들어간다.
-List<Int?>?는 리스트 안의 각 값이 null이 될 수도 있고 리스트를 가르키는 변수에도 널이 들어갈 수 있다.
6.3.2 읽기 전용과 변경 가능한 컬렉션
코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와
컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다.
읽기 전용 인터페이스
Collection
size, iterator(), contains()
변경 가능 인터페이스
MutableCollection
add(), remove(), clear()
→원소를 추가하거나 제거하는 메소드는 MutableCollection에서!
MutableCollection은 Collection을 확장함
읽기 전용 인터페이스를 기본적으로 사용하기.
코드가 컬렉션을 변경할 필요가 있을 때만 변경 가능한 버전을 사용하기.
→코드를 보고 유추하기 쉬움.
6.3.3 코틀린의 컬렉션과 자바
모든 코틀린의 컬렉션은 자바 컬렉션 인터페이스의 인스턴스이다.
코틀린 컬렉션의 읽기 전용과 변경 가능 인터페이스의 기본 구조는 자바 컬렉션 인터페이스의 구조를 그대로 옮겨 놓았다.
자바메소드를 호출할때 컬렉션을 인자로 넘겨야 한다면 따로 변환이나 복사등의 추가 작업 없이 컬렉션을 그대로 넘기면 된다.
하지만 자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로, 코틀린에서 읽기 전용 컬렉션으로 선언한 객체라도 자바 코드에서는 읽기 전용 컬렉션을 변경할 수 있다.
따라서 코틀린에서 만든 읽기 전용 컬렉션을 자바 코드에게 넘겨야 할때는 주의해야 한다~
6.3.4 컬렉션을 플랫폼 타입으로 다루기
플랫폼 타입의 경우 널 관련 정보가 없기 때문에 컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느 쪽으로든 사용할 수 있게 허용한다.
마찬가지로 자바쪽에서 선언한 컬렉션 타입의 변수를 코틀린에서는 플랫폼 타입으로 본다.
따라서 코틀린 코드는 그 타입을 읽기 전용 컬렉션이나 변경 가능 컬렉션 어느 쪽으로든 다룰 수 있다.
컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드하려는 경우,
어떤 코틀린 컬렉션 타입으로 표현할 지 결정해야 한다.
이런 선택을 제대로 하려면 자바 인터페이스나 클래스가 어떤 맥락에서 사용되는지 정확히 알아야한다.
보통 자바에서 가져온 컬렉션에 대해 코틀린 구현에서 어떤 작업을 수행해야 할지 검토하면 쉽게 결정을 내릴 수 있다.
6.3.5 객체의 배열과 원시 타입의 배열
코틀린에서 배열 만들기
val a = ArrayOf<Int>(5)
val b = ArrayOfNulls<Int?>(5)
// 모든 원소가 null 임, 원소 타입이 널이 될수 있는 타입이어야 한다
원시타입 배열 만들기
val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0, 0, 0, 0, 0)
코틀린의 원시 타입 배열은 자바 원시 타입 배열로 컴파일 된다.
→배열의 값을 박싱하지 않고 효율적인 방식으로 저장이 됨
'Kotlin' 카테고리의 다른 글
Kotlin in action 7장 연산자 오버로딩과 기타 관례 (0) | 2020.04.23 |
---|---|
Kotlin in action 5장 람다로 프로그래밍 (0) | 2020.04.09 |