6. 코틀린 타입 시스템
CS/코틀린 인 액션

6. 코틀린 타입 시스템

제일 큰 차이는 null를 다루는지 여부. 이것만 알아도 90%는 먹고 들어감

 

6.1. 널 가능성

어느 타입이 null이 될 수 있으면 ?가 붙고, 명확하게 null이 될 수 없다면 붙지 않는다. 즉,

Type? = Type + null

이 되는거

 

그래서 만약 내가 다루고자 하는 타입이 널이 될 수 있다면 널인지 아닌지 체크해줘야 한다.

fun strLenSafe(s: String?): Int = 
    if (s != null) s.length else 0
    
val x: String? = null
println(strLenSafe(x)) // 0

println(strLenSafe("abc")) // 3

그럼 ?가 있을 때마다 이렇게 해야되냐? 그래서 타입 ? 을 만들면서 널을 다루는 전용 연산자들을 만들었다(마음에 드는 철학).

 

 

안전한 호출 연산자 ?. 은 해당 변수가 null이 아니면 그대로 함수를 실행하고, null이면 null을 반환한다.

fun printAllCaps(s: String?) {
    val allCaps: String? = s?.uppercase(Locale.getDefault())
    println(allCaps)
}

printAllCaps("abc") // ABC
printAllCaps(null) // null

 

엘비스 연산자 ?:는 만약 null이면 다른걸 대입하라는 거다.

이름이 엘비스인 이유는 저 기호가 엘비스 가수 머리모양이랑 비슷해서

 

http://t0.gstatic.com/licensed-image?q=tbn:ANd9GcTr4NoZ0FtPzrZ1Ya_MSKuBYEFOi8LQ4kXcfo_kmCrKvcFCH-0ddrEwy9Czr4BkTiJl

fun strLenSafe(s: String?): Int =
    s?.length ?: 0

 

안전한 캐스트 as? 는 변환을 시도해서 되면 하고 아니면 null

class Person(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false

        return otherPerson.firstName == firstName &&
            otherPerson.lastName == lastName
    }

    override fun hashCode(): Int =
        firstName.hashCode() * 37 + lastName.hashCode()
}

val p1 = Person("John", "Smith")
val p2 = Person("John", "Smith")

println(p1 == p2) // true
println(p1.equals(p2)) // true

println(p1.equals(42)) // false

 

널 아님 단언 !! 은 코드 작성자가 얘는 null이 될 수 없다고 변환시켜주는 것. 물론 에러뜨면 사용자 책임

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}

fun main(args: Array<String>) {
    ignoreNulls(null)
}

/*
Exception in thread "main" java.lang.NullPointerException
	at MainKt.ignoreNulls(Main.kt:2)
	at MainKt.main(Main.kt:7)
*/

그래도 좋은 건 컴파일러가 검사해주기 때문에 함수 실행 시점에 에러를 뜨게 하는 것. 그래서 막 깊숙히 이것저것 다 실행하고 난 뒤에 에러 떠서 찾기 힘든게 아니라 내 코드에서 명확히 알 수 있음.

 

let 함수는 해당 함수가 null이 아니면 실행, null이면 아무 일도 없었다

fun sendEmailTo(email: String) {
    println("Sending email to $email")
}

fun main(args: Array<String>) {
    var email: String? = "yole@example.com"
    email?.let { sendEmailTo(it) } // Sending email to yole@example.com
    email = null
    email?.let { sendEmailTo(it) }
}

 

나중에 초기화하는 lateinit도 있는데, null하다가 생기는 NullPointerException보단 "lateinit property ~~ has not been initialized"라는 명확한 에러문이 떠서 좋음.

class MyTest {
    private lateinit var myService: MyService
    
    @Before fun setUp() { // setUp을 해 줘야 초기화됨
        myService = MyService()
    }
    
    @Test fun testAction() {
        Assert.assertEquals("foo",
            myService.performAction())
    }
}

 

 

널이 될 수 있는 타입 확장은 null까지 고려해서 작성하는 거. string에서 null이나 ""이나 그게 그거 아닐까

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) {
        println("Please fill in the required fields")
    }
}

fun main(args: Array<String>) {
    verifyUserInput(" ")
    verifyUserInput(null)
    verifyUserInput("test")
}

isNullOrBlank는 다음 확장 함수라고 생각하면 됨

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

 

let은 안쓰냐 하지만, 다시 보면 let도 널 안전 연산자 ?.를 붙여써서 안의 it은 null인지 아닌지 검사 안함

val person: Person? = ...
person.let { sendEmailTo(it) } // ERROR: Type mismatch: inferred type is Person? but Person was expected
person?.let { sendEmailTo(it) }

 

타입 파라미터의 널 가능성은 꺽쇠 <>로 알려줄 수 있음

fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null) // Any?로 추론됨 // null
}
fun <T: Any> printHashCode(t: T) {
    println(t.hashCode())
}

fun main(args: Array<String>) {
    // printHashCode(null) // Kotlin: Null can not be a value of a non-null type TypeVariable(T)
    printHashCode(42)
}

 

 

하지만 자바는 null이니 아니니 그런 타입이 없다. 그래서 추가 데코레이터로 @Nullable, @NotNull을 지원하기도 하고 코틀린도 이걸 읽어서 타입 변환할 줄 안다.

