Kotlin

Kotlin - 타입 시스템

엔꾸꾸 2020. 11. 15. 18:13

널 가능성

널 가능성 (Nullablility) 은 NPE를 피하기 위한 코틀린 타입 시스템의 특성이다.

코틀린을 비롯한 최신 언어에서는 null을 컴파일 타임에 미리 감지해 런타임 예외 가능성을 줄인다.

 

널이 될 수 있는 타입

코틀린 타입 시스템은 널이 될수 있는 타입을 명시적으로 지원한다.

널이 될 수 있는 변수에 대해 메소드 호출을 하지 못하게 함으로써 많은 오류를 방지한다.

 

/**
 * null 이 올수 있는 타입을 넣을 경우 컴파일 에러를 발생시켜 NPE를 방지한다.
 */
fun strLen(s: String) = s.length
fun strLenNullable(s: String?) = s.length

fun main(args: Array<String>) {
    // null 이 올 수 있는 타입을 넣을 경우 컴파일 에러가 발생한다.
    strLen(null)
    
    strLenNullable(null)
}

함수가 널과 문자열을 인자로 받을 수 있게 하려면 타입 명 뒤에 물음표를 명시해야 한다.

 

어떤 타입이든 타입 뒤에 물음표를 붙이면 null 참조를 저장할 수 있다.

 

널을 체크하기 위한 도구 if 하나 뿐이라면 코드가 매우 난잡해질 것이다.

코틀린에서는 널 체크를 위한 다양한 도구를 제공한다.

 

 

타입의 의미

타입이란 무엇이며, 왜 변수에 타입을 지정해야 할까?

타입은 분류 (classification), 타입은 어떤 값들이 가능한지와 그 타입에 대해 연산을 수행할 수 있는 종류를 결정한다.

코틀린의 널이 될 수 있는 타입은 여러가지 문제에 대한 종합적인 해법을 제공한다.

각 타입 값에 대해 어떤 연산이 가능할지 명확히 이해할 수 있고, 실행 시점에 예외가 발생할 수 있는 연산에 대해 판단이 가능하다.

 

실행 시점에 nullable / non-null 타입의 객체는 같다.
모든 검사는 컴파일 시점에 수행되기 때문에 실행시점에 별도의 부가비용이 들지 않는다.

 

안전한 호출 연산자 - ?.

코틀린이 제공하는 도구중 하나인 안전한 호출연산자 (?.) 이다.

?. 는 null 검사와 메소드 호출을 한번의 연산으로 수행하며 연쇄적인 호출이 가능하다.

/**
 * Null Safe 연산자를 사용하면 좀 더 간결하게 표현할 수 있다.
 */
fun printAllCaps(s: String?) {
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}

fun main(args: Array<String>) {
    printAllCaps("abc")
    printAllCaps(null)
}

 

엘비스 연산자 - ?:

코틀린은 null 대신에 사용할 디폴트 값을 지정할 때 편리한 연산자를 제공하는 이를 엘비스 연산자 (elvis operator) 라고 한다.

이 연산자를 90도 돌리면 엘비스 프레슬리의 특유 헤어스타일, 눈과 닮아서 엘비스 연산자라고 부른다.

/**
 * 엘비스 연산자는 디폴트 값을 지정할때 유용하게 사용한다.
 * 좌항의 값이 널이라면 디폴트값을 결과로하고, 좌항의 값이 널이아니라면 좌항의 값이 결과가 된다.
 */
fun foo(s: String?) {
    val t: String = s ?: "" // s 가 널이라면 결과는 빈문자열이다.
}

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

fun main(args: Array<String>) {
    strLenSafe(null)
    strLenSafe("abc")
}

 

안전한 캐스트 - as?

as로 명시적인 캐스팅을 할때마다 is 를 사용해 매번 타입 검사를 수행하는 것은 코틀린스럽지 않은 방법이다.

as? 연산자는 어떤 값을 지정한 타입으로 캐스팅하며, 만약 캐스팅 할 수 없다면 null을 반환한다.

