본문 바로가기

코틀린

[Kotlin In Action] 6장. 코틀린 타입 시스템

null 가능성

코틀린에서는 기본적으로 NPE를 방지하기 위해 모든 데이터 타입이 Non-Nullable이다.
코틀린은 널이 될 수 있는 타입과 널이 될 수 없는 타입을 명시적으로 지원한다.

int strLen(String s) {
    return s.length();
}

자바에서는 해당 String에 대한 null 체크를 하지 않기 때문에 runtime 시점에 NPE가 터진다.
코틀린은 null과 관련된 문제를 가능한 실행 시점에서 컴파일 시점으로 옮긴다.

fun strLen(s: String) = s.length
>>> strLen(null)
ERROR: Null can not be a value of a non-null type String

null이 될 수 있는 타입 

  • 타입 이름 뒤에 ?를 붙임
  • 프로퍼티나 변수에 null 가능 타입을 지정
fun strLen(s: String?) = if (s != null) s.length else 0
strLen(null)
strLen("10")

null이 될 수 없는 타입

타입 뒤에 ?가 없으면 null을 가질 수 없는 타입으로 null을 할당하면 컴파일 에러가 발생한다.

안전한 호출 연산자: "?."

null 체크와 메서드 호출을 한 번의 연산으로 수행한다.
호출하는 값이 null이 아니라면 일반 메서드 호출처럼 작동하고, 호출하려는 값이 null이면 호출을 무시하고 null이 결과 값이 된다. 

fun printAllCaps(s: String?) {
  val allCaps: String? = s?.toUpperCase()
  println(allCaps)
}

>>> printAllCaps("abc")
ABC
>>> printAllCaps(null)
null

엘비스 연산자 "?:"

좌한 값이 null이 아니면 좌항 값을, null이면 우항 값을 결과로 한다. 

fun managerName(employee: Employee): String? = employee.manager?.name

>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(developer))
Da Boss
>>> println(managerName(ceo))
null

안전한 캐스트: as?

지정한 타입으로 캐스트하는데, 대상 타입으로 변환할 수 없으면 null을 반환한다.

fun equals(o: Any?): Boolean {
  // val other: Person? = o as? Person
  val other: Person = o as? Person ?: return false
  return ...
}

널 아님 단언: !!

어떤 값이든 널이 될 수 없는 타입으로 바꾼다.
값 null이면 NPE가 발생한다.
조금 TMI인데, 코틀린 설계자들이 컴파일러가 검증할 수 없는 이 !!를 사용하기보다 더 좋은 방법을 찾게 하기 위해 못생긴 이 !! 기호를 사용했다고 한다. 남발하지 말것.

fun ignoreNulls(s: String?) {
  val notNull: String = s!! 
}

let 함수 

자신의 수신 객체를 인자로 전달받은 람다에게 넘긴다. 
null이 될 수 있는 값을 null이 될 수 없는 값만 인자로 받는 함수에 전달할 때 사용한다. 

foo?.let {
    println("This is Not Null")
} ?: println("This is Null")

 

나중에 초기화 할 프로퍼티 : lateinit

null이 될 수 없는 프로퍼티를 생성 시점이 아닌 나중에 초기화할 수 있게 한다.
프로퍼티는 var이어야하며 초기화 전에 접근하면 예외가 발생한다.

class MyTest {
    private lateinit var myService: MyService
    @Before fun setUp() {
        myService = MyService()
    }
}

널이 될 수 있는 타입 확장

널 가능 타입에 확장을 정의하면 널이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있다.
이 때 확장 함수 안에서 this의 null 여부를 검사하므로 안전한 호출을 할 필요가 없다.

fun String?.isNullOrBlank(): Boolean =
this == null || this.isBlank()

str.isNullOrBlank()

타입 파라미터의 널 가능성

타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 ?가 없더라도 T가 널이 될 수 있는 타입이다.

fun <T> printHashCode(t: T) { // t는 Any?로 추론
  println(t?.hashCode()) // t.hashCode()는 컴파일 에러
}
// 타입 파라미터의 상한 지정
fun <T: Any> printHashCode(t: T) { // t는 Any 타입으로 추론
  println(t.hashCode())
}

 

