Any와 Nothing 클래스Null 가능 참조null은 에러를 유발함null 가능 타입 사용하기safe-call 연산자elvis operator사용해서는 안될 안전하지 않은 확정 연산자 !!when의 사용Platform types타입 체크와 캐스팅타입 체크is 사용하기스마트 캐스트명시적 타입 캐스팅제네릭타입 불변성공변성(covariance) 사용하기반공변성(contravariance) 사용하기where를 사용한 파라미터 타입 제한스타 프로젝션구체화된 타입 파라미터reified
코틀린은 디자인 바이 컨트랙트(Design By Contract) 접근방식으로 개발자는 함수나 메소드가 null 을 받거나 리턴할 수 있는지 명확하게 표현할 수 있으며, 그 시점도 알 수 있다.
만약에 참조가 null 이 될 수 있다면 참조하고 있는 객체의 속성이나 메서드를 사용할 땐 언제나 null 체크를 하도록 강제한다. 코틀린은 이런식으로 코드를 안전하게 만든다. 그리고 안전한 코드는 디버깅과 서비스 중에 일어나는 오류를 막아준다.
그리고 코틀린은 null에 사용 가능한 연산자를 몇 가지 제공한다. 그런 연산자를 사용하면 null일 가능성이 있는 참조를 다룰 때 코드에 혼란이 적어진다. 이 기능의 더 특별한 기능은 이런 체크가 모두 컴파일 타임 시간에 이루어지고 바이트코드에는 아무것도 추가되지 않는다는 점이다.
Any와 Nothing 클래스
- 코틀린의 Any 클래스는 Java의 Object에 대응되는 클래스라고 볼 수 있음
- 비록 Any가 Java 바이트코드에서 Object에 매칭되지만, Any와 Object가 동일한 것은 아님. Any는 확장함수를 통해 특별한 메소드들을 제공
- Java와 같은 언어에서는 리턴이 없는 메서드에 void를 사용. 코틀린에서는 표현식이 리턴을 하지 않을때 void 대신 Unit을 사용.
- Nothing을 메서드의 리턴타입으로 사용한다면, 그건 해당 함수가 절대로 리턴을 하지 않는다는 이야기이다. 함수호출은 예외만 발생시킨다.
- 예외는 Nothing 타입을 대표한다.
Null 가능 참조
null은 에러를 유발함
fun nickName(name: String): String { if (name == "William") { return "Bill" } return null // Error }
- 코틀린은 참조타입이 null 불가인 곳에 null을 리턴하려고 하면 컴파일 오류를 냄
- 위의 경우 반환 타입이 String 이어야 하는데 null을 반환하여 error를 발생
null 가능 타입 사용하기
- null 가능 타입은 타입 이름 뒤에 ? 가 붙음
- String → null 불가 타입. String? → null 가능 타입
null 가능 타입의 바이트코드 맵핑
JVM에는 직접 표시할 수 없으므로 null 가능 타입은 대응되는 null 불가 타입으로 대체됨.
코틀린 컴파일러는 이 메타 인스트럭션을 컴파일 시간에 체크하기 때문에 실행 시간에는 성능적 오버헤드가 없음
safe-call 연산자
fun nickName(name: String?): String? { if (name == "William") { return "Bill" } if (name != null) { return name.reserved() } return null } // safe-call 연산자 사용 fun nickName(name: String?): String? { if (name == "William") { return "Bill" } return name?.reserved() }
- ? 연산자를 이용하면 메소드 호출 또는 객체 속성 접근과 null 체크를 하나로 합칠 수 있음
- 참조가 null 일 경우 세이프 콜 연산자의 결과는 null임
elvis operator
- 세이프 콜 연산자는 타깃이 null 일 경우 null 을 리턴하는데, null 이 아닌 다른 것을 리턴해주고 싶을때! 사용하는 것이 엘비스 연산자임
fun nickName(name: String?): String { if (name == "William") { return "Bill" } val result = name?.reserved()?.toUpperCase() return if (result == null) "Joker" else result } // 위의 두 줄으르 아래와 같이 한줄로 변경 가능. return name?.reversed()?.toUpperCase() ?: "Joker"
- 눈(:) 위로 엘비스의 헤어스타일(?)이 보이는가. 이 연산자의 이름이 엘비스인 이유임
사용해서는 안될 안전하지 않은 확정 연산자 !!
- 사용하지 마라.
- null 이 아니라고 확신했는데 null 넘어가면? NullPointerException 나온다
when의 사용
- null 가능 참조로 작업을 할 때 참조의 값에 따라서 다르게 동작하거나 다른 행동을 취해야 한다면 ?. 이나 ?: 보다는 when을 사용하는 것을 고려해보자.
fun nickName(name: String?): String { if (name == "William") { return "Bill" } return name?.reversed()?.toUpperCase() ?: "Joker" } fun nickName(name: String?) = when (name) { "William" -> "Bill" null -> "Joker" else -> name.reversed().toUpperCase() }
- 위와 같이 함으로써 더 명확하게 코드 작성이 가능하다
Platform types
[ 공식문서 참고 ]
T! ↔ T or T?
Java에서 사용되는 변수는 Kotlin에서 표현할 방법이 없다. null 일수도 null 이 아닐수도 있으니
그래서 이를 표현하기 위해 Platform Types를 도입한 것
타입 체크와 캐스팅
권장사항
- 가능한 스마트 캐스트를 사용하라
- 스마트 캐스트가 불가능한 경우에만 안전한 캐스트 연산자를 사용하라
- 애플리케이션이 불타거나 무너지는걸 보고 싶다면 안전하지 않은 캐스트 연산자 사용해라(절대 쓰지마)
타입 체크
- 그게 기능인지 결함인가? 는 실행 시간 타입 체크에 대한 끝없는 논쟁임. 가끔 객체의 타입을 체크하는 것은 필수적이지만, 확장성의 측면에서 봤을 때 타입체크는 최소한으로만 해야 함
- 임의의 타입을 체크하는 행위는 새로운 타입을 추가 했을 때 코드를 부서지기 쉽게 만들고 개방-폐쇄 원칙을 위배하기 때문에 (Java의
instanceof
)
is 사용하기
class Animal { override operator fun equals(other: Any?) = other is Animal }
- is 연산자는 객체가 참조로 특정 타입을 가리키는지 확인함. 위 코드에서는 other가 Animal 클래스인지 확인
- is 연산자를 부정과 함께 사용 → other !is Animal
- 코틀린에서 is, !is 를 사용하면 캐스팅을 공짜로 할 수 있다.
스마트 캐스트
@Override public boolean equals(Object other) { if (other instanceof Animal) { return age == ((Animal) other).age; } return false; }
- Java에서는 age 속성에 접근하기 위해 other이 Animal인지 확인 후, 캐스팅을 직접 해주어야 함
class Animal(val age:Int) { override operator fun equals(other: Any?): Boolean { return if (other is Animal) age == other.age else false } } // otehr is Animal && age == other.age로 바꿀 수 있음
- other.age라고 바로 사용이 가능함. if 문 전에 other.age로 접근하면 컴파일 오류
- 하지만 is 연산자로 체크를 했기에 캐스트를 직접 할 필요가 없다
명시적 타입 캐스팅
- 명시적 타입캐스팅은 컴파일러가 타입을 확실하게 결정할 수 없어 스마트 캐스팅을 하지 못할 경우에만 사용
- 예를 들어 var 변수가 체크와 사용 사이에서 변경되는 경우
fun fetchMessage(id: Int): Any = if (id == 1) "Record found" else StringBuilder("data not found") // 이렇게 하면 StringBuilder가 반환될 경우 ClassCastException 발생 for (id in 1..2) { println("Message length: ${(fetchMessage(id) as String).length}" } // 반면 as? 는 null 가능 참조 타입을 결과로 가짐 val message: String = fetchMessage(1) as String val message: String? = fetchMessage(1) as? String // 최종 결과 println("Message length: ${(fetchMessage(id) as? String).length ?: "---"}")
- as 연산자는 캐스팅이 실패하면 죽는데 반해 안전한 캐스트 연산자인 as? 는 캐스팅이 실패하면 null을 할당함
제네릭
- 타입 T 이외에도 공변성(covariance)을 허용해주길 원할 때가 있다
- ↔ 유저가 컴파일러에게 파라미터 타입 T의 자식 클래스도 사용하도록 허용해달라
- Java에서 <? extends T> 문법을 사용해 공변성을 사용
- 반공변성(contravariance)
- ↔ 파라미터 타입 T의 부모 클래스를 타입 T가 필요한 자리에서 쓸 수 있도록 한다
- Java → <? super T>
타입 불변성
- 메소드가 클래스 T의 객체를 받을 때, T 클래스의 자식이라면 어떤 객체든 전달할 수 있다. 예를 들어 Animal 의 인스턴스를 전달할 수 있다면, Animal의 자식 클래스인 Dog의 인스턴스 역시 전달 가능
- 하지만 메서드가 타입 T의 제네릭 오브젝트를 받는다면(예 : List<T>) T의 파생 클래스를 전달할 수 없다.
- 예로, List<Animal>을 전달할 수 있지만, Dog extends Animal 이라도 List<Dog> 전달이 불가능 ↔
타입 불변성
fun receiveFruits(fruits: Array<Fruit>) { println("Number of fruits : ${fruits.size}") } val bananas: Array<Banana> = arrayOf() receiveFruits(bananas) //ERROR: type mismatch
- Array<Banana> 가 Array<Fruit>을 인자로 받는 메소드에 전달이 가능하다면 메서드 내부에서 Orange를 Array<Fruit>에 담게 될 때 문제가 발생. 이런 상황에서 Array<Banana>를 처리할 때 우리가 Orange를 Banana로 취급을 하게 되면서 캐스팅 예외가 발생함
- 코틀린은 Banana가 Fruit을 상속받았더라도 Array<Banana>를 Array<Fruit>으로 취급해서 전달하는 것을 막아서 제네릭을 타입 안정적으로 만들었다.
공변성(covariance) 사용하기
fun copyFromTo(from: Array<Friut>, to: Array<Fruit>) { for (i in 0 until from.size) { to[i] = from[i] } }
- from 파라미터는 파라미터의 값을 읽기만 하기 때문에 Array<T>의 T에 Fruit 클래스나 Fruit 클래스의 하위 클래스가 전달되더라도 아무 위험이 없다. 이런 것을 타입이나 파생 타입에 접근하기 위한 파라미터 타입의 공변성이라고 이야기함
- Fruit의 자식 클래스들을 전달 가능하게 만들기 위해
from: Array<out Fruit>
문법을 사용
fun copyFromTo(from: Array<Friut>, to: Array<out Fruit>) { for (i in 0 until from.size) { to[i] = from[i] } }
- 코틀린은 from 레퍼런스에 data가 새로 들어가게 하는 메소드 호출이 없다는 사실을 확인하고 메서드 시그니처가 호출되는 것을 확인하여 이를 검증함
from[i] = Fruit() // ERROR from.set(i, to[i]) // ERROR
- from에서는 읽기만 하고 to에 값을 설정하는 경우에만 from 파라미터 위치에 Array<Banana>, Array<Orange>, Array<Fruit>을 전달할 수 있음
- Array<T> 클래스는 T 타입의 객체를 읽고, 쓰는 메서드 모두를 가지고 있다. Array<T>를 사용하는 모든 함수는 읽고, 쓰는 메서드 모두를 사용 가능. 하지만 공변성을 사용하기 위해 우리가 코틀린 컴파일러에게 Array<T> 파라미터에 어떤 값도 추가하거나 변경하지 않겠다는 약속을 해야 함.
- 이런 제네릭 클래스를 사용하는 관점에서 공변성을 이용하는 것을
사용처 가변성
(use-site variance) 혹은타입 프로젝션
이라 부름
반공변성(contravariance) 사용하기
<in T>
: 이것으로 정의되면 전체적으로 파라미터 타입을 받을 수만 있고 리턴하거나 다른 곳으로 보낼 수는 없는 반공변성으로 특정된다. val things = Array<Any>(3) { _ -> Fruit() } val bananaBasket = Array<Banana>(3) { _ -> Banana() } copyFromTo(bananaBasket, things) // ERROR : type mismatch fun copyFromTo(from: Array<out Fruit>, to: Array<in Fruit>) { for (i in 0 until from.size) { to[i] = from[i] } }
where를 사용한 파라미터 타입 제한
fun <T> useAndClose(input: T) { input.close() // ERROR : unresolved reference: close } fun <T: AutoCloseable> useAndClose(input: T) { input.close() } // 여러 개의 제약 조건을 넣고 싶을 때 where 절 사용 fun <T> useAndClose(input: T) where T: AutoCloseable, T: Appendable { input.append("there") input.close() }
스타 프로젝션
Java에서 함수가 모든 타입의 제네릭 객체를 받아서 읽기전용으로 사용할 수 있도록 만들기 위해 ? 를 사용
파라미터 타입을 정의하는 스타 프로젝션
<*>
은 제네릭 읽기 전용 타입과 raw 타입을 위한 코틀린의 기능스타 프로젝션은 타입에 대해 정확히는 알 수 없지만 타입 안정성을 유지하면서 파라미터를 전달할 때 사용. 읽는 것만 허용하고 쓰는 것은 허용되지 않음
fun printValues(values: Array<*>) { for (value in values) { println(value) } // values[0] = values[1] // ERROR }
- 스타 프로젝션
<*>
은out T
와 동일하지만 더 간결하게 작성할 수 있다.
- 스타 프로젝션이 선언처 가변성에서
<in T>
로 정의된 반공분산으로 사용된다면in Nothing
을 사용한 것과 같아짐
구체화된 타입 파라미터
Java에서 제네릭을 사용할 때 Class<T>를 함수에 파라미터로 전달해야 하는 냄새나는 코드를 볼 수 있다. 제네릭 함수에서 특정 타입이 필요하지만 Java의 타입 이레이져 때문에 타입 정보를 잃어버릴 경우 필수적으로 따라옴
코틀린은 구체화된 타입 파라미터(
reified
)를 이용해서 악취를 제거abstract class Book(val name: String) class Fiction(name: String) : Book(name) class NonFiction(name: String) : Book(name) val books: List<Book> = listOf( Fiction("Moby Dick"), NonFiction("Learn to Code"), Fiction("LOTR")) fun <T> findFirst(books: List<Book>, ofClass: Class<T>): T { val selected = books.filter { book -> ofClass.isInstance(book) } if (selected.size == 0) { throw RuntimeException("Not Found") } return ofClass.cast(selected[0]) } println(findFirst(books, NonFiction::class.java).name)
reified
inline fun <reified T> findFirst(books: List<Book>): T { val selected = books.filter { book -> book is T } if (selected.size == 0) { throw RuntimeException("Not Found") } return selected[0] as T }
- 함수를
inline
으로 선언하고 파라미터 타입 T를reified
로 선언함으로써 Class<T> 파라미터를 제거, 함수 안에서 T를 타입 체크와 캐스팅용으로 사용 가능하게 됨