안전한 캐스트를 사용할때 보통 수행한 뒤 엘비스 연산자를 사용하는 패턴을 많이 사용한다.

/**
 * 안전한 캐스트 연산자를 이용한 equals 구현 패턴
 * 보통 안전한 캐스트를 사용할때 뒤에 엘비스 연산자를 사용하는 패턴을 많이 사용한다.
 */
class SafePerson(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? SafePerson ?: return false
        return otherPerson.firstName == firstName &&
                other.lastName == lastName
    }
}
안전한 호출, 안전한 캐스트, 엘비스 연산자는 유용하기 때문에 코틀린 코드에서 자주 등장한다.

 

널 아님 단언문 - !!

널 아님 단언 (not-null assertion) 은 코틀린에서 널이 될 수 있는 타입의 값을 다룰때 가장 단순하면서도 무딘 도구이다.

느낌표를 이중으로 사용하면 어떤 값이든 널이 될 수 없는 타입으로 바꿀 수 있다.

/**
 * NotNullAssertion 은 nullable한 타입의 값을 nullable 하지 않게 강제로 바꾼다.
 * 코틀린 개발자들은 컴파일러가 검증할 수 없는 단언을 사용하기 보다 더 나은 방법을 찾아보라는 의드로
 * !! 라는 기호로 표현하였다.
 */
fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!
    println(sNotNull.length)
}
널 아님 단언문이 꼭 나쁜것만은 아니고 가장 나은 해법인 경우도 있다.
어떤 값이 널인지 쉽게 파악하기 위해 여러 !! 단언문을 한줄에 쓰는것을 피해야 한다.

 

let 함수

let 함수를 사용하면 널이 될 수 있는 식을 더 쉽게 다룰 수 있다.

안전한 호출 연산자와 함께 사용하면 원하는 식을 평가해서 결과가 널인지 검사한 뒤

그 결과를 변수에 대입하는 작업을 간단한 식으로 처리할 수 있다.

let 함수를 통해 인자를 전달할 수도 있으며, let 함수는 자신의 수신객체를 인자로 전달받은 람다에게 넘긴다.

 

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

/**
 * let과 안전한 연사자를 사용해 null 객체를 다룰때 좀 더 간결해지는 예제이다.
 */
fun main(args: Array<String>) {
    var email: String? = "ncucu.me@kakaocommerce.com"

    // email 이 null 이 아닐경우 let 의 람다로 전달되어 sendEmailTo 함수가 호출된다.
    email?.let { sendEmailTo(it) }
}
여러 값이 널인지 검사해야 한다면, let 을 중첩해서 할 수 있지만, 그런 경우 가독성이 떨어지기 때문에 if 를 사용하는것이 낫다.

 

나중에 초기화할 프로퍼티

객체 인스턴스를 생성한 뒤 나중에 초기화하는 프레임워크가 많다.

예를 들어 Junit 에서는 @Before 애노테이션이 적용된 메소드 내에서 초기화 로직을 수행한다.

코틀린 에서는 클래스 내에 존재하는 널이 될 수 없는 프로퍼티를 생성자 안에서 초기화하지 않고 특별한 메소드 안에서 초기화 할 수 없다.

이를 위해 lateinit 이라는 변경자를 지원한다.

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

/**
 * lateinit 변경자를 사용해 늦은 초기화를 할 수 있다.
 */
class MyTest {
    private lateinit var myService: MyService

    @Before
    fun setUp() {
        myService = MyService()
    }

    @Test
    fun testAction() {
        myService.performAction()
    }
}
lateinit 프로퍼티는 항상 var 여야 하며, 초기화 하기 전에 접근하면 has not been initialized 예외가 발생한다.

 

널이 될 수 있는 타입 확장

널이 될 수 있는 타입에 대해 확장 함수를 정의하면 강력한 도구로 활용할 수 있다.

직접 변수에 대해 메소드를 호출하더라도 확장함수인 메소드가 알아서 널처리를 수행한다.

일반 멤버에 대한 호출은 인스턴스를 통해 dispatch 되므로, 널 여부를 검사하지 않는다.

/**
 * 널이 될수 있는 수신 객체에 대한 확장 함수 호출
 * 안전한 호출(?.) 없이도 널이 될 수 있는 수신 객체 타입에 대해 선언된 확장함수 호출이 가능하다.
 * null 이 들어오는 겨우를 적절히 처리한다.
 */

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

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

fun main(args: Array<String>) {
    verifyUserInput("")
    verifyUserInput(null)
}
자바 메소드 내에서 this 는 항상 널이지만, 코틀린에서는 널이 될 수 없는 타입의 확장함수 내에서의 this 는 널이 될 수 있다.

 

디스패치 란?

객체지향 언어에서 동적 타입에 따라 적절한 메소드를 호출해주는 것을 동적 디스패치 라고 한다.

컴파일 시점에 결정하여 코드를 생성하는 방법을 정적 디스패치라고 한다.

보통 동적 디스패치는 객체 별로 자신의 메소드에 대한 테이블을 저장하는 방법을 많이 사용한다.

 

 

타입 파라미터의 널 가능성

코틀린에서 함수나 클래스의 모든 타입 파라미터는 널이 될 수 있다.

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

/**
 * 코틀린에서 타입 파라미터는 널이 될 수 있는 타입이다.
 * 타입 뒤에 ? 가 없더라도 널이 될 수 있는 타입이라는 점을 기하라.
 */
fun <T> printHashCode(t: T) {
    println(t?.hashCode())
}

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

타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한 (upper bound) 를 지정해야 한다.

/**
 * 타입 파라미터가 널이 아님을 확실히 하려면 널이 될 수 없는 타입 상한을 지정해야한다.
 */
fun <T: Any> printHashCodeNotNull(t: T) {
    println(t.hashCode())
}

 

널 가능성과 자바

코틀린은 자바 상호운용성을 강조하는 언어이다.

자바의 타입시스템은 널 가능성을 지원하지 않는다.

자바와 코틀린을 조합했을땐 매번 null 검사를 수행해야 하는가?

- 자바 코드에도 애노테이션으로 표시된 널 가능성 정보가 있다.

- 그런 정보가 존재할 경우 코틀린에서도 이를 활용한다.

코틀린은 여러 널 가능성 애노테이션을 알아보며, 널 가능성 애노테이션이 존재하지 않는 자바 타입은 코틀린의 플랫폼 타입이 된다.

 

플랫폼 타입

플랫폼 타입이란 코틀린이 널 관련 정보를 알 수 없는 타입이다.

널이 될 수 있는 타입과 널이 될 수 없는 타입 둘중 아무렇게나 처리해도 된다.

자바와 마찬가지로 플랫폼 타입에 대해 수행하는 모든 연산의 책임은 개발자에게 있다는 의미이다.

코틀린은 플랫폼 타입에 대해 수행하는 연산은 경고를 표시하지 않는다.

코틀린 컴파일러는 public 가시성을 가지는 코틀린 함수의 널이 아닌 타입 파라미터와 수신 객체에 대해 널 검사를 추가해 준다.

파라미터 값 검사는 호출 시점에 이루어 진다.

자바 API 를 다룰때는 조심해야 한다. 대부분 널 관련 애노테이션을 사용하지 않기 때문에 플랫폼 타입이기 때문이다.

 

코틀린이 플랫폼 타입을 도입한 이유

모든 자바 타입을 널이 될 수 있는 타입으로 다루면 더 안전하지만

널이 될 수 없는 값에 대해서도 불필요한 널 검사가 들어가기 때문이다.

코틀린에서 플랫폼 타입을 선언 할 수 없다.

 

상속

코틀린에서 자바 메소드를 오버라이드 할 때 그 메소드의 파라미터와 반환 타입을 널이 될 수 있는 타입인지, 널이 될 수 없는 타입인지 선언해 주어야 한다.