널 가능성과 자바

자바 코드에 널 가능성 어노테이션을 사용하면, 코틀린은 그 정보를 활용한다.

@Nullable String ‐> String?
@NotNull String ‐> String

 

플랫폼 타입

코틀린이 null 관련 정보를 알 수 없는 타입을 뜻한다. 
플랫폼 타입은 개발자가 널이 될 수 있는 타입으로 처리할 수도, 그 반대로도 처리할 수 있다. 
코틀린은 널이 될 수있는 값에 경고를 표하지만 이 플랫폼 타입의 값은 아무 경고도 표시하지 않는다. 
모든 타입의 값에 대해 항상 널 검사를 하는 것은 성가시니 개발자에게 그 책임을 맡긴 것이다. 

TMI를 적자면, 근로를 하다 이 플랫폼 타입을 마주한 적이 있다. 
CSVRecorder라는 라이브러리를 가져다 쓰는데 그 내부를 들여다보면 null이 될 수 있는 반환을 하고 있다.

쨌든 이 덕분에 정리하게 된 플랫폼 타입.

코틀린의 기본 타입

원시 타입

자바에서 원시타입은 값을 직접 넣고, 레퍼런스 타입은 메모리 상의 위치가 들어가게 된다. 
하지만 코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다. 

코틀린은 런타임에 숫자 타입은 가능한 자바의 원시타입으로 컴파일하며,
널 가능 원시 타입은 래퍼 타입으로 컴파일한다. 
또한 제네릭 클래스 경우 래퍼 타입을 사용한다. 

숫자 변환

한 숫자 타입을 다른 타입의 숫자로 자동으로 변환하지 않아 명시적으로 변환해야한다. 
숫자 리터럴은 컴파일러가 필요한 변환을 알아서 처리한다. 

val b: Byte = 1
val l = b + 1L
val l1: Long = 42 // Int 타입 리터럴을 Long으로 컴파일러가 변환
val i1: Int = 42
// val l2: Long = i1 // 컴파일 에러
val l2: Long = i1.toLong()

 

Any, Any?: 최상위 타입

자바의 Object 같은 최상위 타입으로 코틀린에는 Any가 있다. 
Any는 원시 타입을 포함한 널 불가능의 모든 타입의 조상 타입이다. 

Unit 타입

자바의 void와 유사하며 관심 가질 만한 내용을 반환하지 않는 함수의 반환 타입으로 Unit을 사용한다. 
Unit은 void와 달리 타입, Unit이라는 단 하나의 인스턴스만 갖는 타입을 의미한다. 
리턴 타입이 Unit인 함수는 묵시적으로 Unit을 리턴한다.

Nothing 타입

함수가 정상적으로 끝나지 않음을 표현한다. 
아무 값도 포함하지 않으며 함수의 반환 타입이나 반환 타입으로  쓰일 타입 파라미터로만 사용이 가능하다.

fun fail(message: String): Nothing {
  throw IllegalStateException(message)
}

 

컬렉션과 배열

널 가능성과 컬렉션

List<Int?>는 컬렉션 내부에 Int 값 또는 null 값이 저장될 수 있음을 의미한다.
List<Int>?는 컬렉션을 참조하는 변수가 null이 될 수 있음을 의미하며 내부에는 Int 값만 저장할 수 있음을 의미한다.

읽기 전용과 변경 가능한 컬렉션

코틀린은 컬렉션의 인터페이스를 다음과 같이 분리했다.

  • kotlin.collections.Collection : 조회 기능만 제공, 원소를 추가하거나 제거하는 메서드가 없다.
  • kotlin.collections.MutableCollection : 수정 기능도 제공

Collection가 무조건 변경 불가능한 컬렉션이지는 않는다. 
같은 인스턴스를 가리키는 MutableCollection에서 수정을 하는 경우 수정이 가능하기 때문에 항상 Thread Safe 하지는 않다. 

코틀린 컬렉션과 자바

코틀린에서 실제 내부적으로는 자바 구현체를 사용한다.

컬렉션 생성 함수

타입 읽기 전용  변경 가능 타입
List listOf mutableListOf, arrayListOf
Set setOf Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf