Kotlin In Action 6장

코틀린 타입 시스템

코틀린 타입 시스템에서 자바와 가장 큰 차이를 보이는 부분은 Nullable Type읽기 전용 컬렉션일 것이다.

지금부터 코틀린의 타입 시스템에 대해서 조금 더 자세히 살펴보자.

널 가능성

코틀린에서는 기본적으로 모든 데이터 타입이 Non-Nullable Data Type이다.

이는 자바 개발자들에게 고통을 안겨준 NPE를 방지하기 위함이다.

그리고 이러한 Nullable CheckingNPE의 발생 위험을 컴파일 시점에 알아차릴 수 있게 해준다.

val a: String = null // error!
val b: String? = null // OK!

? (물음표)를 붙여야 Nullable Type으로 선언할 수 있다.

타입의 의미

그러면 조금 더 철학적인 질문을 던져보자.

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

위키피디아에서는 타입의 의미에 대해 다음과 같이 적혀있다.

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

이 정의를 자바에 적용해보자.

자바의 double 타입을 예를 들어, double 타입은 64비트 부동소수점 수를 표현할 수 있는 자료형이다.

double 타입은 일반 수학 연산을 수행할 수 있다.

따라서 double 타입의 변수에 대한 연산을 컴파일러가 통과시킨 경우 그 연산이 성공적으로 실행되리란 사실을 확인할 수 있다.

자 그러면 Stringdouble을 비교해보자.

누구나 알듯이 Stringdouble은 완전히 다른 값을 지원한다.

String 타입의 변수에는 문자열이나 null을 대입할 수 있다.

그러나 이 두 종류의 값은 완전히 다르다. 심지어 자바의 instanceof 연산자도 nullString이 아니라고 판단한다.

그리고 String이 들어간 변수의 경우 String 클래스에 정의된 모든 메소드를 호출할 수 있다.

하지만 null이 들어있는 경우 사용할 수 있는 메소드가 많지 않다.

이는 자바의 타입 시스템이 널을 제대로 다루지 못한다는 뜻이다.

즉, 자바는 타입 시스템 자체에서 널을 판별할 수 없기 때문이다.

물론 자바 8에서 Optional이 추가되며 null을 감싸는 래퍼 타입을 활용할 수 있기는 하다.

그러나 Optional의 경우 그 한계가 매우 명확하다.

가령 코드가 매우 지저분해지며 래퍼가 추가됨에 따라 실행 시점에 성능이 저하되고, 전체 에코 시스템에서 일관성있게 사용하기가 어렵다.

우리가 작성하고 있는 코드에서 Optional을 사용해 널을 처리하더라도 JDK 메소드나 다른 프레임워크에서 제공하는 기능이나 써드파티 라이버르리에서 반환되는 널을 여전히 처리해야만 한다.

안전한 호출 연산자 ?.

코틀린이 제공하는 가장 유용한 도구 중 하나가 바로 안전한 호출 연산자인 ?.이다.

?.은 null 검사와 메소드 호출을 한번의 연산으로 수행하기 때문에 더욱 더 직관적이고 간결한 코드를 작성할 수 있다.

가령 foo?.bar()를 호출했을 때 foonull이 아니라면 foo.bar()를 호출할 것이고, 만약 foonull이라면 뒤의 bar() 메소드는 수행하지 않고 그대로 null을 반환하게 된다.

엘비스 연산자 ?:

엘비스 연산자는 연산의 결과가 null인 경우 별도의 처리를 위해 사용한다.

앞서 foo?.bar()를 예로 들어보자.

foo?.bar() ?: println("Null!")

엘비스 연산자를 사용하여 foonull이 되는 경우 Null!을 출력하는 처리가 가능하게 되었다.

이런식으로 엘비스 연산자는 null을 스마트하게 처리할 수 있는 기능을 제공하고 있기 때문에 많이 사용된다.

안전한 캐스트 as?

