- 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
- 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
- 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계
코틀린 타입 시스템은 널이 될 수 있는 타입을 명시적으로 지원한다
-> null을 허용
널과 문자열을 인자로 받을 수 있게 하려면 타입 이름 뒤에 물음표(?)를 명시해야 한다
Type? = Type or null
널이 될 수 있는 타입인 변수에 대해 변수.메소드()처럼 직접 호출할 수 없다
널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없다
널이 될 수 있는 타입의 값을 널이 될 수 없는 타입의 파라미터를 받는 함수에 전달할 수 없다
실행 시점에 널이 될 수 있는/없는 타입의 객체는 같다
널이 될 수 있는 타입은 감싼 래퍼 타입이 아니다
모든 검사는 컴파일 시점에 수행된다
코틀린의 클래스 안의 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화할 수는 없다
-> 일반적으로 생성자에서 모든 프로퍼티를 초기화해야 한다
lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다
-> 나중에 초기화하는 프로퍼티는 var여야 한다
-> val은 생성자 안에서 반드시 초기화해야하기 때문
널이 될 수 있는 타입에 대한 확장 함수를 정의하면 null 값을 다루는 강력한 도구로 활용 가능
어떤 메소드를 호출하기 전에 수신 객체 역할을 하는 변수가 널이 될 수 없다고 보장하는 대신, 직접 변수에 대해 메서드를 호출해도 확장 함수인 메서드가 알아서 널을 처리해준다
널이 될 수 있는 타입에 대한 확장을 정의하면 널이 될 수 있는 값에 대해 그 확장 함수를 호출할 수 있다.
자바의 경우 메서드 안의 this는 그 메서드가 호출된 수신 객체를 가리키므로 항상 널이 아니다
그러나 코틀린에서는 널이 될 수 있는 타입의 확장 함수 안에서 this가 널이 될 수 있다
let을 사용할 때 수신 객체가 널이 아닌지 검사하고 싶다면
안전한 호출 연산인 ?. 을 사용해야 한다
코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 널이 될 수 있다
널이 될 수 있는 타입을 포함하는 어떤 타입이라도 타입 파라미터를 대신할 수 있다
-> 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 이름 끝에 물음표가 없더라도 T가 널이 될 수 있는 타입이다
// 널이 될 수 있는 타입 파라미터 다루기
fun <T> printHashCode(t: T) {
println(t?.hashCode()) // "t"가 null이 될 수 있으므로 안전한 호출 써야함
}
>>> printHashCode(null) // "T"의 타입은 "Any?"로 추론된다
null
// 타입 파라미터에 대해 널이 될 수 없는 상한을 사용하기
fun <T: Any> printHashCode(t: T) { // 이제 "T"는 널이 될 수 없는 타입
println(t.hashCode())
}
>>> printHashCode(null) // 널이 될 수 없는 타입의 파라미터에 널을 넘길 수 없다
Error: Type parameter bound for `T` is not satisfied
>>> printHashCode(42)
42
Int, Boolean, Any 등의 원시 타입에 대해 살펴본다
코틀린은 원시 타입과 래퍼 타입을 구분하지 않는다
원시 타입의 변수(int 등)에는 그 값이 직접 들어간다
참조 타입의 변수(String 등)에는 메모리상의 객체 위치가 들어간다
자바는 참조 타입이 필요한 경우 특별한 래퍼 타입으로 원시 타입 값을 감싸서 사용한다
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용한다
즉, 자바처럼 Integer와 int로 구분되지 않고 Int 하나만 존재한다
코틀린의 타입은 컴파일 시 자바의 primitive 또는 wrapper 타입으로 자동 변환된다
/* 코틀린 null이 될 수 있는 Int? 타입 */
val price: Int? = null
/* 자바로 변환된 형태 */
Integer price = (Integer)null;
코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다
-> 결과 타입이 허용하는 숫자의 범위가 원래 타입의 범위보다 넓은 경우조차도 자동 변환은 불가능하다
-> 직접 변환 메소드를 호출해야 한다
코드에서 동시에 여러 숫자 타입을 사용하려면 예상치 못한 동작을 피하기 위해 각 변수를 명시적으로 변환해야 한다
숫자 리터럴을 사용할 때는 보통 변환 함수를 호출할 필요가 없다
ex) 42L or 42.0f
코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다
원시 타입 값을 Any 타입의 변수에 대입하면 자동으로 값을 객체로 감싼다
val answer: Any = 42 <- Any가 참조 타입이기 때문에 42가 박싱된다
박싱이란 원시타입->래퍼타입으로 변환하는 것,
언박싱이란 래퍼타입->원시타입으로 변환하는 것을 의미
코틀린 Unit 타입은 자바 void와 같은 기능을 한다
반환 타입 선언 없이 정의한 블록이 본문인 함수와 같다
fun f(): Unit { }
fun f() { }
interface Processor<T> {
fun process(): T
}
class NoResultProcessor : Processor<Unit> { // Unit을 반환하지만 타입을 지정할 필요는 없다.
override fun process() {
// do stuff
} //여기서 따로 return을 명시할 필요가 없다.
}
------------------ Unit의 쉬운 이해 ---------------------
/* Unit 반환 타입 명시 */
fun init() : Unit { ... }
/* Unit 반환 타입 생략 */
fun init() { ... }
/* Unit 타입을 인자로 사용 */
class NoResultProcessor : Processor<Unit> {
override fun process() { //Unit을 반환하지만 반환 타입을 생략해도 됌
...
}
}
코틀린에는 반환 값이라는 개념 자체가 의미 없는 함수가 일부 존재
Nothing 타입은 아무 값도 포함하지 않는다
Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다
-> 그 외의 다른 용도로 사용하는 경우 Nothing 타입의 변수를 선언하더라도 그 변수에 아무 값도 저장할 수 없으므로 의미 없다
Nothing type은 이 함수가 정상적으로 끝나지 않는다는 걸 명시적으로 표현하는 타입
예를 들면,
함수 중간에 throw 시키도록 한다면 해당 함수는
정상적으로 끝나지 않는 함수가 되겠죠?
이런 함수의 반환 type을 Nothing으로 하면
컴파일러가 정상 종료되지 않는 함수임을 미리 알 수가 있습니다
Nothing 함수는 엘비스 연산자의 우항에 사용해서 전제 조건을 검사하는 데에 주로 사용됩니다.
경우에 따라 널이 될 수 있는 값으로 이뤄진 널이 될 수 있는 리스트를 정의해야 할 수도 있다
-> List<Int?>?
로 표현
-> 이런 리스트를 처리할 때는 변수에 대해 널 검사 수행 후 리스트에 속한 모든 원소에 대해 널 검사 수행해야 한다
널이 될 수 있는 값으로 이뤄진 컬렉션으로 널 값을 걸러낼 때
-> filterNotNull 함수 사용
-> 컬렉션 안에 널이 들어있지 않음을 보장해주므로
List<Int>
으로 사용 가능
가능한 항상 읽기 전용 인터페이스를 사용하는 것을 일반적인 규칙으로 삼자
읽기 전용 컬렉션이 항상 스레드 안전 하지는 않다
컬렉션 타입 | 읽기 전용 타입 | 변경 가능 타입 |
---|---|---|
List | listOf | mutableListOf, arrayListOf |
Set | setOf | mutablesetOf, hashSetOf, linkedSetOf, sortedSetOf |
List | mapOf | mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf |
코틀린에서 배열을 만드는 방법
- arrayOf 함수에 원소를 넘기면 배열을 만들 수 있다
- arrayOfNulls 함수에 정수 값을 인자로 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다
물론 원소 타입이 널이 될 수 있는 타입인 경우에만 이 함수를 쓸 수 있다- Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화해준다
arrayOf를 쓰지 않고 각 원소가 널이 아닌 배열을 만들어야 하는 경우 이 생성자를 사용한다
// 컬렉션을 vararg 메소드에게 넘기기
>>> val strings = listOf("a", "b", "c")
// vararg 인자를 넘기기 위해 스프레드 연사자를 사용
>>> println("$s/$s/$s".format(*strings.toTypedArray())
a/b/c
다른 제네릭 타입에서처럼 배열 타입의 타입 인자도 항상 객체 타입이 된다
코틀린은 원시 타입의 배열을 표현하는 별도 클래스(IntArray , ByteArray , CharArray , BooleanArray등)를 각 원시 타입마다 하나씩 제공한다
원시 타입의 배열을 만드는 방법
- 각 배열 타입의 생성자는 size 인자를 받아서 해당 원시 타입의 디폴트 값(보통은 0)으로 초기화된 size 크기의 배열을 반환한다
- 팩토리 함수는 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다