# 6장 코틀린 타입 시스템

### 이번장에서는...

#### 배운점, 느낀점

- 널이 될 수 있는 타입과 널을 처리하는 구문의 문법
- 코틀린 원시 타입 소개와 자바 타입과 코틀린 원시 타입의 관계
- 코틀린 컬렉션 소개와 자바 컬렉션과 코틀린 컬렉션의 관계

---

- 타입 시스템: 자바와 비교하면 코틀린의 타입 시스템은 코드의 가독성을 향상시키는 데 도움이 되는 몇 가지 특성을 새로 제공한다.
    - 널이 될 수 있는 타입
    - 읽기 전용 컬렉션

### 널 가능성

- 코틀린은 null에 대한 접근 방법은 가능한 한 이 문제를 실행 시점에서 컴파일 시점으로 옮기는 것이다. 널이 될 수 있는지 여부를 타입 시스템에 추가함으로써 컴파일러가 여러 가지 오류를 컴파일 시 미리 감지해서
  실행 시점에 발생 할 수 있는 예외의 가능성을 줄일 수 잇다.

#### 널이 될 수 있는 타입

- 코틀린과 자바의 중요한 차이는 코틀린 타입 시스템이 널이 될 수 있는 타입을 명시적으로 지원한다는 점
- 널이 될 수 있는 타입은 프로그램 안의 프로퍼티나 변수에 null을 허용하게 만드는 방법
- 어떤 변수가 널이 될 수 있다면 그 변수에 대해(그 변수를 수신 객체로) 메서드를 호출하면 `NullPointerException`이 발생할 수 있으므로 안전하지 않다. 코틀린은 그런 메서드 호출을 금지함으로써
  많은 오류를 방지한다.