2장에서 타입 캐스트 연산자인 as에 대해 살펴보았다.

as 역시 안전한 처리가 가능한데, 다음을 보자

foo = foo as? Int
// if foo is Int, foo cast Int
// but foo is not Int, it will retunn null

널 아님 단언 !!

널 단언은 코틀린에서 널이 될 수 있는 타입의 값을 다룰 때 사용할 수 있는 도구 중 가장 단순하면서 무딘 도구다.

어떤 값이든 널이 될 수 없는 타입으로 강제로 바꿀 수 있다.

가령 foo가 Nullable 타입이라고 하더라도, !!를 사용하여 컴파일러의 경고를 무시할 수 있다.

우리는 !!가 약간은 무례하다고 느낄지도 모른다.

하지만 이는 코틀린 개발자들이 의도한 바이다.

그들은 !!기호가 마치 컴파일러에게 소리를 지르는 것 같은 느낌을 들게 만든다.

코틀린 설계자들은 컴파일러가 검증할 수 없는 단언을 사용하기 보다는 더 나은 방법을 찾아보라는 의도를 넌지시 표현하고자 !!라는 못생긴 기호를 사용했다.

그렇기 때문에 !!는 남발하지 않는 편이 좋다는 것 또한 알 수 있다.

let 함수

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

가령 앞서 본 foo 예제의 경우 다음과 같이 사용할 수 있다.

foo?.let {
    println("Not Null!")
}

foo가 널이 아닌 경우라면 let 함수 내부의 블럭을 실행한다.

이는 엘비스 연산자와 조합하여 더욱 우아하게 사용할 수 있다.

foo?.let {
    println("Not Null!")
} ?: println("Null!")

나중에 초기화할 프로퍼티

객체를 일단 선언한 다음 나중에 초기화하는 프레임워크가 많다.

가장 대표적으로 Spring의 인터페이스 빈 주입이 그런 경우일 것이다.

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

코틀린에서는 일반적으로 생성자에서 모든 프로퍼티를 초기화해야한다.

게다가 프로퍼티 타입이 널이 될 수 없는 타임이라면 반드시 널이 아닌 값으로 그 프로퍼티를 초기화해야한다.

그런 초기화 값을 제공할 수 없는 경우라면 널이 될 수 있는 타입을 사용할 수 밖에 없는 것이다.

하지만 널이 될 수 있는 타입을 사용하면 모든 프로퍼티 접근에 널 검사나 !! 연산자를 사용해야한다.

코틀린도 이를 알고 특별한 키워드를 제공하고 있는데, 그것은 lateinit이다.

lateinit 변경자를 붙이면 Non-Nullable Type도 프로퍼티를 나중에 초기화 할 수 있다.

널이 될 수 있는 타입 확장

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

널이 될 수 있는 타입의 변수가 널이 될 수 있는 타입의 확장 함수를 사용할 때 안전한 호출 없이도 호출이 가능한 것이다.

코틀린에선 어떻게 널이될 수 있는 타입의 확장함수를 선언하면 이를 알아차릴 수 있는 것일까?

스크린샷 2020-04-12 오후 6 45 51 스크린샷 2020-04-12 오후 6 45 07

실험을 위해 hello라는 확장함수를 만들고 호출해보았다.

결과는 예상대로 Hello Harry가 출력되는 것을 볼 수 있다.

그리고 확장함수인 hello는 static final 메소드로 선언이되며 @Nullable 애노테이션이 붙어 있는 것을 볼 수 있다.

즉, 호출 시 호출 객체가 null이더라도 받아줄 수 있게 되어있다는 뜻이다.

다음은 null을 넣어서 호출한 코드이다.

스크린샷 2020-04-12 오후 6 45 45 스크린샷 2020-04-12 오후 6 46 16

자바에서는 NullabeNon-Nullable을 구분하지 않기 때문에 확장 함수에서는 별도의 Null Checking 없이도 확장 함수를 사용할 수 있는 것이다.

타입 파라미터의 널 가능성

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

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

T는 추론한 타입이 널이 될 수 있는 Any? 타입이다.

t 파라미터의 타입 이름 T에는 물음표가 없지만 t는 null을 받을 수 있다.

T를 사용하면서 널이 될 수 없음을 명시하고 싶은 경우라면 타입 상한을 지정해야한다.

다음과 같이 가능하다.

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

널 가능성과 자바

자바에서는 널 가능성을 지원하지 않는다.

그러면 자바와 코틀린을 조합하면 어떤 일이 생길까?

그 둘을 조합한 프로그램은 안전하지 않게 될까?

자바 코드에서는 애노테이션으로 널 가능성 정보를 표현할 수 있다.

@Nullable이 그것이다.

가령 자바에서 @Nullable String이라는 타입이 존재한다면 이는 코틀린에서 봤을 때 String?와 같은 의미를 갖는다.

자바에서 @NotNull String의 타입은 코틀린에서 String과 같은 것이다.

코틀린은 여러 널 가능성 애노테이션을 알아본다.

JSR-305 표준이라던지 안드로이드, 젯브레인 도구들이 지원하는 애노테이션 등 코틀린이 이해할 수 있는 애노테이션들을 확인한다.

그러나 만약 이러한 널 가능성 애노테이션이 소스코드에 없는 경우라면 자바 타입은 코틀린 플랫폼 타입이 된다.

플랫폼 타입

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

해당 타입을 널이 될 수 있는 타입으로 처리할 수도 있고 반대도 가능하다.

즉 이는 플랫폼 타입에 대해 수행하는 모든 연산에 대한 책임은 우리에게 있다는 것을 의미한다.

코틀린은 보통 널이 될 수 없는 타입의 값에 대해 널 안전성 검사하는 연산을 수행하면 경고를 표시하지만

플랫폼 타입의 값은 아무 경고도 표시하지않는다.

즉 자바와 마찬가지로 우리가 플랫폼 타입을 제대로 다루지 못할 경우 컴파일러는 NPE를 발생시킬 것이다.

그렇다면 어째서 코틀린은 플랫폼 타입을 도입했을까?

모든 자바 타입을 널이 될 수 있는 타입으로 다루면 되는데 말이다.

이는 모든 타입을 널이 될 수 있는 타입으로 다루면 결코 널이 될 수 없는 값에 대해서도 불필요한 널 검사가 들어가기 때문이다.

특히 제네릭을 다룰 때 최악의 상황을 맞이하게 되는데,

예를 들어 자바 ArrayList<String>을 코틀린에서 ArrayList<String?>?으로 다루면 배열 원소에 접근할 때 마다 널 검사를 수행하거나 안전한 캐스트를 수행해야한다.

하지만 이런 처리는 이익보다 검사에 드는 비용이 훨씬 많이 들게 된다.

또한 모든 타입의 값에 대해 항상 널 검사를 작성하는 것은 너무 성가신 일이다.

그래서 코틀린 설계자들은 자바의 타입을 가져온 경우 프로그래머에게 그 타입을 제대로 처리할 책임을 부여하는 실용적인 접근 방법을 택했다.

코틀린의 Primitive 타입

지금부터는 코틀린에서 사용하는 Int, Boolean, Any 등의 Primitive 타입에 대해 살펴보자.

코틀린은 PrimitiveWrapper 타입을 구분하지 않는다.

그러면 어떻게 Primitive 타입에 대한 래핑이 작동하는지도 알아보자.

또한 Object,Void 등의 자바 타입과 코틀린 타입 간의 대응 관계에 대해서도 살펴보자.

Primitive Type : Int, Boolean …

알다시피 자바에서는 Primitive TypeReference Type을 구분한다.

Primitive Type의 경우 값이 직접 들어가지만,

Reference Type의 경우 메모리 상의 위치가 들어가게 된다.

알다시피 자바 컬렉션에서는 Primitive Type을 담을 수 없다.

이를 위해 특별한 래퍼타입으로 Primitive Type을 감싸서 사용하는게 자바에서는 일반적이었다.

코틀린은 Primitive TypeReference Type을 구분하지 않는다.

Reference Type을 구분하지 않기 때문에 편리하다.

val i: Int = 4
val list:List<Int> = listOf(1,2,3,i)

그러면 여기서 우리는 의문이 생긴다.

Primitive TypeReference Type이 같다면 코틀린은 항상 이들을 객체로 표현하는 것일까?

그러면 너무 비효율적이지 않나..?

실제로 항상 객체로 표현하면 비효율적이지만 코틀린은 그렇지 않다.

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

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

이렇게 컴파일이 안되는 경우는 컬렉션과 같은 제네릭 클래스를 사용하는 경우 뿐이다.

에를 들어 Int 타입을 컬렉션의 타입 파라미터로 넘기면 그 컬렉션에는 Int의 래퍼 타입에 해당하는 java.lang.Integer 객체가 들어간다.

자바의 Primitive Type을 코틀린에서 사용할 때도 (플랫폼 타입이 아닌) 널이 될 수 없는 타입으로 취급할 수 있다.

널이 될 수 있는 Primitive Type : Int?, Boolean?

코틀린에서 널이 될 수 있는 타입은 자바의 Primitive Type으로 표현할 수 없다.

따라서 코틀린에서 널이 될 수 있는 Primitive Type을 사용하면 그 타입은 자바의 래퍼 타입으로 컴파일 된다. (흠.. 그러면 성능상 이슈가 있을 수 있으니 가급적이면 널이 될 수 없는 Primitive Type을 사용하는게 좋겠네)

또한 앞서 이야기한 대로 제네릭 클래스의 경우 래퍼 타입을 사용한다.

어떤 클래스의 타입 인자로 원시 타입을 넘기면 코틀린은 그 타입에 대한 박스 타입을 사용한다.

이렇게 컴파일 하는 이유는 자바 JVM에서 제네릭을 구현하는 방법 때문이다.

JVM은 제네릭 타입 인자로 Primitive Type을 허용하지 않는다.

따라서 자바나 코틀린 모두 제네릭 클래스는 항상 박스 타입을 사용해야한다.

만약 Primitive Type으로 이루어진 효율적인 컬렉션을 사용하고 싶다면 서드파티 라이브러리를 사용하거나 배열을 사용해야한다.

숫자 변환

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

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

val i = 1
val l: Long = i // 타입 미스매칭 Error!

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

val i = 1
val l:Long = i.toLong()

이는 때때로 불편하다고 느끼게 되는데, 사실 이렇게 숫자 변환을 까다롭게 하는데에는 다 이유가 있다.

코틀린 개발자들은 혼란을 피하기 위해 타입 변환을 명시하기로 결정하였다.

특히 박스 타입을 비교하는 경우 문제가 많은데, 두 박스 타입 간의 equals() 메소드는 그 안에 들어있는 값이 아니라 박스 타입 객체를 비교한다.

자바에서 예를들면,

new Integer(42).equals(new Long(42)) == false // True!

박스 내부의 값이 같더라도 박스 타입 객체가 다르기 때문에 false를 반환한다.

코틀린 역시 이러한 문제를 회피하기 위해 숫자 타입간의 타입 변경 또한 명시적으로 변환하게끔 하였다.

Any, Any? : 최상위 타입

자바에서 모든 클래스의 최상위 계층인 Object가 있었다면 코틀린에서는 Any가 그 역할을 대신한다.

그러나 자바에서는 Reference Type에서만 Object가 최상위 클래스지

Primitive Type의 경우 해당 계층에 포함되지는 않았다.

그러나 코틀린에서는 AnyPrimitive Type을 포함해 모든 타입의 조상 타입이다.

코틀린의 Any는 자바의 Object와 대응한다.

그래서 자바 메소드에서 Object를 인자로 받거나 변환하면 코틀린에서는 Any로 취급한다.

물론 널이 될 수 있는지 없는지 여부를 알 수 없으므로 플랫폼 타입인 Any!로 취급하는 것이 더 정확하겠다.

코틀린 함수가 Any를 사용하면 자바 바이트코드의 Object로 컴파일된다.

Unit Type : 코틀린식 void

자바의 void와 코틀린의 Unit은 뭐가 다를까?

Unit은 모든 기능을 갖는 일반적인 타입이며, void와는 달리 Unit을 타입 인자로 쓸 수 있다.

Unit 타입에 속한 값은 Unit 하나 뿐이다.

Unit 타입의 함수는 묵시적으로 Unit 값을 반환한다.

그러면 코틀린에서는 왜 Void가 아니라 Unit이라는 다른 이름을 골랐는지 궁금할 것이다.

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

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

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

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

Nothing 타입은 이럴 때 사용하는데, 아무 값도 포함하지 않으며 주로 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 쓸 수 있다.

컬렉션과 배열

코틀린 컬렉션이 자바 라이브러리를 바탕으로 만들어졌고 확장 함수를 통해 기능을 추가한다는 사실을 우리는 이전에 공부해서 알고 있다.

하지만 여전히 코틀린의 컬렉션 지원과 자바와 코틀린 컬렉션 간의 관계에 대해 이야기할만한 내용들이 남았다.

널 가능성과 컬렉션

컬렉션에서 널이 될 수 있는 변수를 저장할 수 있는 경우를 생각해보자.

List<Int?>는 컬렉션 내부에 Int 값 또는 null 값이 저장될 수 있음을 의미한다.

List<Int>?는 컬렉션을 가르키는 변수가 null이 될 수 있음을 의미하며 내부에는 Int 값만 저장할 수 있음을 의미한다.

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

코틀린 컬렉션과 자바 컬렉션을 나누는 가장 중요한 특성 중 하나는 코틀린에서는 컬렉션 안의 데이터에 접근하는 인터페이스와 컬렉션 안의 데이터를 변경하는 인터페이스를 분리했다는 점이다.

kotlin.collections.Collection은 컬렉션 안의 원소에 대해 이터레이션하고, 컬렉션의 크기를 얻고, 어떤 값이 컬렉션 안에 들어있는지 검사하며 컬렉션에서 데이터를 읽는 여러 다른 연산들을 수행할 수 있다. 하지만 Collection에는 원소를 추가하거나 제거하는 메소드가 없다.

컬렉션의 데이터를 수정하려면 kotlin.collections.MutableCollection 인터페이스를 사용해야한다.

여기서 중요한 점은 읽기 전용 컬렉션이라고 해서 꼭 변경 불가능한 컬렉션일 필요는 없다는 것이다.

물론 코틀린은 이후 표준 라이브러리에 불변 컬렉션을 추가할 것이라고 하였다.

다만 읽기 전용 인터페이스 타입인 변수를 사용할 때 그 인터페이스는 실제로는 어떤 컬렉션 인스턴스를 가라키는 수많은 참조 중 하나일 수 있다는 의미이다.

가령 같은 인스턴스를 가리키는 변경 가능한 인터페이스 타입의 참조가 있을 수도 있다.

이 경우 읽기 전용 컬렉션은 변경의 영향을 받게 된다.

물론 하나의 컬렉션 객체를 가르키는 다른 타입의 참조가 동시에 이 컬렉션 객체의 내용을 변경하려고 하는 경우 ConcurrentModificationException이나 다른 오류가 발생할 수 있다.

따라서 읽기 전용 컬렉션이 항상 Thread Safe하지는 않다는 점을 명심해야한다.

