코틀린 스터디 6장 정리 - 코틀린 타입 시스템

Posted by 김순철 on November 12, 2018

Chapter 6. 코틀린 타입 시스템

스터디 날짜

2018-11-07(수)

6.1 null 가능성

  • null 가능성nullability은 nullPointerException(NPE)를 피할 수 있게 해준다.

6.1.1 null이 될 수 있는 타입

  • 코틀린은 자바와 다르게 null이 될 수 있는 타입을 명시적으로 지원한다. 아래 자바 함수 살펴보자.
/* 자바 */

int strLen(String s) {
    return s.length();
}
  • 위 함수에 null을 넘기면 NPE가 발생한다. 위 예제를 코틀린으로 작성하면 아래와 같다.
/* 코틀린 */

fun strLen(s: String) = s.length
  • 위 예제의 strLen에 null을 넘기면 컴파일 시점에 오류가 발생한다.
>>> strLen(null)
ERROR: null can not be a value of non-null type String
  • 위 예제에서 null을 허용하게 하려면 타입 이름 뒤에 물음표(?)를 명시해한다.
fun strLenSafe(s: String?) = ...
  • 어떤 타입이든 타입 이름 뒤에 물음표를 붙이면 null 참조를 저장할 수 있다.
  • 반대로 말하면 물음표가 없는 타입은 null을 저장할 수 없다. 따라서 코틀린에서는 기본적으로 타입은 null이 될 수 없다.
  • null이 될 수 있는 타입의 변수는 연산이 제한된다. 변수.메소드() 처럼 메소드를 직접 호출 불가능하다. 그런 경우 아래와 같인 오류가 발생한다.
>>> fun strLenSafe(s: String?) = s.length()
ERROR: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type kotlin.String?
  • null이 될 수 있는 값을 null이 될 수 없는 타입의 변수에 대입 할 수 없다.
>>> val x: String? = null
>>> val y: String = x
ERROR: Type mismatch: inferred type is String? but String was expected
  • null이 될 수 있는 타입의 값을 null이 될 수 없는 타입의 파라미터에 전달 할 수 없다.
fun strLen(s: String) = s.length()

>>> val x: String? = null
>>> strLen(x)
ERROR: Type mismatch: inferred type is String? but String was expected
  • null이 될 수 있는 타입은 위와 같이 제약이 많음에도 불구하고 존재하는 가장 중요한 이유는 null과 비교하는 것이다.
  • 최초에 null과 비교하고 나면 컴파일러는 그 사실을 기억하고 null이 아님이 확실한 영역에서는 해당 값을 null이 될 수 없는 타입의 값처럼 사용한다. 아래 예를 보자.
/* if 검사를 통해 null 다루기 */

fun strLenSafe(s: String?): Int =
    if (s != null) s.length else 0  /* <-- null 검사를 하면 컴파일 된다. */

>>> val x: String? = null
>>> println(strLenSafe(x))
0
>>> println(strLenSafe("abc"))
3

6.1.3 안전한 호출 연산자: ?.

  • ?. 연산자는 안전한 호출safe call이라고 부르며, null 검사와 메소드 호출을 한번의 연산으로 가능하게 해준다.
  • s?.toUpperCase() 는 풀어 쓰면 if (s != null) s.toUpperCase() else null 과 같다.
  • 이름은 안전한 호출이지만 안전한 호출의 결과도 null이 될 수 있음에 유의해야 한다.
fun printAllCaps(s: String?) {
    val allCaps: String? = s?.toUpperCase() /* <-- allCaps는 null일 수도 있다. */
    println(allCaps)
}

>>> printAllCaps("abc")
ABC
>>> printAllCaps(null)
null
  • 프로퍼티를 읽거나 쓸 때도 안전한 호출을 사용할 수 있다.
/* null이 될 수 있는 프로퍼티를 다루기 위해 안전한 호출 사용하기 */
class Employee(val name: String, val manager: Employee?)

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


>>> val ceo = Employee("Da Boss", null)
>>> val developer = Employee("Bob Smith", ceo)
>>> println(managerName(develop))
Da Boss
>>> println(managerName(ceo))
null
  • 안전한 호출을 연쇄해서 사용할 수도 있다.
/* 예제 6.3 안전한 호출 연쇄시키기 */
class Address(
    val streetAddress: String,
    val zipCode: Int,
    val city: String,
    val country: String)

