널 가능성
- null 이란 아무것도 참조하지 않는 참조값을 말함
- java에서 NullPointException은 runtime에서만 체크가능
- 코틀린은 널 값이 될 수 있는 참조 타입과 널 값이 될 수 없는 참조 타입을 확실하게 구분하여 컴파일 시점에 NullPointException 상당 부분을 막을 수 있음
널이 될 수 있는 타입
- 코틀린은 기본적으로 모든 참조 타입은 널이 될 수 없는 타입
- 코틀린에서 널이 될 수도 있는 값을 받는 함수를 작성하려면 파라미터 타입 뒤에 물음표(?)를 붙여서 타입을 널이 될 수 있는 타입으로 지정해야함
fun isBooleanString(s: String?) = s == "false" || s == "true"
- String? 같은 타입을 nullable type이라함
- ?를 붙이면 널이 될 수 없는 값에 널이 들어 갈 수 있음 하지만 그 반대로 널이 들어갈 수 없는 변수에 널이 될 수 있는 타입의 값을 대입할 순 없음
- 원시 타입도 널이 될 수 있는 타입이 존재하나, 이런 타입은 항상 박싱한 값만 표현함
- 가장 작은 널이 될 수 있는 타입 Nothing?
- 널 상수 이외의 어떤 값도 포함하지 않음
- null 값 자체의 타입, 다른 모든 널이 될 수 있는 타입의 하위 타입
- 가장 큰 널이 될 수 있는 타입은 Any?
- 코틀린 타입 시스템 전체에서 가장 큰 타입
- 널이 될 수 있는 모든 타입과 널이 될 수 없는 모든 타입의 상위 타입
널 가능성과 스마트 캐스트
- 널이 될 수 있는 값을 처리하는 가장 직접적인 방법은 해당 값을 조건문을 사용해 null과 비교하는 것
- 's == null return false'라는 식으로 코드의 흐름 중 한 흐름이 널이고 다른 흐름이 널이 아닌 것을 보장하면 컴파일러는 컴파일 에러를 내지 않고 넘어감
- 컴파일러는 앞의 정보를 활용해 널이 될 수 있는 값을 널이 될 수 없는 값으로 cast함 -> 스마트 캐스트
- 가변 프로퍼티에 대해서는 스마트 캐스트 적용할 수 없음
널 아닌 단언 연산자
- !!연산자는 널 아님 단언(not-null assertion)이라고 부름
- kotilinNullPointException 예외를 발생시킬 수 있는 연산자
- !!연산자가 붙은 식의 타입은 원래 타입의 널이 될 수 없는 버전
안전한 호출 연산자
fun readInt() = readLine?.toInt()
- ?. 연산자
- 수신 객체가 널이 아닌 경우에는 의미 있는 일을 하고, 수신 객체가 널인 경우에는 널을 반환하라
엘비스 연산자
- ?: 연산자
- 이 연산자를 사용하면 null을 대신할 디폴트 값을 지정할 수 있음
fun sayHello(name: String?){
println("Hello, " + (name ?: ""Unknown))
}
- 안전한 연산과 엘비스 연산자를 조합해서 수신 객체가 널일 때의 디폴트 값을 지정하면 유용함
val n = readLine?.toInt() ?: 0
- 표준 입력이 널을 반환할 경우 0을 n에 대입
class Name(val firstName: String, val familyName: String?)
class Person(name: Name?){
fun describe(): String{
val currentName = name ?: return "Unknown"
return "${currentName.firstName} ${currentName.familyNAme}"
}
}
fun main(){
println(Person(Name("John", "Doe")).describe())
println(Person(null).describe())
}
- return이나 throw 같은 제어 흐름을 깨는 코드를 엘비스 연산자 오른쪽에 넣는 방법도 있음
- 우선순위 면에서 엘비스 연산자는 or 등의 중위 연산자와 in, !in 사이에 위치함. 특히 비교/동등성 연산자나 ||, && 대입보다 더 우선순위가 높음
단순한 변수 이상인 프로퍼티
최상위 프로퍼티
- 클래스나 함수와 마찬가지로 최상위 수준에 프로퍼티를 정의할 수 있음
- 이런 경우 프로퍼티는 전역 변수나 상수와 비슷한 역할을 함
- 프로퍼티에 최상위 가시성(public/internal/private) 지정 가능
- 임포트를 통해 최상위 프로퍼티 임포트 가능
늦은 초기화
- 클래스 인스턴스화 할 때 프로퍼티 초기화 -> 번거로울 수 있음 -> 늦은 초기화 가능
- 생성자 에서는 초기화되지 않은 상태라는 사실을 의미하는 디폴트 값(예 null)을 대입, 실제 값을 필요할 때 대입 가능
class Content {
lateinit var text: String
fun loadFile(file: File) {
text = file.readText()
}
}
- lateinit 표시가 붙은 프로퍼티는 값을 읽으려고 시도할 때 프로그램이 프로퍼티 초기화 됐는지 검사해서 초기화되지 않은 경우 UninitializedPropertyAccessException을 던짐
- lateinit을 만들기 위해서 다음과 같은 조건 만족해야함
- 프로퍼티가 코드에서 변경될 수 있는 지점이 여러 곳일 수 있으므로 프로퍼티를 가변 프로퍼티(var)로 정의 해야함
- 프로퍼티의 타입은 널이 아닌 타입어야하고 Int, Boolean 같은 원시 값을 표현하는 타입이 아니어야 함
- lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 값을 바로 대입할 수 없음
커스텀 접근자 사용하기
*프로퍼티 -> 필드 + 접근자, 함수가 내장된 변수
- 코틀린 프로퍼티의 실제 능력은 변수와 함수의 동작을 한 선언 안에 조합할 수 있는 기능에 있음
class Person(val firstName: String, val familyName: String) {
val isAdult: Boolean
get() = this.age >= 20
}
- custom geter로 필드를 쓰는 것 만으로 함수 호출같이 쓸 수 있음
class Person(val firstName: String, val familyName: String) {
val fullName: String
get(): String {
return "$firstName $familyName"
}
get() = $firstName $familyName"
}
}
fun main() {
val person = Person("John", "Doe")
println(person.fullName)
}
- 프로퍼티 값을 읽을 때 사용하는 커스텀 getter 정의
- firstName, familyName과 달리 fullName에 백킹 필드가 없기 때문에 인스턴스에서 메모리 차지 안함
- 백킹필드의 규칙
- 코틀린 파일을 자바 파일로 변환했을 때 자바의 필드
- 프로퍼티에 명시적으로 field를 사용하는 디폴트 접근자나 커스텀 접근자가 하나라도 있으면 백킹필드 생성
- fullName은 프로퍼티 형태인 함수와 같음
class Person(val firstName: String, val familyName: String ) {
val age: Int = age
get(): Int {
println("Accessing age")
return field
}
}
- 백킹 필드에 직접 접근은 field 키워드 사용해서 함
- field를 사용하지 않았을 때 recursive call이 일어날 수 있음
- setter 안에서 또 setter를 호출 함 참고
- 불변 프로퍼티의 접근자는 읽기 접근자 하나
- 가변 프로퍼티의 경우 두가지 접근자가 있음
- 두 접근자 모두 커스텀화하고 두 접근자가 모두 다 field 키워드를 통해 백킹필드를 사용하지 않는 경우를 제외하면 항상 백킹필드가 생김
- 코틀린의 함수보다 프로퍼티 사용하는 권장사항
- 값을 계산하는 과정에서 예외가 발생할 여지가 없을 때
- 값을 계산하는 비용이 충분히 쌀 때
- 값을 캐시해 둘 때
- 클래스 인스턴스의 상태가 바뀌기 전에 여러 번 프로퍼티를 읽을 때
- 함수를 호출해도 항상 똑같은 결과를 내는 경우
출처
코틀린 완벽 가이드