자바 클래스나 인터페이스를 코틀린에서 구현할 경우 널 가능성을 제대로 처리하는 것이 중요하다.

코틀린 컴파일러는 구현 메소드를 널이 될 수 없는 타입으로 선언한 모든 파라미터에 대해 널이 아님을 검사하는 단언문을 생성해 준다.

 

 

코틀린원시 타입

코틀린은 원시타입과 래퍼타입을 구분하지 않는다.

 

원시타입 - Int, Boolean 등..

자바는 원시 타입과 참조 타입을 구분한다.

원시 타입의 변수에는 그 값이 직접 들어가지만, 참조 타입은 변수의 주소값이 들어간다.

코틀린은 원시 타입과 래퍼타입을 구분하지 않고 항상 같은 타입을 사용한다.

항상 객체로 표현한다면 매우 비효율적이겠지만 코틀린은 그렇지 않다.

실행 시점에 숫자 타입은 가능한 가장 효율적인 방식으로 표현된다.

- 대부분의 경우 코틀린 Int 타입은 자바 int 타입으로 컴파일 됨

Int 와 같은 코틀린 타입에는 널 참조가 들어갈 수 없기 때문에 그에 상응하는 자바 원시 타입으로 컴파일 된다.

자바 원시 타입을 코틀린에서 사용할 때도 플랫폼 타입이 아닌 널이 될 수 없는 타입으로 취급 된다.

 

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

코틀린의 널이 될 수 있는 원시타입을 사용하면 자바의 래퍼타입으로 컴파일 된다.

제네릭 클래스의 경우 래퍼타입을 사용한다.

이는 JVM 에서 제네릭을 구현하는 방법 때문이다.

JVM 은 타입 인자로 원시 타입을 허용하지 않기 때문에 코틀린 자바 모두 제네릭 클래스는 항상 박스 타입을 사용해야 한다.

/**
 * 널 가능성이 있는 타입의 두 값을 직접 비교할 수는 없다.
 * 널 검사를 마친 뒤 일반 값처럼 다루게 허용된다.
 */
data class HelloPerson(val name: String,
                       val age: Int? = null) {
    fun isOlderThan(other: HelloPerson): Boolean? {
        if (age == null || other.age == null) {
            return null
        }
        return age > other.age
    }
}

 

숫자 변환

코틀린과 자바의 가장 큰 차이점중 하나는 숫자를 변환하는 방식이다.

코틀린은 다른 타입의 숫자로 자동 변환을 하지 않는다.

결과 타입이 허용하는 범위가 원래 타입 범위보다 넓은 경우에도 자동 변환을 하지 않는다.

직접 변환 메소드를 호출해야 한다.

fun main(args: Array<String>) {
    // 컴파일 에러, 자동 변환 하지 않음
    val i = 1
    val l: Long = i

    // 직접 변환 함수를 호출 해주어야 한다.
    val i2 = 1
    val l2: Long = i2.toLong()
}
코틀린은 Boolean 을 제외한 모든 원시 타입에 대한 변환 함수를 제공한다.

 

Any, Any? - 최상위 타입

자바에서 Object 가 최상위 타입이라면, 코틀린 에서는 Any 타입이 모든 널이 될 수 없는 타입의 조상이다.

자바에서는 원시 타입 대신 래퍼클래스만 Object를 조상으로 가지지만, 코틀린은 그렇지 않다.

코틀린에서 원시 타입 값을 Any 타입 변수에 대입하면 자동으로 값을 객체로 감싼다.

/**
 * 자바에서 Object 가 최상위 클래스인것 처럼 코틀린에서는 Any가 최상위 클래스이다.
 * 차이점 이라면 원시타입의 조상도 Any 라는 것이다.
 */
fun main(args: Array<String>) {
    val answer: Any = 42
}
코틀린 Any 타입을 사용할 경우 자바 Object 타입으로 컴파일 된다.

 

Unit 타입 - 코틀린의 Void

Unit 타입은 자바의 void 와 같은 기능을 한다.

반환 타입 선언 없이 정의한 함수와 같다.