class Company(val nanme: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
    val country = this.company?.address?.country /* <-- 안전한 호출을 연쇄 사용 */
    return if (county != null) country else "Unknown"
}

>>> val person = Person("Dmitry", null)
>>>p println(person.countryName())
Unkown
  • 위 코드는 잘 동작한다. 하지만 null 검사를 하여 null인 경우 “Unkown”을 리턴하는 코드가 들어 있다. 이런 코드를 없앨 수도 있다. 다음 섹션을 보자.

6.1.4 엘비스 연산자: ?:

  • 코틀린은 null 대신 사용할 디폴트 값을 지정을 할 수 있는 앨바스elvis 연잔사를 제공한다.
  • 엘비스 연산자는 ?:을 사용하면 된다. (시계 방향으로 90도 돌려서 보면 엘비스 프레슬리 헤어스타일과 눈을 볼 수 있다.)
fun foo(s: String?) {
    val t: String = s ?: ""
}
  • 객체가 null인 경우에 대비한 값을 지정하는 경우에 많이 쓰인다.
/* 엘비스 연산자를 활용해 null 값 다루기 */
fun strLenSafe(s: String?) : Int = s?.length ?: 0

>>> println(strLenSafe("abc"))
3
>>> println(strLenSafe(null))
0
  • 예제 6.3 의 countryName 함수도 한 줄로 쓸 수 있다.
fun Person.countryName() = company?.address?.country ?: "Unknown"
  • 함수의 전제 조건precondition 검사하는 경우 엘비스 연산자 우항에 return이나 throw 등을 사용하면 유용하다.
/* throw와 엘비스 연산자 함께 사용하기 */

class Address(
    val streetAddress: String,
    val zipCode: Int,
    val city: String,
    val country: String)

class Company(val nanme: String, val address: Address?)

class Person(val name: String, val company: Company?)

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

>>> val address = Address("중구 세종대로 110", 04524, "서울시", "대한민국")
>>> val cityHall = Company("서울시청", address)
>>> val person = Person("박원순", cityHall)

>>> printShippingLabel(person)
중구 세종대로 110
04524 서울시, 대한민국

>>> printShippingLabel(Person("김연아", null))
java.lang.IllegalArgumentException: No address

6.1.5 안전한 캐스트: as?

  • 코틀린 타입 캐스트에는 asas?가 있다. 안전한 캐스트 연산자는 as? 이다.
  • as 연산자는 자바 타입 캐스트와 만찬가지로 대상 값을 지정한 타입으로 변경하는 연산자이다.
  • as 연산자는 자바와 동일하게 지정한 타입으로 변경 불가능한 경우 ClassCastException이 발생한다.
  • as? 연산자는, as와 다르게, 지정한 타입으로 변경 불가능한 경우 null을 반환한다.
  • 안전한 캐스트는 일반적으로 엘비스 연산자와 함께 사용한다.
/* 안전한 연산자를 사용해 equals 구현하기 */

class Person(val firstName: String, val lastName: String) {
    override fun equals(o: Any?): Boolean {
        val otherPerson = o as? Person ?: return false
        
        /* 안전한 캐스를 하고 나면 스마트 캐스트 된다 */
        return otherPerson.firstName == firstName && 
            otherPerson.lastName == lastName
    }
    
    override fun hashCode(): Int =
        firstName.hashCode() * 37 + lastName.hashCode()
}

>>> val p1 = Person("Dmitry", "Jemerov")
>>> val p2 = Person("Dmitry", "Jemerov")
>>> println(p1 == p2)
true
>>> println((p1.equals(42)))
false

6.1.6 null 아님 단언: !!

  • null 아님 단언non-null assertion은 느낌표를 이중(!!)으로 표기하고, 어떤 값이든 null이 될 수 없는 타입으로 강제로 바꾼다.
  • null에 대해서 !!를 적용하면 NPE가 발생한다.
/* null 아님 단언 사용하기 */

fun ignoreNulls(s: String?) {
    val sNotNull: String = s!!  /* <-- 예외는 이 라인을 가르킨다. */
    println(sNotNull.length)
}