다중 스레드 환경에서 데이터를 다루는 경우 그 데이터를 적절히 동기화 하거나 동시 접근을 허용하는 데이터 구조를 활용해야한다.

그러면 코틀린은 어떻게 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하는 걸까?

앞에서 코틀린의 컬렉션은 자바 컬렉션과 동일하다고 했는데 그러면 이는 모순이 아닐까?

코틀린 컬렉션과 자바

무슨 일이 벌어지고 있는지 조금 더 자세히 알아보자.

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

그러나 코틀린은 모든 자바 컬렉션 인터페이스마다 읽기 전용 인터페이스와 변경 가능한 인터페이스 두 가지를 제공한다.

collections-diagram

이 컬렉션 구조만 보더라도 코틀린의 철학을 세삼 엿볼 수 있다.

함수형 프로그래밍에서는 기본적으로 불변성을 강조하고 있지만, 그 역시도 코틀린은 개발자에게 전적으로 맡기고 자유를 준다.

뭘 선택해서 개발할지는 니가 정해. 우리는 만들어만 놓을게라느 느낌을 받기도 한다.

자 그러면 여기서 궁금한 점이 생긴다.

자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다.

코틀린에서 읽기 전용 컬렉션으로 선언된 객체라도 자바 코드에서는 그 컬렉션 객체의 내용을 변경할 수 있다.

코틀린 컴파일러는 자바 코드가 컬렉션에 대해 어떤 일을 하는지 완전히 분석할 수 없다.

따라서 컬렉션을 변경하는 자바 메소드에 읽기 전용 컬렉션을 넘겨도 코틀린 컴파일러가 이를 막을 수 없다는 이야기이다.

이 마저도 코틀린은 우리에게 모든 것을 열어놓고 알아서 쓰셈 대신 책임은 니가 지는거고 ㅋㅋ라는 뉘앙스를 풍긴다.

따라서 컬렉션을 자바 코드에게 넘길 때는 특별히 더 주의를 기울여야 하며 코틀린 쪽 타입이 자바 쪽에서 컬렉션에게 가할 수 있는 변경의 내용을 반영하게 해야한다.

객체의 배열과 Primitive 타입의 배열

코틀린의 배열은 다음과 같이 사용 가능하다.

val arr:Array<Int> = arrayOf(1,2,3)

코틀린에서 배열을 만들 수 있는 방법은 다양하다.

  • 위의 예시처럼 arrayOf 함수에 원소를 넘긴다.

  • arryOfNulls 함수에 정수 값을 넘기면 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다. 물론 원소 타입이 널이 될 수 있는 경우에만 사용 가능

  • Array 생성자는 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화해준다.

마지막 Array 생성자는 예시를 보면 더 와닿는다.

val letters = Array<String>(26) { i -> ('a' + i).toString()}

컬렉션을 배열로 변환하기 위한 함수 또한 존재한다. toTypedArray 메소드를 사용해보자.

val strings = listOf("a","b","c")
println("%s/%s/%s".format(*strings.toTypedArray()))

*이 붙는 이유는 vararg 인자를 넘기기 위해 스프레드 연산자인 *를 사용한 것이다.

그러면 Primitive Type의 배열도 만들어보자.

코틀린은 Primitive Type의 배열을 표현하는 별도 클래스를 각 Primitive Type마다 하나씩 제공한다.

예를 들어 Int의 경우 IntArray이다.

이 외에도 ByteArray, CharArrayPrimitive Type의 배열을 제공한다.

이 모든 타입은 당연히 자바 Primitive Type 배열인 int[], byte[], char[] 등으로 컴파일 된다.

Primitive Type 배열을 만드는 방법은 다음과 같다.

  • 각 배열 타입의 생성자는 size 인자를 받아서 해당 Primitive Type의 디폴트 값(보통은 0)으로 초기화된 size 크기의 배열을 반환한다.
  • 팩토리 함수는 여러 값을 가변 인자로 받아서 그런 값이 들어간 배열을 반환한다.
  • 크기와 람다를 인자로 받는 생성자를 사용한다.