컴파일러가 묵시적으로 return Unit 을 넣어주기 때문에 명시적으로 반환하지 않아도 된다.

/**
 * Unit 반환 타입은 반환 타입 선언 없이 정의한 함수와 동일하다.
 */
fun f(): Unit {
    // ...
}

fun f2() {
    // ...
}
대부분의 경우 void 와 Unit의 차이를 알기란 어렵다.
코틀린 함수 반환 타입이 Unit 타입이고, 제네릭 함수를 오버라이드 하지 않는다면 자바 void 함수로 컴파일 된다.

함수형 프로그래밍에서 전통적으로 Unit 은 단 하나의 인스턴스만 갖는 타입을 의미 해 왔다.

그 유일한 인스턴스 유무가 바로 자바의 void 와 코틀린 Unit을 구분하는 큰 차이이다.

다시 정리하면, 코틀린은 void '리턴값이 없는' 상태를 Unit이라는 싱글톤 클래스로 정의했다.

 

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

코틀린에서 결코 성공적으로 값을 돌려주는 일이 없으므로 반환값 이라는 개념 자체가 없는 함수가 일부 존재한다.

테스트 라이브러리의 fail 과 같은 함수는 반환 값 없이 예외를 던져 테스트를 실패시킨다.

이런 함수를 호출하는 코드 분석시 함수가 정상적으로 끝나지 않는다는 사실을 알려주기 위해 Nothing 이라는 반환 타입이 있다.

Nothing은 어떠한 값도 포함하지 않는 타입이며 private constructor로 정의되어 있어 인스턴스를 생성할 수 없다.

import java.lang.IllegalStateException

/**
 * 정상적으로 끝나지 않는 함수의 경우 (테스트 프레임워크의 fail등..)
 * Nothing 반환 타입을 명시해 줌으로써 정상적으로 끝나지 않음을 표시해주면 유용하다.
 */
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

 

Nothing 은 모든 클래스의 서브클래스이다. (이는 개발자가 새롭게 정의한 클래스에도 포함된다.)

Nothing 이 모든 클래스의 서브클래스 이기 때문에 아래 예제와 같은 방식의 코드가 가능해진다. 

// null 일경우 예외를 던지는 코드
val str: String? = null
val result = str ?: throw RuntimeException()

// null 일경우 함수를 종료시키는 코드
val str: String? = null
val result: Int = str?.toInt() ?: return

 

 

널 가능성과 컬렉션

컬렉션 안에 널 값을 넣을 수 있는지 여부는 매우 중요하다.

타입 인자로 쓰인 타입에도 ? 를 붙이면 nullable 한 타입이 된다.

/**
 * 널이 될수 있게 만들때는 주의 해야한다
 * List<Int?> 는 내부 요소가 널이 될 수 있고
 * List<Int>? 는 리스트 전체가 널이 될 수 있다.
 */
fun readNumbers(reader: BufferedReader): List<Int?> {
    val result = ArrayList<Int?>()
    // BufferedReader 를 코틀린에서 다루는 베스트한 방법
    for (line in reader.lineSequence()) {
//        try {
//            val number = line.toInt()
//            result.add(number)
//        } catch (e: NumberFormatException) {
//            result.add(null)
//        }
        // 코틀린 1.1 이후
        val number = line.toIntOrNull()
        result.add(number)
    }
    return result
}
유의 할점은 List<Int?>? 는 리스트도 널이 될 수 있고, 리스트 내부 요소도 널이 될 수 있다.

 

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

코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 하나가 있다.

코틀린의 컬렉션은 컬렉션 내부 데이터에 접근하는 인터페이스와 컬렉션 내부 데이터를 변경하는 인터페이스를 분리 했다는 점이다.

데이터 접근에 대한 인터페이스는 kotlin.collections.Collection 이고, 핵심 메소드는 size, iterator, contains이다.

데이터 수정에 대한 인터페이스는 kotlin.collections.MutableCollection 이며 이는 Collection을 확장하며 내용을 변경하는 메소드들을 추가 제공한다.