>>> ignoreNulls(null)
Exception in thread "main" kotlin.KotlinNullPointerException
at <...>.ignoreNulls(07_NotnullAssertions.kt:2)
  • 객체가 null인 경우 자바에서는 sNotNull.legnth()에서 NPE가 발생한다. 하지만 코틀린의 !!를 사용하게되면 메소드를 호출하는 곳이 아닌 단언문(!!)을 사용한 곳에서 예외가 발생한다.
  • !!를 사용하여 발생하난 NPE에 대한 스택 트레이스stack trace에는 어떤파일의 몇 번째 줄인지에 대한 정보는 들어 있지만, 어떤 식에서 예외가 발생했는지는 알 수없다. 따라서 아래와 같이 사용하는 일은 피하는 것이 좋다.
/* !! 를 사용할 때 피해야 하는 작성 방법 */
person.company!!.address!!.country  /* <-- company, address 둘 중 누가 NPE를 발생시켰는지 알 수 없다 */

6.1.7 let 함수

  • let 함수를 사용하면 nullable 식을 좀 더 쉽게 다룰 수 있다.
  • let함수와 안전한 호출 연산자를 함께 사용하면 null 체크 후 결과를 변수에 넣는 작업을 간단하게 처리할 수 있다.
  • let을 사용하는 가장 흔한 케이스는 nullable 값을 함수의 non-null 파라미터에 넘기는 경우다.
fun sendEmailTo(email: String) { ... } /* <-- email에 null을 넘길 수 없다 */

>>> val email: String? = ...
>>> sendEmailTo(email)
ERROR: Type mismatch: inferred type is String? but Sting was expected
  • 위 코드를 동작하게 하려면 파라미터를 넘기기 전에 null 검사를 해야 한다.
if (email != null) sendEmailTo(email)
  • 위 코드를 let 함수를 사용하는 코드로 변경하면 아래와 같이 표현할 수 있다.
/* let 함수 사용 */
email?.let { email -> sendEmailTo(email) }

/* 위 코드를 it을 사용하면 아래오 같이 좀 더 줄일 수 있다. */
email?.let { sendEmailTo(it) }
/* let을 사용해 null이 아닌 인자로 함수 호출하기 */
fun sendEmailTo(email: String) {
    println("Sending emial to ${email}")
}

>>> var email: String? = "yole@example.com"
>>> email?.let { endEmailTo(it) }
Sending email to yole@example.com
>>> email = null
>>> email?.let{ endEmailTo(it) }
  • let 함수를 사용하면 null 체크시 추가 변수를 사용하지 않고 검사할 수 있다.
/* let을 사용하지 않고 검사시 아래와 같이 표현할 수 있다 */
var person: Person? = getTheBestPersonInTheWorld()
if (person != null) sendEmailTo(person.email)
/* let을 사용하면 위 코드의 person을 선안할 필요가 없다. 아래는 위 코드와 동일한 의미 */
getTheBestPersonInTheWorld()?.let {sendEmailTo(it.email) }
  • 여러 값에 대해서 null 체크를 해야 한다면 let을 중첩시켜 처리할 수도 있으나, 그러나 이 경우는 코드가 복잡해져서 가독성이 떨어진다. 이 때는 if를 사용하는 편이 낫다.

6.1.8 나중에 초기화할 프로퍼티

  • 코틀린엣는 일번적으로 생성자에서 모든 프로퍼티를 초기화 한다.
  • 프로퍼티 타입이 non-null 타입이라면 반드시 null이 아닌 값으로 초기화 해야 한다.
/* non-null 단언을 사용해 null이 될 수 있는 프로퍼티 접근하기 */
class MyServie {
    fun performAction(): String = "foo"
}

class MyTest {
    private var myService: MyService? = null
    
    @Before fun SetUp() {
        myService = MyService()
    }
    
    @Test fun testAction() {
        /* 반드시 null 가능성에 신경써야 한다. 따라서 !!나 ?를 꼭 써야 한다 */
        Assert.assertEquals("foo", myService!!.performAction())
    }
}
  • 위 코드는 보기가 좋지 않으며 특히 프로퍼티를 여러번 사용해야하면 더욱 가독성이 떨어진다.
  • 이를 해결하기 위해 myService 프로퍼티를 나중에 초기화late-initialized할 수 있다
  • lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화 할 수 있다.
/* 나중에 초기화하는 프로퍼티 사용하기 */
class MyService {
    fun performAction(): String = "foo"
}

class MyTest {
    /* 초기화하지 않고 null이 될 수 없는 프로퍼티를 선언한다. */
    private lateinit var myService: MyService
    
    @Before fun SetUp() {
        myService = MyService()
    }
    