직접 예시를 보자.

val fiveZeros = IntArray(5)
val fiveZerosToo = intArrayOf(0,0,0,0,0)
val squares = IntArray(5) {i-> (i+1)*(i+1)}

코틀린에서는 배열 기본 연산에 더해 컬렉션에 사용할 수 있는 모든 확장 함수를 배열에도 제공해주고 있다.

따라서 컬렉션에서 사용했던 filtermap같은 확장 함수를 Primitive Type으로 이루어진 배열에서도 똑같이 사용할 수 있다.

listOf 메소드에 대한 고찰 (feat. Kotlin Collection과 Java Collection)

listOf 메소드를 사용하다 문득 의문이 들었다.

스크린샷 2020-04-12 오후 7 39 07

listOfList<T> 인터페이스를 반환하고 있다. 그러나 List<T>는 인터페이스 타입이기 때문에 인스턴스로 만들 수 없다.

그래서 listOf 함수가 어떤 값을 반환하는지 궁금해져서 찾아보았다.

스크린샷 2020-04-12 오후 7 39 27

Collections.kt에 정의된 listOf 메소드는 asList() 확장함수를 호출하고 있었다.

스크린샷 2020-04-12 오후 7 39 34

asList_ArraysJvm.kt에 정의된 확장함수였고, ArrayUtilJVM 객체의 asList를 호출하고 있었다.

스크린샷 2020-04-12 오후 7 39 41

놀랍게도 ArraysUtilJVM은 자바 코드였고, 여기서 자바 ArraysasList를 호출하고 있었다.

스크린샷 2020-04-12 오후 7 39 46

그리고 알다시피 ArraysasListArrayList를 인스턴스로 반환하고 있었다.

여기서 든 한가지 의문은, 어떻게 코틀린은 자바의 ArrayList를 사용하면서 불변 상태를 유지할 수 있는가?에 대한 부분이었다.

알다시피 listOf로 생성된 리스트는 Immutable한 성질을 갖게 된다.

그러나 자바 코드에서는 Immutable이라는 개념이 컬렉션에 없기 때문에 나는 이 부분이 궁금하였다.

스크린샷 2020-04-12 오후 7 01 49

Kotlin의 List에는 add 메소드가 없다.

그런데 생각해보니, 코틀린의 컬렉션은 별도로 구현된 컬렉션이 아니며 자바의 표준 컬렉션을 그대로 사용한다는 것이 생각이 났다.

즉, 코틀린은 자바의 컬렉션을 사용하고 있으며, 대신 Mutable한 컬렉션을 별도로 두고, 기존의 CollectionImmutable하게 유지하게 함으로써 제한을 둔 것 외에는 자바의 표준 라이브러리를 그대로 사용하고 있다.

아래의 글은 내가 이 내용에 대해서 찾아보다가 찾은 글인데, 아주 잘 정리되어있어서 가져와 보았다.

스크린샷 2020-04-12 오후 7 49 24

출처 : https://steelkiwi.com/blog/collections-kotlin/

가장 중요한 부분은 import java.util.*이다.

이를 통해 자바와 코틀린 사이의 Collection 사용이 자유롭고, 자바의 Collection이 코틀린에서 사용될 경우 Kotlin의 Collection의 규약(Immutable or Mutable)을 따르게 되는 것이다.

아래는 직접 그 예시를 작성해보았다.

스크린샷 2020-04-12 오후 7 38 00

listImmutable의 경우 add 메소드를 사용할 수 없는 반면, MutableList는 add 메소드를 사용할 수 있는 것을 확인할 수 있다.

Reference

Kotlin In Action (드미트리 제메로프, 스베트라나 이사코바)



© 2022. by minkuk

Powered by minkuk