가능하다면 항상 읽기 전용 인터페이스를 사용하는 것을 규칙으로 삼아야 한다. (불변성 유지를 위함)

MutableCollection 을 인자로 받는 함수에 전달할 때에는 원본의 변경을 막기 위해 컬렉션을 복사해야 할 수도 있다.

- 이는 방어적 복사 (defensive copy) 패턴 이라고 한다.

/**
 * Kotlin Collection 의 특징은 읽기전용과, 읽기/쓰기용 인터페이스를 분리 했다는 점이다.
 * Collection 은 읽기전용, MutableCollection 은 쓰기가 가능한 컬렉션이다.
 * 어떤 함수의 MutableCollection 인터페이스 인자를 보고 변경이 일어날수 있음을 짐작할 수 있다.
 */
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)
}
읽기 전용 컬렉션은 항상 Thread-Safe 하지 않다 라는 것을 명심해야 한다.
읽기 전용 컬렉션 타입으로 받았지만 구현체는 읽기 전용이 아닐 수 있다.
해당 구현체를 멀티스레드 환경에서 내용을 변경하는 일이 생긴다면 ConcurrentModificationException 이 발생할 수 있다.

 

코틀린 컬렉션과 자바

모든 코틀린 컬렉션은 자바 컬렉션 인터페이스의 인스턴스 이다.

코틀린과 자바를 오갈때 아무 변환도 필요 하지 않다.

코틀린은 모든 자바 컬렉션 인터페이스 마다 읽기 전용과 읽기/쓰기 용 인터페이스라는 두가지 표현을 제공한다.

코틀린의 읽기 전용과 읽기/쓰기용 인터페이스의 기본 구조는 java.util 패키지에 있는 자바 컬렉션 인터페이스 구조를 그대로 옮겨 두었다.

 

코틀린 컬렉션

타입 읽기 전용 읽기/쓰기
List listOf mutableListOf, arrayListOf
Set setOf mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf
Map mapOf mutableMapOf, hashMapOf, linkedMapOf, sortedMapOf

 

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

자바 코드에서 정의한 타입을 코틀린에서는 플랫폼 타입으로 처리한다.

플랫폼 타입은 코틀린 쪽에서 널과 관련된 정보가 없다. (이는 컬렉션도 동일)

만약 컬렉션 타입이 메소드 시그니처에 포함된 자바 메소드를 오버라이드 할 경우 읽기 전용과 읽기/쓰기용 컬렉션 차이는 문제가 된다.

플랫폼 타입에서 널 가능성을 다룰 때 처럼 어떤 코틀린 컬렉션 타입으로 표현할지 결정해야 한다.

- 컬렉션이 nullable 한가 ?

- 컬렉션 원소가 nullable 한가?

- 오버라이드하는 메소드가 컬렉션을 변경할 수 있는가?

코틀린에서 변경 불가능한 컬렉션으로 넘기더라도 자바에서는 변경할 수 있다는 점을 기억하라.

 

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

코틀린 배열은 타입 파라미터를 받는 클래스 이다.

배열의 원소 타입은 타입 파라미터에 의해 결정된다.

 

코틀린에서 배열을 만드는 방법

코틀린에서 배열을 만드는 방법은 크게 3가지로 나뉜다.

1. arrayOf

2. arrayOfNulls - 모든 원소가 null 이고, 인자로 넘긴 값과 크기가 같은 배열

3. Array 생성자

- 배열 크기와 람다를 인자로 받아 람다가 각 배열 원소를 초기화 한다.

- arrayOf 를 쓰지 않고 각 원소가 널이 아닌 배열을 만들경우 사용한다.

/**
 * 코틀린은 타입 파라미터를 받는 클래스이다.
 * 원소의 타입은 타입 파라미터에 의해 결정된다.
 */