@Nullable + Type = Type?

@NotNull + Type = Type

 

하지만 저런게 없을 경우, 코틀린은 다 고려해서 작성해야 한다. 이를 플랫폼 타입이라 함.

Type = Type? 또는 Type

 

// 자바
public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
fun yellAt(person: Person) {
    println(person.name.toUpperCase() + "!!!")
}


fun main(args: Array<String>) {
    yellAt(Person(null)) // java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ~~
}
fun yellAtSafe(person: Person) {
    println((person.name ?: "Anyone").toUpperCase() + "!!!")
}


fun main(args: Array<String>) {
    yellAtSafe(Person(null))
}

 

// 자바
interface StringProcessor {
    void process(String value);
}
class StringPrinter: StringProcessor {
    override fun process(value: String) {
        println(value)
    }
}

class NullableStringPrinter: StringProcessor {
    override fun process(value: String?) {
        if (value != null) {
            println(value)
        }
    }
}

코틀린은 두개 다 받아들인다.

 

 

6.2. 코틀린의 원시 타입

코틀린의 컬렉션은 자바의 컬렉션을 그대로 쓰는 것 처럼, 다른 int나 boolean 같은 것들도 자바의 것을 가져다 쓴다.

하지만 타입도 원시 타입과 참조 타입이 있다. 사실 자바가 콜렉션에 Int 넣을 때 Int를 쌩으로 넣는게 아닌 래퍼타입을 넣는데, 쌩 Int에 메서드를 사용하거나 컬렉션에 원시 타입을 담을 순 없기 때문. 원시 타입(primitive type)(int 등)의 변수에는 그 값이 직접 들어가지만, 참조 타입(reference type)(string 등)의 변수에는 메모리상의 객체 위치가 들어간다.

코틀린은 무슨 타입을 사용할지를 실행 시점에 결정한다. 원시 타입이 계산이 빠르기 때문에 가능한 경우엔 원시 타입을 사용하다가 컬렉션에 넣을 땐 래퍼 타입을 사용하는 방식.

널이 될 수 없는 타입의 경우, 자바에서 원시 타입은 널이 될 수 없기 때문에 코틀린에서 그대로 쓸 수 있고, 코틀린에서도 널이 될 수 없는 타입은 자바에서도 널이 될 수 없기때문에 서로 그대로 가져다 쓰면 된다.

 

그럼 널이 될 수 있는 타입(Int?, Boolean? 등)은? 래퍼인 참조 타입 쓴다.

 

숫자 간 타입 변환이 가능한데, 코드 작성 시 실수를 줄이도록 메서드를 작성하여 변환하도록 명시하고 있다.

val i = 1
// val l:Long = i // Error: Type mismatch
val l:Long = i.toLong()
println(l) // 1
val m = 1L
println(m) // 1
val o = 1.0f
println(o) // 1.0

 

Any 같은 경우, 자바의 모든 클래스의 조상인 java.lang.Object와 대응된다.

val answer:Any = 42
println(answer)
println(answer::class) // class java.lang.Integer (Kotlin reflection is not available)

 

Unit은 자바의 void와 대응.

 

Nothing은 이 함수가 정상적으로 끝나지 않는다는 표시다.

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

fun main(args: Array<String>) {
    fail("Error occurred") // Exception in thread "main" java.lang.IllegalStateException: Error occurred

}

Nothing을 작성하면 컴파일러는 이 함수가 결코 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용한다.

라고 한다.

 

 

6.3 컬렉션과 배열

리스트 자체가 널이 될 수 있는지, 원소가 널이 될 수 있는건지, 둘 다 인지 헷갈리지 말기

 

코틀린의 컬렉션에서 중요한 성질이 있는데, 읽기 전용 컬렉션 인터페이스랑 변경 가능한 컬렉션(Mutable) 인터페이스를 분리했다는 것이다. 그래서 읽기 전용 컬렉션은 변경할 수 있는 메서드 자체가 없다.

fun <T> copyElements(source: Collection<T>,
                     target: MutableCollection<T>) {
    for (item in source) {
        target.add(item)
    }
}

fun main(args: Array<String>) {
    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    println(target)
    
    val source2: Collection<Int> = arrayListOf(3, 5, 7)
    val target2: Collection<Number> = arrayListOf(1.0)
//    copyElements(source2, target2) // Error: Type mismatch
}

하지만 이 악물고 바꿀라고 하면 바꿀 수 있다. 변경 가능한 것을 참조할 수도 있기 때문.

그래서 완전 안심하지는 말자. 즉 읽기 전용 컬렉션이 항상 스레드 안전(thread safe)하지는 않다는 점을 명심해야 한다.

 

 

자바랑은

코틀린의 기본 구조는 java.util 패키지에 있는 자바 컬렉션 인터페이스의 구조를 그대로 옮겨 놓았다.

자바의 컬렉션은 코틀린이 자신의 것을 상속한 것 처럼 취급한다. 하지만 자바엔 읽기 전용, 변경 가능 그런게 없기 때문에 자바와 공유할 땐 사용자가 잘 짜야 한다.

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