```java
int setLen(String s){
        return s.length(); //이 함수에 null을 넘기면 NullpointerException이 발생
        }
fun strLen(s: String) = s.length // 컴파일 오류
fun strLenSafe(s: String?) = s.length
fun strLenSafe(s: String?) = s.length() // X
val x: String? = null
var y: String = x // X
strLen(x)
fun strLenSafe(s: String?): Int =
    if (s != null) s.length else 0 // null 검사를 추가하면 코드가 컴파일 된다.

fun main(args: Array<String>) {
    val x: String? = null
    println(strLenSafe(x))
}

타입의 의미

실행 시점에 널이 될 수 있는 타입이나 널이 될 수 없는 타입의 객체는 같다. 널이 될 수 있는 타입은 널이 될 수 없는 타입을 감싼 래퍼 타입이 아니다. 모든 검사는 컴파일 시점에 수행된다. 따라서 코틀린에서는 널이 될 수 있는 타입을 처리하는 데 별도의 실행시점 부가 비용이 들지 않는다

안전한 호출 연산자 : ?.

// 예제 1
fun printAllCaps(s: String?) {
    val allCaps: String? = s?.toUpperCase() // allCaps는 널일 수 있다.
    println("allCaps=  $allCaps")
}

// 예제2
class Employee(val name: String, val manager: Employee?)

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

// 예제3
class Address(val streetAddress: String, val zipcode: Int, val city: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country//여러 안전한 호출 연산자를 연쇄해 사용한다.
    return if (country != null) country else "Unknown"
}

fun main(args: Array<String>) {
    //예제 1
    printAllCaps("abc")
    printAllCaps(null)

    // 예제 2
    val ceo = Employee("Da Boss", null)
    val developer = Employee("bob smith", ceo)

    println(managerName(developer))
    println(managerName(ceo))

    // 예제 3
    val person = Person("Dmitry", null)
    println(person.countryName())
}

엘비스 연산자 ?:

fun foo(s: String?)
val t: String = s ?: "" // 's'가 널이면 결과는 빈 문자열("")이다

// 예제 1
fun strLenSafe2(s: String?): Int = s?.length ?: 0

// 예제 2
class Address2(val streetAddress: String, val zipCode: Int, val city: String, val country: String)
class Company2(val name: String, val address: Address2?)
class Person2(val name: String, val company: Company2?)

fun printShippingLabel(person: Person2) {
    val address = person.company?.address
        ?: throw IllegalAccessException("No address")
    with(address) {
        println(streetAddress)
        print("$zipCode $city, $country")
    }
}

fun main(args: Array<String>) {
    // 예제 1
    println(strLenSafe2("abc"))
    println(strLenSafe2(null))

    // 예제 2
    val address = Address2("Elsestr. 47", 80687, "Munich", "Germany")
    val jetbrains = Company2("JetBrains", address)
    val person = Person2("Dmitry", jetbrains)

    printShippingLabel(person)
}

안전한 캐스트: as?


class Person3(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person3 ?: return false // 타입이 서로 일치하지 않으면 false를 반환한다.
        return otherPerson.firstName == firstName && otherPerson.lastName == lastName // 안전한 캐스트를 하고나면 otherPerson이 Person타입으로 스마트 캐스트된다.
    }

    override fun hashCode(): Int {
        var result = firstName.hashCode()
        result = 31 * result + lastName.hashCode()
        return result
    }

}

fun main(args: Array<String>) {
    val p1 = Person3("Dmitry", "Jemerov")
    val p2 = Person3("Dmitry", "Jemerov")

    println(p1 == p2) //== 연산자는 equals 메서드를 호출한다

    print(p1.equals(42))
}

널 아님 단언: !!

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

fun main(args: Array<String>) {
    ignoreNulls(null) //java.lang.NullPointerException
}

person.company!!.address!!.country // 이런 식으로 코드를 작성하지 말라.

let 함수

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

fun main(args: Array<String>) {
    var email: String? = "[email protected]"
    email?.let { sendEmailTo(it) }

    email = null
    email?.let { sendEmailTo(it) }

}

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

class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private val myService: MyService? = null // null로 초기화하기 위해 널이 될 수 있는 타입인 프로퍼티를 선언한다

    @Before
    fun setUp() {
        myService = MyService() // setUp 메서드 안에서 진짜 초깃값을 지정한다.
    }

    @Test
    fun testAction() {
        Assert.assertEquals("foo", myService!!.performAction()) // 반드시 널 가능성에 신경 써야 한다 !!나 ?를 꼭 써야한다,
    }
}
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    private lateinit var myService: MyService // 초기화하지 않고 널이 될 수 없는 프로퍼티를 선언한다.

    @Before
    fun setUp() {
        myService = MyService() // setUp 메서드 안에서 진짜 초깃값을 지정한다.
    }

    @Test
    fun testAction() {
        Assert.assertEquals("foo", myService.performAction()) // 널 검사를 수행하지 않고 프로퍼티를 사용한다.
    }
}

널이 될 수 있는 타입 확장

fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) { // 안전한 호출을 하지 않아도 된다. 널이 될 수 있는 타입의 확장 함수는 안전한 호출 없이도 호출 가능하다
        println("Please fill in the required fields")
    }
}

fun main(args: Array<String>) {
    verifyUserInput(" ")
    verifyUserInput(null) // isNullOrBlank null을 수신 객체로 전달해도 아무런 예외가 발생하지 않는다.
}
fun String?.isNullOrBlank(): Boolean = //널이 될 수 잇는 String의 확장
    this == null || this.isBlank() // 두번째 this 에는 스마트 캐스트가 적용된다.

타입 파라미터의 널 가능성

fun <T> printHashCode(t: T) {
    println(t?.hashCode()) // t가 널이 될 수 있으므로 안전한 호출을 써야만 한다.
}

fun main(args: Array<String>) {
    printHashCode(null) //T의 타입은 Any?로 추론된다.
}
fun <T : Any> printHashCode(t: T) { // 이제 T는 널이 될 수 없는 타입이다.
    println(t?.hashCode())
}

fun main(args: Array<String>) {
    printHashCode(null) //이 코드는 컴파일 되지 않는다. 널이 될 수 없는 타입의 파라미터에 널을 넘길 수 없다.
}

널 가능성 자바

플랫폼 타입
val i: Int = person.name
//ERROR: Type mismatch: inferred type is String! but Int was expected

상속

interface String processor {
    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)
        }
    }
}

결론

코틀린의 원시 타입

원시 타입: Int, Boolean등

val i: Int = 1
val list: List<Int> = listOF(1, 2, 3)
fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100) // 특정 범위값 제한
    println("We're ${percent} % done !")
}

fun main(args: Array<String>) {
    showProgress(146)
}

널이 될 수 있는 원시 타입: Int?, Boolean? 등

data class Person(val name: String, val age: Int? = null) {
    fun isOlderThan(other: Person): Boolean? {
        if (age == null || other.age == null)
            return null
        return age > other.age
    }
}

fun main(args: Array<String>) {
    println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
    println(Person("Sam", 35).isOlderThan(Person("Jane")))
}

숫자 변환

val i = 1
val l: Long = i // error: type mismatch 컴파일 오류 발생
val i = 1
val l: Long = i.toLong() // 직접 변환 메서드를 호출해야한다.
val x = 1 //int 타입인 변수
val list = listOf(1L, 2L, 3L) // Long 값으로 이뤄진 리스트
x in list // 묵시적 타입 변환으로 인해 false 임
val x = 1
println(x.toLong()) in listOf(1L, 2L, 3L) // true
val x = listOf(1_234, 1_000) // 리터럴 중간에 밑줄(_)을 넣을 수 있다.
val k = 42L
val h = 42.0f
fun foo(l: Long) = pritln(l)

fun main(args: Array<String>) {
    val b: Byte = 1 // 상수 값은 적절한 타입으로 해석된다
    val l = b + 1L // + Byte와 Long 인자로 받을 수 있다.
    foo(42) // 컴파일러는 42를 Long 값으로 해석한다,
}

Any, Any?: 최상위 타입

Unit 타입: 코틀린의 void

interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> { // Unit을 반환하지만 타입을 지정할 필요는 없다
    override fun process() {
        TODO("Not yet implemented")
    } // 여기서 return을 명시할 필요가 없다.
}

Nothing 타입: 이 함수는 결코 정상적으로 끝나지 않는다.

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

fun main(args: Array<String>) {
    fail("Error occurred")
}
val address = company.address ?: fail("No address")

fun main(args: Array<String>) {
    println(address.city)
}

컬렉션과 배열

널 가능성과 컬렉션

fun readNumbers(reader: BufferedReader): List<Int?> {
    val result = ArrayList<Int?>()

    for (line in reader.lineSequence()) {
        {
            try {
                val number = line.toInt()
                result.add(number)
            } catch (e: NumberFormatException) {
                result.add(null)
            }
        }
    }
    return result
}

import java.io.BufferedReader

fun addValidNumbers(numbers: List<Int?>) {
    var sumOfValidNumbers = 0
    var invalidNumbers = 0

    for (number in numbers) {
        if (number != null) {
            sumOfValidNumbers += number
        } else {
            invalidNumbers++
        }
    }

    println("SUm of valid numbers: $sumOfValidNumbers")
    println("Invalid numbers: $invalidNumbers")
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("1\\nabc\\n42"))
    val numbers = readNumbers(reader)
    addValidNumbers(numbers)
}
fun addValidNumbers(numbers: List<Int?>) {
    val validNumbers = numbers.filterNotNull()
    println("SUm of valid numbers ${validNumbers.sum()}")
    println("Invalid numbers: ${numbers.size - validNumbers.size}")
}

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

일기 전용과 변경 가능한 컬렉션 인터페이스
fun <T> copyElements(
    source: Collection<T>,
    target: MutableCollection
) {
    for (item in source) { // source 컬렉션의 모든 원소에 대해 루프를 돈다
        target.add(item) // 변경 가능한 Target 컬렉션에 원소를 추가한다.
    }
}

fun main(args: Array<String>) {
    val source: Collection<Int> = arrayListOf(3, 5, 7)
    val target: MutableCollection<Int> = arrayListOf(1)
    copyElements(source, target)
    println(target)
}

코틀린 컬렉션과 자바

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

컬렉션을 플랫폼 타입으로 다루기

  1. 컬렉션이 널이 될 수 있는가?
  2. 컬렉션의 원소가 널이 될 수 있는가?
  3. 오버라이드하는 메서드가 컬렉션을 변경할 수 있는가?
컬렉션 파라미터가 있는 자바 인터페이스
interface FileContentProcessor {
    void processContents(File path,
                         byte[] binaryContents,
                         List<String> textContents);
}
class FileIndexer : FileContentProcessor {
    override fun processContents(
        path: File,
        binaryContents: ByteArrat?,
        textContents: List<String>?
    )
}

객체의 배열과 원시 타입의 배열

fun main(args: Array<String>) {
    for (i in args.indices) { // 배열의 인덱스 값의 범위에 대해 이터레이션 하기 위해 array.indices 확장 함수를 사용한다.
        println("Argument $i is ${args[i]}") // array[index]로 인덱스를 사용해 배열 원소에 접근한다.
    }
}
val letters = Array<String>(26) { i -> ('a' + i).toString() }
fun main(args: Array<String>) {
    println(letters.joinToString(""))
}
val strings = listOf("a", "b", "c")
fun main(args: Array<String>) {
    println("%s/%s/%s".format(*strings.toTypedArray())) // vararg 인자를 넘기기 위해 스프레드 연산자*를 써야 한다.
}
val fiveZeros = IntArray(5)
val ficeZerosTwo = intArrayOf(0, 0, 0, 0, 0)

val squares = IntArray(5) { i -> (i + 1) * (i + 1) }
println(squares.joinToString())

fun main(args: Array<String>) {
    args.forEachIndexed { index, element ->
        println("Argument $index is : $element")
    }
}