fun main(args: Array<String>) {
    // 코틀린 배열 사용하기
    for (i in args.indices) {
        println("Argument $i is: ${args[i]}")
    }

    // 배열을 생성하는 방법

    // 1. arrayOf
    // 원소를 받아 배열을 생성
    val arr1: Array<Int> = arrayOf(1, 2, 3, 4)

    // 2. arrayOfNulls
    // 파라메터 값 만큼 null 원소를 갖는 배열을 생성
    val arr2: Array<Int?> = arrayOfNulls(4)

    // 3. Array 생성자
    // arrayOf를 사용하지 않고 널이 아닌 배열을 초기화할때 사용
    // 사이즈 파라메터와, 원소 초기화 람다를 받는다.
    val arr3: Array<Int> = Array(4) { 1 }
}

 

컬렉션을 배열로 변환

/**
 * 컬렉션을 배열로 변환
 */
fun main(args: Array<String>) {
    val strings = listOf("a", "b", "c")
    strings.toTypedArray()
}
코틀린에서 배열을 인자로 받는 자바 함수를 호출하거나 vararg 파라메터를 받는 코틀린 함수를 호출하기 위해 배열을 만든다.

 

코틀린에서 배열을 선언하면 해당 배열은 박싱된 배열이 된다.

- Array<Int> = java.lang.Integer[]

원시 타입의 배열을 표현하기 위해서는 코틀린에서 제공하는 원시 배열을 사용해야 한다.

IntArray, ByteArray, CharArray 와 같은 원시타입 배열을 제공한다. 

 

원시타입 배열 만들기

size 인자를 받아 각 원소를 해당 원시타입의 디폴트 값으로 초기화 한다.

팩토리 함수는 여러 값을 가변 인자로 받아 그 값이 들어간 배열을 반환한다.

일반 배열과 동일하게 사이즈와 람다를 받는 생성자를 사용한다.

/**
 * 컬렉션을 배열로 변환
 */
fun main(args: Array<String>) {
    val strings = listOf("a", "b", "c")
    strings.toTypedArray()
}
박싱된 값이 들어간 컬렉션이나 배열이 있다면 toIntArray 와 같은 함수를 사용해서 원시 배열로 변환할 수 있다.
또한 코틀린 표준 라이브러리는 배열의 기본 연산에 더해 컬렉션에서 제공하는 모든 확장 함수를 배열에도 제공한다.
fun main(args: Array<String>) {
    // * 코틀린 배열은 컬렉션과 동일하게 확장 함수를 제공한다.
    args.forEachIndexed { index, e ->  println("Argument $index is $e") }
}

 

정리

- 코틀린은 널이 될 수 있는 타입을 지원해서 NPE 를 컴파일 타임에 감지한다.
- 안전한 호출 (?.) 엘비스 연산자 (?:) 널 아님 단언 (!!) let 함수 등을 사용하면 nullable 한 타입을 간결한 코드로 다룰수 있다.
- as? 연산자를 사용하면 값을 다른 타입으로 변환하는 것과 불가능한 경우를 한번에 처리할 수 있다.
- 자바에서 가져온 타입은 플랫폼 타입으로 취급된다. nullable, not-null 모두 취급할 수 있다.
- 코틀린에서 원시 타입은 일반 클래스와 똑같이 생겼고, 동일하게 동작하지만 대부분 컴파일러는 원시타입으로 컴파일한다.
- nullable 한 원시타입은 자바의 래퍼 클래스와 대응된다.
- Any 타입은 자바의 Object 에 해당한다. Unit 은 자바의 void 와 비슷하다.
- 함수가 정상적으로 끝나지 않는다면 Nothing 반환 타입을 지정해 명시하는것이 좋다.
- 코틀린 컬렉션은 표준 자바 컬렉션 클래스를 사용한다. 하지만 읽기 전용과 읽기/쓰기용 으로 구별해 제공한다.
- 자바 클래스를 코틀린에서 확장하거나 구현한다면 메소드 파라미터의 널가능성과 변경 가능성에 대해 고민해야 한다.
- 코틀린 Array 클래스는 자바 배열로 컴파일 된다.
- 원시 타입의 배열은 IntArray 처럼 제공한다.