kotlin 관련 책을 보면 항상 kotlin collections 와 java collection 의 관계에 대해 나오는데
매번 어느정도만 이해하고 넘어간 것 같아 한 번 알아보고 정리를 하려 한다.
책이나 여러 글에서 일반적으로 Text 랑 컬렉션의 계층 구조로만 내용을 보니
이해가 잘 안되고 와닿지가 않아서 최대한 내부 코드로 얘기 하려 한다
kotlin in action 의 6.3.3 코틀린 컬렉션과 자바 챕터에서 정리를 마음 먹었기에 이를 살펴보자.
아래는 해당 챕터에서 나오는 내용이다.
모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스라는 점은 사실이다. 따라서 코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없다.
자바 컬렉션 인터페이스의 인스턴스? 코틀린 컬렉션은 인터페이스이지 않나? 인스턴스는 객체가 아닌가?
라는 생각이 들게 되는 문장이었고 멈추게 되었다.
말을 풀어보면 "코틀린 컬렉션을 생성하면 결국 자바 컬렉션 인터페이스가 구현된다"는 의미로 해석이 되고 이걸 자세히 확인해 보자
아래 3개의 List를 예시로 알아보려 한다.
- listOf(vararg elements: T)
- emptyList()
- mutableListOf()
왜 1번의 listOf 에 파라미터가 존재하냐면
존재하지 않으면 내부적으로 emptyList()를 생성하기에 2번과 중복되어, 파라미터가 존재하는 걸 예시로 한다.
Kotlin 에서 listOf 로 List 를 만들면 어떤 일이 일어날까
fun main() {
val ages: List<Int> = listOf(26, 27, 28)
println(ages.javaClass) // class java.util.Arrays$ArrayList
}
이렇게 Int 타입을 내부로 가지는 List 를 예시로 만들었다.
확인해 보면 해당 List의 런타임 시점의 구현체는 java.util.Arrays$ArrayList 로 확인된다.
왜 갑자기 java 가 나오지?
더 알아보기 위해 컴파일된 ByteCode 를 확인해 보자.
java.util.List 타입으로 확인된다.
그럼 타입은 java.util.List 이고 구현체는 java.util.Arrays$ArrayList 로 생각할 수 있다.
이걸 확인하기 위해 listOf 코드를 파고 들어가면
listOf 의 내부 코드는 다음과 같다.
그럼 여기서 asList() 를 들어가 보자.
다시 ArraysUtilJVM.asList 를 들어가 보자.
java.util.List 를 반환하는 모습을 보고 있다.
하지만 인텔리제이가 asList 내부 구현의 디컴파일된 코드는 제공하지 않기에, 직접 다시 컴파일된 ByteCode 를 확인해 보자.
INVOKESTATIC java/util/Arrays.asList ([Ljava/lang/Object;)Ljava/util/List;
를 보게 되면 java.util.Arrays.asList 함수를 호출하는 것을 볼 수 있다. 해당 함수는 자바에서 고정된 크기를 가지는 리스트를 반환하는 함수이다.
또 볼 수 있는 건 반환 타입이 java.util.List 이다.
java.util.Arrays.asList 코드는 아래의 사진에서 볼 수 있다.
보다시피 java.util.List 타입을 반환하지만 구현체는 java.utill.Arrays$ArrayList 이다.
결국 내부적으로 위 과정을 통해
타입은 java.util.List 이지만 구현체는 java.utill.Arrays$ArrayList 인 ages 리스트가 만들어진다.
그런데 평소 우리가
val ages: List<Int> = listOf(26, 27, 28)
이렇게 만들면 해당 변수의 타입은 kotlin.collections.List 로 생각한다. 하지만 컴파일된 ByteCode 를 확인하면 java.util.List 이다.
→ 이게 의미하는 바는 kotlin.collections.List 가 컴파일 과정에서 java.util.List 로 매핑된다.
그럼 이제 책의 아래 말이 이해가 된다.
모든 코틀린 컬렉션은 그에 상응하는 자바 컬렉션 인터페이스의 인스턴스라는 점은 사실이다. 따라서 코틀린과 자바 사이를 오갈 때 아무 변환도 필요 없다.
코틀린 컬렉션을 생성하면 사실 이건 코틀린 컬렉션 인터페이스가 아닌 자바 컬렉션 인터페이스의 인스턴스이다.
더 알아보자.
emptyList() 로 생성한다면?
이전에 listOf() 로 생성했을 때 이는 java.util.Arrays$ArrayList 구현체인 것을 확인했다.
emptyList() 로 빈 List를 생성한다면 어떻게 될까?
fun main() {
val emptyAges = emptyList<Int>()
println(emptyAges.javaClass) // class kotlin.collections.EmptyList
}
구현체가 kotlin.collections.EmptyList 이다. 그럼 얘는 listOf() 랑 다른가? ByteCode를 확인하자
타입은 listOf() 와 똑같은 java.util.List 이다.
조금 전에 kotlin.collections.List 가 컴파일 과정에서 java.util.List 로 매핑된다는 사실을 알아차렸다.
아래에서 볼 수 있듯 emptyList()로 빈 List 를 생성하면 타입은 kotlin.collections.List 로 지칭되지만 컴파일 후엔 실제로는 java.util.List이다.
그럼 여기서 또 알 수 있는 재밌는 사실이 있다.
emptyList() 로 생성한 List 에서 내장 함수를 사용한다면 이것은 누구의 함수일까?
아래처럼 테스트해 보자.
fun main() {
val emptyAges = emptyList<Int>()
println(emptyAges.isEmpty())
}
위 코드에서 isEmpty()와 관련된 ByteCode는 다음과 같다.
보다시피 실제로는 java.util.List 의 isEmpty() 함수가 호출된다.
출력은 구현체인 EmptyList 에서 override 하고 있기에 0 이 출력된다.
그래서 emptyList() 로 만들어도 이는 java 컬렉션 인터페이스의 인스턴스이다.
mutableListOf() 도 확인해 보자
이제 마지막으로 mutableListOf() 를 확인하자.
fun main() {
val mutableAges = mutableListOf<Int>()
println(mutableAges.javaClass) // class java.util.ArrayList
}
위에서 확인한 listOf() 로 생성된 List 와 구현체가 같아 보이지만 사실 다르다.
listOf() → java.util.Arrays$ArrayList
mutableListOf() → java.util.ArrayList
이렇게 된다.
내부 코드를 들어가 보면 그냥 java.util.ArrayList 를 대놓고 생성한다.
이전과 똑같이 ByteCode 를 확인하자.
컴파일 후의 mutableListOf() 로 생성된 List의 타입은 java.util.List 이다.
지금까지 listOf(), emptyList(), mutableListOf() 로 생성된 각각의 List 를 확인했을 때
실제로 컴파일 후의 타입은 모두 java.util.List 타입이다.
그럼 결국 kotlin.collections.List 은 틀이다.
kotlin.collections.List 인터페이스를 kotlin 에서 실제로 구현하지는 않는다. 그저 틀이다.
그럼 kotlin.collections.List 는 왜 있는 걸까?
그럼 왜 있을까?
없이 그냥 kotlin 코드에서 java.util.List 타입으로 명시하면 안 되는 걸까?
왜 굳이 컴파일 과정에서 변환을 하는 걸까?
잠깐 자바 코드를 보자.
public class Main {
public static void main(String[] args) {
List<Integer> fixed_ages = Arrays.asList(26, 27);
List<Integer> mutable_ages = new ArrayList<>();
fixed_ages.add(28); // java.lang.UnsupportedOperationException
mutable_ages.add(26);
}
}
이렇게 하면 에러가 발생해서 펑 터진다.
터지는 곳은 보다시피 fixed_ages.add() 이다.
Arrays.asList() 로 생성하면 해당 List 의 구현체는 java.util.Arrays$ArrayList 이다.
이전에 kotlin 에서 listOf() 로 List를 생성하면 구현체는 java.util.Arrays$ArrayList 라고 했다.
둘이 구현체는 똑같다. 차이점은 코드 레벨에서의 타입이다.
Arrays.asList() 함수는 생성된 List 의 코드 레벨에서 타입은 java.util.List 이다.
listOf() 함수로 생성된 List의 코드 레벨에서 타입은 kotlin.collections.List 이다.
java.util.List 에는 add() 함수가 있지만, Arrays.asList() 로 생성한 List 는 고정된 크기를 갖고 싶다.
그래서 내부적으로 UnsupportedOperation(지원되지 않는 동작) 예외를 던진다.
하지만 kotlin.collections.List 에 add() 함수가 없다. 그렇기에 listOf() 로 생성한 List 는 add 함수를 사용할 수도 없다.
그래서 안전하다.
기존에 java 의 List 에서 모두 add 함수를 지원하던 걸 방지하고자
kotlin 에선 kotlin.collections.List(read-only), kotlin.collections.MutableList(mutable) 로 각각 나눈 게 아닐까 생각한다.
참고로. 위에서 살펴본 List 뿐 아니라 다른 모든 kotlin 의 컬렉션들은 컴파일 과정에서 java 의 컬렉션으로 변환된다.
끄읕.
도움을 받았습니다.
https://discuss.kotlinlang.org/t/kotlin-collection-vs-java-util-collection/256/3
https://proandroiddev.com/the-mystery-of-mutable-kotlin-collections-e82cbf5d781