    @Test fun testAction() {
        /* null 체크를 하지 않고 프로퍼티를 사용한다. */
        Assert.assertEquals("foo", myService.performAction())
    }
}
  • lateinit 하는 프로퍼티는 항상 var여야 한다.
  • 프로퍼티를 초기화하기 전에 프로퍼티에 접근하면 “lateinit property myService has not been initialized”라는 예외가 발생한다.

6.1.9 null이 될 수 있는 타입 확장

  • null이 될 수 있는 타입에 대한 확장함수를 정의하면, 확장 함수인 메소드가 알아서 null을 처리해준다. 이런 처리는 확장함수에서면 가능하다.
  • 아래 예를 보자.
/* null이 될 수 있는 수신 객체에 대해 확장 함수 호출하기 */
fun verifyUserInput(input: String?) {
    if (input.isNullOrBlank()) { /* <-- 안전한 호출을 하지 않아도 된다. */
        println("Please fill in the required fields")
    }
}

>>> verifyUserInput(" ")
Please fill in the required fields
>>> verifyUserInput(null)
Please fill in the required fields
  • 위 코드에서 input.isNullOrBlank()에 input이 null 임에도 NPE가 발생하지 않고 정상 동작하고 있다.
  • isNullOrBlank의 구현은 아래와 같다.
fun String?.isNullOrBlank(): Boolean =
    this == null || this.isBlank()

6.1.10 타입 파라미터<T>의 null 가능성

  • 코틀린에서는 함수나 클래스의 모든 타입 파라미터는 기본적으로 null이 될 수 있다.
  • 타입 파라미터 에 대해서 클래스나 함수 안에서 특정 타입을 명시하고 이름 끝에 물음표가 없더라고 T는 null이 될 수 있다. 아래 예를 보자.
/* null이 될 수 있는 타입 파라미터 다루기 */
fun <T> printHashCode(t: T) {
    println(t?.hashCode()) /* <-- t가 null이 퇼 수 있으므로 안전한 호출을 써야 한다 */
}

>>> printHashCode(null) /* <-- T의 타입은 Any?로 추론 된다 */
null
  • 타입 파라미터가 non-null임을 확실히 하려면 non-null 타입 상한upper bound을 지정해야 한다.
  • 타입 상한을 지정하면 null이 될 수 있는 값을 거부하게 된다.
/* 타입 파라미터에 대해 null이 될 수 없는 상한을 지정하기 */
fun <T: Any> printHashCode(t: T) { /* <-- <T: Any>로 선언했기 때문에 t는 non-null 타입이다 */
    println(t.hashCode())
}

>>> printHashCode(null)
Error: Type parameter bound for `T` is not satisfied
>>> printHashCode(42)
42

6.2 코틀린의 원시 타입

6.2.1 원시 타입: Int, Boolean 등

  • 자바는 원시 타입primitive type과 참조 타입referene type을 구분한다.
  • 코틀린은 원시 타입과 래퍼 타입을 구분하지 않고 항상 같은 타입을 사용한다.
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)
  • 대부분의 경우, 코틀린의 Int 타입은 자바 int 타입으로 컴파일 된다.
  • 코틀린의 Int 타입을 컬렉션 제네릭 로 넘기는 경우는 java.lang.Integer로 컴파일 된다.

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

  • 코틀린에서 null이 될 수 있는 원시 타입을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 된다.
/* null이 될 수 있는 원시 타입 */
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    
    }
}

>>> println(Person("Sam", 35).isOlderThan(Person("Amy", 42)))
false
>>> println(Person("Sam", 35).isOlderThan(Person("Jane")))
null
  • Person 클래스에 선언된 age 프로퍼티의 값은 java.lang.Integer로 저장 된다.

6.2.3 숫자 변환

  • 코틀린과 자바의 가장 큰 차이점 중 하나는 숫자를 변환하는 방식이다.
  • 코틀린은 한 타입의 숫자를 다른 타입으로 자동변환 하지 않는다.
  • Int -> Long 으로 변환하는 경우 조차도 명시적 변환이 없는 경우 컴파일러가 거부한다.
val i = 1
val l = Long = i  /* <-- "Error: type mismatch" 컴파일 오류 발생 */
  • 반드시 아래와 같이 변환 메소드를 사용해야 한다.
