[Kotlin]6장. 코틀린 타입 시스템
6.1.7 let 함수
foo?.let{...}
if(foo != null) // it은 람다 안에서 널이 아니다.
if(foo == null) // 아무 일도 일어나지 않는다.
널이 될 수 있는 값에 대해 안전한 호출 구문을 사용해 let을 호출하되 널이 될 수 없는 타입을 인자로 받는 람다를 let에 전달한다.
여러 값이 널인지 검사해야 한다면 let호출을 중첩시켜서 처리할 수 있다. 하지만 이렇게 하면 코드가 복잡해져 알아보기 어려워진다. 이때 if를 사용해 모든 값을 한꺼번에 검사하는 편이 낫다.
6.1.8 나중에 초기화할 프로퍼티
class MyService{
fun performAction(): String = "foo"
}
class MyTest {
private lateinit var myService: MyService
@BeforeAll fun setUp() {
myService = MyService() // setUp 메소드 안에서 진짜 초깃값을 지정한다.
}
@Test fun testAction(){
assertEquals("foo", myService.performAction()) // 반드시 null 가능성을 신경써야 한다.
}
}
나중에 초기화하는 프로퍼티는 항상 var여야 한다. val 프로퍼티는 final 필드로 컴파일되며, 생성자 안에서 반드시 초기화해야 한다. 나중에 초기화하는 프로퍼티는 널이 될 수 없는 타입이라 해도 더 이상 생성자 안에서 초기화할 필요가 없다. 그 프로퍼티를 초기화하기 전에 프로퍼티에 접근하면 lateinit property myService has not been initialized
이라는 예외가 발생해 NPE보다 좋다.
6.1.9 널이 될 수 있는 타입 확장
널이 될 수 있는 타입에 대한 확장함수를 정의 : 어떤 메소드를 호출하기 전에 수신 객체 역할을 하는 변수가 널이 될 수 없다고 보장하는 대신, 직접 변수에 대해 메소드를 호출해도 확장 함수인 메소드가 알아서 널을 처리해준다.
fun verifyUserInput(input: String?){
if(input.isNullOrBlank()){ // 안전한 호출을 하지 않아도 된다.
println("please fill in the required fields")
}
}
6.1.10 타입 파라미터의 널 가능성
// t는 null이 가능
fun<T> printHashCode1(t: T) = println(t?.hashCode())
// t는 null이 불가능
fun<T: Any> printHashCode2(t: T) = println(t.hashCode())
6.1.11 널 가능성과 자바
코틀린은 여러 널 가능성 애노테이션을 알아본다. 만약 널 가능성 애노테이션이 소스코드에 없는 경우 자바의 타입은 코틀린의 플랫폼 타입이 된다.
플랫폼 타입 : 코틀린이 널 관련 정보를 알 수 없는 타입. 컴파일러는 모든 연산을 허용하고, 널 안정성 검사를 중복 수행해도 아무 경고도 표시하지 않는다.
fun yellAt(person: PersonSample) = println(person.name.toUpperCase() + "!!!")
fun main(){
yellAt(PersonSample(null))
}
여기서 NPE가 아닌, toUpperCase()가 $Receiver로 널을 받을 수 없다는 더 자세한 예외가 발생한다. 코틀린 컴파일러는 public 가시성인 코틀린 함수의 널이 아닌 타입인 파라미터와 수신 객체에 대한 널 검사를 추가해준다. 따라서 공개 가시성 함수에 널 값을 사용하면 즉시 예외가 발생한다. 파라미터값 검사는 함수 호출 시점에 이뤄지기 때문에(가능한 한 빨리) 예외가 발생해도 더 빨리 원인을 파악할 수 있다.
상속
자바로 구현한 메소드를 다른 코틀린 코드가 호출할 수 있으므로 코틀린 컴파일러는 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 만들어 준다.
6.2 코틀린의 원시 타입
6.2.1 원시 타입: Int, Boolean 등
코틀린은 원시 타입과 래퍼 타입을 구분하지 않으므로 항상 같은 타입을 사용한다. 대부분의 경우 코틀린 Int 타입은 자바 int 타입으로 컴파일된다. 하지만 컬렉션의 타입 파라미터로 넘길 경우 Integer 타입으로 컴파일된다.
6.2.2 널이 될 수 있는 원시타입: Int?, Boolean? 등
null 참조를 자바의 참조 타입의 변수에만 대입할 수 있기 때문에 null이 될 수 있는 코틀린 타입은 자바의 래퍼 타입으로 컴파일된다.
JVM은 타입 인자로 원시 타입을 허용하지 않는다. 따라서 자바나 코틀린 모두에서 제네릭 클래스는 항상 박스 타입을 사용해야 한다.
6.2.3 숫자 변환
코틀린은 한 타입의 숫자를 다른 타입의 숫자로 자동 변환하지 않는다.
val i = 1
val l: long = i // Error: type mismatchval i = 1
val l: long = i.toLong()
두 박스 타입 간의 equals 메소드는 그 안에 들어있는 값이 아니라 박스 타입 객체를 비교한다. 따라서 new Integer(42).equals(new Long(42))
는 false다.
val x = 1
val list = listOf(1l, 2l, 3l)
>>> x in list -> falseval x = 1
>>>println(x.toLong() in listOf(1l, 2l, 3l)) -> true
6.2.4 Any, Any?: 최상위 타입
자바에서 Object 가 클래스 계층의 최상위 타입이듯 코틀린에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상 타입이다. 자바에서는 참조타입만 Object를 정점으로 하는 타입 계층에 포함되며, 원시 타입은 포함되지 않는다. 하지만 코틀린에서는 Any가 모든 타입의 조상 타입이다.
6.2.5 Unit 타입: 코틀린의 void
코틀린 Unit 타입은 자바 void와 같은 기능을 한다. 자바 void와 다른 점은 Unit은 모든 기능을 갖는 일반적인 타입이며, void와 달리 Unit을 타입 인자로 쓸 수 있다. (Processor<Unit>: 유닛을 반환하지만 타입을 지정할 필요는 없다.)
6.2.6 Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다.
테스트 라이브러리들은 fail이라는 함수를 제공해 정상적으로 함수가 끝나지 않는다는 것을 알린다. Nothing 타입은 아무 값도 포함하지 않는다. 따라서 Nothing은 함수의 반환타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다.
fun fail(message: String): Nothing{
throw IllegalStateException(message)
}
6.3 컬렉션과 배열
6.3.1 널 가능성과 컬렉션
List<Int?> : 리스트 안의 각 값이 널이 될 수 있다.
List<Int>? : 전체 리스트가 널이 될 수 있다.
filterNotNull
이란 함수를 사용해 널이 될 수 있는 값으로 이뤄진 컬렉션에서 널 값을 걸러내 사용한다.
fun collectionWithNull(): List<Int> {
val listWithNull = listOf(1, null, 3)
return listWithNull.filterNotNull()
}>>> [1, 3]
6.3.2 읽기 전용과 변경 가능한 컬렉션
코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다. (Collection과 MutableCollection)
읽기 전용 컬렉션이라고해서 꼭 변경 불가능한 컬렉션일 필요는 없다. 같은 컬렉션 객체를 가리키는 다른 타입의 참조들(읽기 전용과 변경 가능 리스트)이 있는 경우, 변경 가능 리스트가 해당 컬렉션의 값을 바꾸면 읽기전용 리스트에서는 ConcurrentModificationException이나 다른 오류가 발생할 수 있다. 따라서 다중 스레드 환경에서 데이터를 다루는 경우 그 데이터를 적절히 동기화 하거나 동시 접근을 허용하는 데이터 구조를 활용해야 한다.
6.3.3 코틀린 컬렉션과 자바
자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않으므로, 코틀린에서 읽기 전용 컬렉션으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다. 따라서 컬렉션을 자바로 넘기는 코틀린 프로그램을 작성한다면 호출하려는 자바 코드가 컬렉션을 변경할지 여부에 따라 올바른 파라미터 타입을 사용할 책임은 개발자에게 있다.
6.3.4 컬렉션을 플랫폼 타입으로 다루기
플랫폼 타입의 경우 코틀린 쪽에는 널 관련 정보가 없다. 따라서 컴파일러는 코틀린 코드가 그 타입을 널이 될 수 있는 타입이나 널이 될 수 없는 타입 어느 쪽으로든 사용할 수 있게 허용한다.
컬렉션 타입이 시그니처에 들어간 자바 메소드 구현을 오버라이드하려는 경우 읽기 전용 컬렉션과 변경 가능 컬렉션의 차이가 문제가 된다. 플랫폼 타입에서 널 가능성을 다룰 때처럼 이런 경우에도 오버라이드하려는 메소드의 자바 컬렉션 타입을 어떤 코틀린 컬렉션 타입으로 표현할 지 결정해야 한다. 즉 플랫폼 타입에 대해 컬렉션을 구현하는 쪽에 책임이 있으며, 다음과 같은 조건을 코틀린에서 고려해야 한다.
- 컬렉션이 널이 될 수 있는가?
- 컬렉션의 원소가 널이 될 수 있는가?
- 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?
6.3.5 객체의 배열과 원시타입의 배열
코틀린에서 배열을 만드는 방법
- arrayOf 함수
- arrayOfNulls 함수 (모든 원소가 널로 채워짐)
- Array 생성자는 배열크기와 람다를 인자로 받아서 생성
val letters = Array<String>(26) {i -> ('a'+i).toString()} // ex1)
val strings = listOf("a", "b", "c")
println("%s/%s/%s".format(*strings.toTypeArray())) // varargs 넘기기 위해 * 연산자 사용
다른 제네릭 타입에서 처럼 배열 타입의 타입 인자도 항상 원시타입이 아닌 객체타입이 된다.즉 Array -> 박싱된 정수의 배열 (자바의 Integer)
박싱하지 않은 원시타입이 필요하면 특별한 배열 클래스를 사용해야한다.IntArray, ByteArray, CharArray 등 -> int[], byte[] .. 식으로 컴파일 된다.
val fiveZeros = IntArray(5)
val fiveZerosTwo = IntArrayOf(0,0,0,0,0)
이밖에 박싱된 array를 toIntArray 등의 변환 함수를 사용해 박싱하지 않은 값이 들어있는 배열로 변환할 수 있다.
출처: 코틀린 인 액션