[Kotlin in Action] 6장 코틀린 타입 시스템
다루는 내용
- 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
- 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
- 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계
가독성을 위해 널이 될 수 있는 타입과 (nullable type) 읽기 전용 컬렉션이 있다.
배열은 삭제했다.
널 가능성
널 가능성 (nullability)는 NPE를 피할 수 있게 돕기 위한 코틀린 타입 시스템의 특성이다. 코틀린 비롯 최신 언어들은 컴파일 시점에 null을 다룰려한다. 컴파일 시 미리 감지해 실행 시점에 발생하는 NPE를 줄이자는 것이다.
널이 될 수 있는 타입
널 가능, 불가능을 명시적으로 구분한다. 코틀린은 널이 될 수 있는 변수에 대해 메소드를 호출하면 NPE 발생 가능하다. 코틀린은 그런 메소드 호출을 금지한다.
// 자바
int strLen(String s) {
return s.length();
}
인자로 널이 들어올 수 있으니 좀 위험해.
코틀린으로 써보자면 가장 먼저 알야하는게 '이 함수가 널을 인자로 받을 수 있는가?' 이다. 널이 들어올 수 없다면
fun strLen(s: String) = s.length
strLen에 널이거나 널이 될 수 있는 인자를 넘기는 것은 금지되며, 컴파일 시 잡힌다.
널을 받을 수 있게 하려면 타입 이름 뒤에 ?를 명시해야한다.
fun strLenSafe(s: String?) = ...
널이 될 수 없는 타입의 변수에 널이 될 수 있는 값을 대입할 순 없다.
널이 될 수 없는 타입의 파라미터를 받는 함수에 널이 될 수 있는 타입의 값을 전달할 순 없다.
이렇게 제약이 많다면 널이 될 수 있는 경우 할 줄 아는게 뭐냐? 바로 null과 비교하는 것이다. null과 비교하고 나면 컴파일러는 이를 기억하고 null이 아님이 확실한 영역에서는 해당 값을 널이 될 수 없는 타입처럼 사용한다.
// if로 null 검사
fun strLenSafe(s: String?): Int = if (s != null) s.length else 0
if로만 null 체크하는건 아니다.
타입의 의미
double은 수학적 자료형으로 관련 연산을 잘 수행할 것을 컴파일로부터 보장 받을 수 있다. String은 문자열도 받을 수 있지만 null도 받을 수 있다. 둘은 전혀 다른 의미이고 instanceof 에 null이 String이 아니라 답하는데. 이런 자바의 타입 시스템이 널을 제대로 다루지 못한다는 뜻이다. 널 체크를 하지 않고도 그냥 실행해 NPE를 발생시킬 수 있다.
자바에도 NPE를 해결하기 위한 도구가 있다. @Nullable이나 @NotNull이 있다. 그래도 명쾌하지 않고, null 값을 코드에서 절대 쓰지 않는 방법이 있다. 자바8에 도입된 Optional 타입 등의 널을 감싸는 래퍼 타입을 사용한다. 이는 어떤 값이 정의되거나 정의도지 않을 수 있음을 표현하는 타입이다. 몇 가지 단점이 있는데 코드가 더 지저분해지고 래퍼가 추가됨에 따라 실행시점에 성능이 저하되며 전체 에코시스템에서 일관성 있게 활용하기 힘들다. 우리가 작성한 코드에서 Optional을 사용하더라도 여전히 JDK 메소드나 외부에서 반환되는 null은 처리를 해야한다.
?* 래퍼가 추가됨에 따라 실행 시점이 저하된다는건 기존 인스턴스를 감싸는 새로운 인스턴스라서 그 생성 비용을 말하는건가?
코틀린은 널 가능, 불가능 타입을 구분해 각 값에 어떤 연산을 할 수 있는지 명확히 이해하고 실행 시점에 예외를 발생시킬 수 있는 연산을 판단한다. 따라서 그러한 연산을 아예 금지시킬 수 있다.
노트
실행 시점에 널이 될 수 있는 타입과 그렇지 않은 타입의 객체는 같다. 널이 아님을 체크한 객체를 말하나? 널이 될 수 있는 타입은 불가능한 타입을 감싼 래퍼 타입이 아니다. 모든 검사는 컴파일 시점에 수행되어 코틀린에서 널이 될 수 있는 타입을 처리하는 데 별도의 실행 시점 부가 비용이 들지 않는다. 옵셔널은 실행 중에 체크를 하나? isEmpty 그런걸로
안전한 호출 연산자: ?.
코틀린이 제공하는 가장 유용한 도구 중 하나가 안전한 호출 연산자 ?.이다. null 검사와 메소드 호출을 한 번의 연산으로 수행한다. if때기 필요 없어서 참 좋아.
if (s != null) s.toUpperCase() else null과 같다.
호출하려는 값이 null이 아니라면 ?.은 일반 메소드 호출처럼 작동한다. 호출하려는 값이 null이면 이 호출은 무시되고 null 결과값이 된다.
안전한 호출의 결과 타입도 널이 될 수 있음을 유의하라. String.toUpperCase는 String 타입의 값을 반환하지만 s가 널이 될 수 있는 타입의 경우 결과 타입은 String?이다.
프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.
// 안전한 호출 연쇄시키기 이쁘네
val country = this.company?.address?.country
return if (country != null) country else "Unknown"
엘비스 연산자: ?:
널 대신 디폴트 값 지정
fun foo(s: String?) {
val t: String = s ?: ""
코틀린에서는 return이나 throw 등의 연산도 식이다.
좌항이 널이면 함수가 즉시 어떤 값을 반환하거나 예외를 던진다. 이런 패턴은 함수의 전제 조건을 검사히게 유용하다.
fun printShippingLabel(person: Person) {
val address = peron.company?.address
?: throw IllegalArgumentException("No address")
with (address) {
println(streetAddress)
println("$zipCode $city, $country")
안전한 캐스트 as?
as?는 어떤 값을 지정한 타입으로 캐스트한다. as?는 변환 못하면 null 반환.
o as? Person ?: return false
타입이 서로 일치하지 않으면 false 반환 이를 엘비스로 바로 대응한다.
널 아님 단언: !!
강제로 널이 될 수 없는 타입으로 바꾼다.자주 쓰려나?
어떤 줄에서 예외가 생겼는지 정보는 있지만 어떤 식에서 발생한거지는 모르니 한 식에 !! 여러 개 쓰지마 하나만 써.
let 함수
널이 될 수 있는 식을 더 쉽게 다룰 수 있다. 흔한 예는 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우다.
체크 함하고 얘껄 집어넣어.
getTheBestPersonInTheWorld()?.let { sendEmailTo(it.email) }
let 중첩시켜 처리하면 코드가 복잡해져서 알아보기 어려워진다. 그런 경우엔 if 사용해 모든 값 한 번에 검사해.
나중에 초기화할 프로퍼티
나중에 초기화하는 프로퍼티는 항상 var이여야한다.
lateinit var 초기화하지 않고 널이 될 수 없는 프로퍼티로 선언한다.
lateinit 프로퍼티는 의존관계 주입 프레임워크와 함꼐 사용 만ㅇ히한다. 이 값을 DI 프레임워크가 설정해준다.
널이 될 수 있는 타입 확장
널이 될 수 있는 타입에 확장 함수 정의하면 null 값 다루는 강력한 도구된다. 수신객체역할 하는 변수가 널이 될 수 없다 보장하는 대신 직접 변수에 대해 메소드를 호출해도 확장 함수인 메소드가 알아서 널 처리한다. 이런 처리를 확장함수에서만 가능. 일바 ㄴ멤버호출은 객체 인스턴스를 통해 디스패치 되므로 그 인스턴스가 널인지 여부를 검사하지 않는다.
디스패치란 동적타입에 따라 적절한 메소드 호출해주는 방식이다.
널이 될 수 있는 타입의 확장 함수는 안전한 호출 없이도 호출이 가능하다. isNullOrBlank는 명시적으로 검사해 널인 경우 트루 반환하고 아닌 경우 isBlank 호출한다.
타입 파라미터의 널 가능성
코틀린에서 함수나 클래스의 모든 타입 파라미터는 널이 될 수 있다. ?가 없어도 널이 될 수 있따.
Any?로 추정된다. 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한 Upper bound를 지정해야한다.
널 가능성과 자바
플랫폼 타입은 코틀린이 널 관련 정보를 알수 없는 타입이다. 얜 널 안전성 중복 수행해도 경고 표시 안한다.
플랫폼 타입 왜 도입했냐
모든 자바 타입을 널이 될 수 있게 하면 더 안전하지 않냐? 물론 그랟도 되지만 모든 타입을 널이 될 수 있게 하면 될 수 없는 값에도 불필요한 널 검사가 들어간다.
플랫폼 타입이 어떤걸 말하는거지? ?*
자바 프로퍼티를 널이 될 수 있는 타입으로도 볼 수 있고 없는 타입으로도 볼 수 있따.
상속 자바 메소드 오버라이드 할 때 파라미터와 반환 타입 널 여부 지정해야한다.
이거 중요하다.
코틀린의 원시 타입
코틀린은 원시타입 래퍼 타입 구분하지 않는다.
Int, Boolean
코틀린은 Int 타입을 컬렉션의 타입 파라로 넘기면 Integer가 되고 Int 같은 타입에 널 참조 들어갈 수 업어 쉽게 상응 자바 원시로 컴파일된다.
널이 될 수 있는 원시 타입: Int?, Boolean?
널검사 마친 뒤에야 두 값을 일반적인 값처럼 다루게 허용한다. Integer로 저자오딘다.
이렇게 컴파일하는건 JVM에서 제네릭 구현하는 방식이다. 원시타입 허용 안해. 항상 박스타입 써.
숫자 변환
코틀린 자바 큰 차이중 하나는 숫자 변환 방식이다. 코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환 안한다.
int -> long 안해줘. 코틀린 모든 원시 타입에 대한 변환 함수 제공한다.
원시타입 리터럴 숫자리터럴 허용한다.
문자열 숫자로 변환
"42".toInt()
Any, Any?: 최상위 타입
내부에서 Any 타입은 Object로 취급된다. Object에있는 다른 메소드를 Any에서 사용할 순 없다. 그러고 시픔 캐스트해.
콭르린 애니가 Int 등 원시 타입을 포함한 모든 타입의 조상.
Unit : 코틀린 void
void , Unit 차이 뭐야. 코틀린 함수 반환 타입이 유닛이고 ㄴ제네릭 함수를 오버라이드 하지 않으면 내ㅜㅂ에서 자바 보이드함수로 컴파일한다.
그런 코틀린 함수를 자바에서 오버라이드하는 경우 void를 반환으로 해야한다. 다른 점은 Unit은 모든 기능을 갖는 일반적 타입이며 Unit을 타입인자로 쓸 수 있따. 속한 값은 단 하면 이름도 Unit이다. 제네릭 파람을 반환하는 함수를 오버라이드 하며 반환 타입을 Unit을 쓸 때 유용하다.
값없음을 자바에서 해결하려면 별도 인퍼테이스 사용해 경우를 분리해야한다. 여전히 유일한 값 null 반환 위해 적어야한다.
Unit은 전통적으로 단 하나의 인스턴스만 갖는 타입을 의미한다.
Nothing: 이 함수는 결코 정상적으로 끝나지 않는다.
반환값 개념 자체가 의미없는 함수가 존재한다. fail이란 함수 가능. 예외를 던져 실패시키는.. 그런경우를 표현하기 위해 Nohing이 있따. 얘를 엘비스와 같이 사용도 가능?
company.adress ?: fail("No address")
컬렉션과 배열
요약
- 코트린은 널이될 수 있는 타입을 지원해 NPE 오류를 컴파일 시점에 감지 가능
- 안전한 호출, 엘비스 연산자. 단, let을 사용해 더 간결하게 코드 짤 수 있따.
- as? 연산자를 ㅏㅅ용하면 값을 다른 타입으로 변환하는 것과 변환이 불가능한 경우를 처리하는 것을 한 꺼번에 편리하게 처리할 수 있따.
- 코틀린에서는 수를 표현하는 타입이 일반 클래스와 똑같이 생겼고 일반 클래스와 똑같이 도작한다.
- 널이 될 수 있는 원시탑은 자바의 박싱한 원시타입과 대응된다.
- Any 타입은 다른 모든 타입의 조상 타입이며, 자바의 Object에 해당한다. Unit은 자바의 Void와 비슷하다.
- 정상적으로 끝나지 않는 함수의 반환을 위해 Nothing 타입 존재
- 코틀린 컬렉션은 자바 컬렉션 클래스 사용. 더 개선해서 읽기 전용, 변경 가능 컬렉션을 구별해 제공
- 자바 클래스를 코틀린에서 혹장하거나 자바 인터페이스를 코틀린에서 구현하는 경우 메소드 파라미터의 널 가능성과 변경 가능성에 대해 깊이 생각해야한다.
- 코틀리늬 어레이클래스는 일반 제네릭 클래스처럼 보인다. 하지만 자바 배열로 컴파일 된다.
- 원시 타입의 배열은 IntArray와 ㅏㅌ이 각 타입에 대한 특별한 배열로 표현된다.
널 가능성과 컬렉션
List<Int?> 각 원소 가능
List
읽기 전용과 변경 가능 컬렉션
코틀, 자바 컬렉션 나눈 큰 특성은 이거다.
어떤 함수가 뮤터블콜렉션을 인자로 받으면 데이터를 바꾸리라 가정할 수 있따.
어떤 컴포넌트의 내부 상태에 컬렉션이 포함되면 뮤터블 인자로 바든 함수에전달할때 원본 변경을 막기 위해 컬렉션을 복사할 수도 있다. 방어적 복사.