val i = 1
val l: Long = i.toLong()
  • 코틀린은 Boolean을 제외한 모든 원시 타입에 대한 변환 함수를 제공한다.
  • 코틀린은 개발자의 혼란을 피하기 위해 타입 변환을 명시적으로 처리하기로 결정했다.
  • 묵시적 변환을 허용한다면 아래와 같은 문제가 발생한다.
val x = 1  /* Int */
val list = listOf(1L, 2L, 3L) /* Long 리스트 */
x in list /* 묵시적 타입 변환으로 인해 false 임 */
  • 위 코드에서 x in list는 false 이다. 이 코드가 컴파일 되는 경우 잠재적 오류를 포함하게 된다. 따라서 코틀린은 명시적 타입 변환을 하도록 결정 되었다.
  • 코틀린에서는 위 코드는 아래와 같이 작성해야 한다.
val x = 1
println(x.toLong() in listOf(1L, 2L, 3L))
true
  • 숫자 리터럴 타입이 알려진 변수에 대입하거나, 함수의 인자로 넘기는 경우 컴파일러가 변환을 자동으로 해준다.
fun foo(l: Long) = pinrtln(l)

>>> val b: Byte = 1 /* <-- 상수 값는 적절한 타입으로 해석 된다. */
>>> val l = b + 1L  /* <-- +는 Byte와 Long을 인자로 받을 수 있다. */
>>> foo(42)         /* <-- 함수 인자이므로 42를 Long으로 해석한다. */
42

6.4.2 Any, Any?: 최상위 타입

  • 자바에서 Object가 클래스 최상위 타입이라면, 코틀린은 Any가 그 역할을 한다.
  • 자바에서는 원시 타입은 Object에 포함되지 않지만, 코틀린에서는 Any에 원시 타입도 포함된다.
  • 자바와 마찬가지로, 원시 타입을 Any 타입 변수에 대입하면 자동으로 박싱이 된다.
val answer: Any = 42
  • Any 타입 변수에는 null이 들어갈 수 없다. null을 허용하려면 Any? 타입을 사용해야 한다.
  • 내부적으로 Any는 자바의 java.lang.Object로 컴파일 된다.
  • 모든 코틀린 클래스에는 toString, equals, hashCode 라는 3개의 메소드가 있다. 하지만 java.lang.Object에 있는 다른 메소드(wait나 notify 등)는 Any에서 사용할 수 없다. 그런 메소드를 사용하고 싶다면 java.lang.Object로 캐스트해야 한다.

6.2.5 Unit 타입: 코틀린의 void

  • 코틀린의 Unit 타입은 자바의 void와 같은 기능을 한다.
fun f(): Unit { ... }
  • 위 코드는 아래와 동일 하다.
fun f() { ... }
  • 대부분의 경우 void와 Unit의 차이를 알기는 어렵다.
  • 하지만 Unit은 viod와 달리 타입으로 처리 되므로 일반적으로 타입이 사용 되는 모든 곳이 사용할 수 있다.
  • 따라서 아래와 같은 코드를 만들 수 있다.
interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> {
    override fun process() {
        // 업무 처리 코드
        
    }
}

class ResultProcessor : Processor<Integer> {
    override fun process(): Int {
        // 업무 처리 코드
        return someInt
    }
}
  • 자바와 같은 경우는 위 코드와 같이 메소드 리턴이 void와 Integer인 케이스를 하나의 인터페이스로 처리할 수 없다.

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

  • 함수가 정상적으로 끝나지 않는 사실을 표현하려면 코틀린의 Nothing을 사용하면 된다.
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

>>> fail("Error occurred")
java.lang.IllegalStateException: Error occurred
  • Nothing 타입은 아무 값도 포함하지 않는다.
  • Nothing은 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 사용 가능하다. 그 외의 용도로 쓰는 경우 아무런 의미가 없다.
  • Nothing을 반환하는 함수를 엘비르 연산자의 우항에 사용하면 전제 조건precondition을 검사 할 수 있다.
val address = company.address ?: fail("No address")
println(address.city)
  • 컴파일러는 함수가 Nothing을 반환하면 이 함수가 정상 종료되지 않음을 알고 그 함수를 호출하는 코드를 분석할 때 사용한다.
  • 위 코드에서는 company.address가 null인 경우 예외가 발생한다는 사실을 파악하고 null이 아님을 추론할 수 있다.

6.3 컬렉션과 배열

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

  • 코틀린 컬렉션은 자바와 다르게 읽기 전용 컬렉션과 변경 가능한 컬렉션이 분리되어 있다.
  • Collection에는 원소를 추가하거나 제거하는 메소드가 없다. 원소를 추가하거나 제거하려면 MutableCollection을 사용해야 한다.
/* 읽기 전용과 변경 가능한 컬렉션 인터페이스 */

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

>>> val source: Collection<Int> = arrayListOf(3, 5, 7)
>>> val target: MutableCollection<Int> = arrayListOf(1)
>>> copyElements(source, target)
>>> println(target)
[1, 3, 5, 7]
  • 위 코드에는 target에 읽기 전용 컬렉션을 넘길 수 없다. 읽기 전용 컬렉션을 넘기면 컴파일 에러가 발생한다.
>>> val source: Collection<Int> = arrayListOf(3, 5, 7)
>>> val target: Collection<Int> = arrayListOf(1)
>>> copyElements(source, target)
Error: Type mismatch: inferred type is Collection<Int>
    but MutableCollection<Int> was expected
  • Collection에 MutableCollection를 할당하는 것은 가능하지만 반대의 경우는 에러가 발생한다.
/* MutableCollection이 Collection 참조 */
val collection: Collection<Int> = arrayListOf(1, 2, 3)
val mutableCollection: MutableCollection<Int> = collection
error: type mismatch: inferred type is Collection<Int> but MutableCollection<Int> was expected


/* Collection이 MutableCollection 참조 */
val mutableCollection: MutableCollection<Int> = arrayListOf(1, 2, 3)
val collection: Collection<Int> = mutableCollection /* <-- 가능 */
>>> collection
[1, 2, 3]
>>> mutableCollection
[1, 2, 3]
>>> mutableCollection.add(5)
>>> mutableCollection
[1, 2, 3, 5]
>>> collection
[1, 2, 3, 5]    /* <-- mutalbeCollection에 5를 추가했지만 collection에도 영향을 미친다 */
  • 주의해야 할 점은 위 코드에서 보듯 Collection이 MutableCollectio을 참조 가능하므로 Collection이라고 해서 반드시 수정 불가능한 것은 아니다.
  • 위와 같은 경우에는 ConcurrentModificationException이나 다른 오류가 발생할 수 있다.
  • 따라서 읽기 전용 컬렉션이 항상 Thread-safe하지 않다는 점을 명심해야 한다.

6.3.3 코틀린 컬렉션과 자바

  • 코틀린에서 읽기 전용인 Collection으로 선언된 객체라도 자바코드에서는, 이를 구분하지 않으므로, 내용을 변경할 수 있다.
/* 자바 코드 */
// CollectionUtils.java

public class CollectionUtils {
    public static List<String> uppercaseAll(List<String> items) {
        for (int i = 0; i < items.size(); i++) {
            items.set(i, items.get(i).toUpperCase());
        }
        
        return items;
    }
}
/* 코틀린 코드 */
// collections.kt

fun printInUppercase(list: List<String>) { /* <-- list는 읽기 전용이다 */
    println(CollectionUtils.uppercaseAll(list)) /* <-- list가 읽기전용이지만 자바에서 변경 된다 */
    println(list.first())
}

>>> val list = listOf("a", "b", "c")
>>> printInUppercase(list)
[A, B, C]
A

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

  • 코틀린의 배열은 타입 파라미터<T>를 받는 클래스이다.
/* 배열 사용하기 */

fun main(args: Array<String>) {
    for (i in args.indices) {
        println("Argument ${i} is: ${args[i]}")
    }
}
/* 알파벳으로 이루어진 배열 만들기 */

>>> val letters = Array<String>(26) { i -> ('a' + i).toString() }
>>> println(letters.joinToString(""))
abcdefghijklmnopqrstuvwxyz
  • Array(26)... 처럼 타입 인자를 굳이 지정했지만 생략하더라도 컴파일러가 추론해준다.
  • 코틀린 원시 타입의 배열을 표현하는 클래스를 각 원시 타입마다 하나씩 제공한다. 예를 들면 Int 타입의 배열은 IntArray이다. ByteArray, CharArray 등의 원시 타입 배열을 제공한다. 컴파일 될 때는 자바의 int[], byte[], char[] 등으로 컴파일 된다.
  • 코틀린 표준 라이브러리는 배열의 기본 연산에 더해 컬렉션에 사용할 수있는 모든 확장 함수를 사용할 수 있다. 자바에서는 컬렉션의 메소드를 배열에서 사용할 수 없다.