class Numbers(numbers: List<Int>) {
val numbers:List<Int> = numbers
}
다음과 같이 Int형 List를 갖는 Numbers가 있다고 하자
fun main() {
val list = mutableListOf(1, 2, 3, 4, 5, 6)
val numbers = Numbers(list)
println(numbers.numbers) // [1, 2, 3, 4, 5, 6]
list.add(7) // 바꾼닷!
println(numbers.numbers) // [1, 2, 3, 4, 5, 6, 7]
}
list
를 Numbers
객체 생성자에 넣어주고, list에 element를 추가하면 numbers.numbers
의 요소도 바뀌는 것을 확인할 수 있다. 객체 생성시 리스트의 reference가 넘어가기 때문에(얕은 복사), 원본이 바뀌기 때문이다.
class Numbers(numbers: List<Int>) {
val numbers:List<Int> = numbers.toList() // 새 리스트로 반환!
}
fun main() {
val list = mutableListOf(1, 2, 3, 4, 5, 6)
val numbers = Numbers(list)
println(numbers.numbers) // [1, 2, 3, 4, 5, 6]
list.add(7) // 바꾼닷!
println(numbers.numbers) // [1, 2, 3, 4, 5, 6]
}
위와 같이 Numbers
에서 numbers.toList()
를 활용하여 새 리스트를 받아 넣어준다면, 원본이 아닌 새 리스트가 참조된다(깊은 복사). 따라서 원본이 바뀌더라도 Numbers
객체의 numbers
는 그대로임을 확인할 수 있다.
그렇다면, 내부에서는 변경 가능한 리스트를, 외부에서는 변경 안되는 보기 전용 리스트를 쓰고 싶다면? 다음과 같이 쓸 수 있겠다.
class Numbers(numbers: List<Int>) {
private val _numbers: MutableList<Int> = numbers.toMutableList()
val numbers: List<Int> // backing property
get() = _numbers
fun add(number: Int) {
_numbers.add(number)
}
fun clear() {
_numbers.clear()
}
}
이렇게 작성하면 객체 생성할 때 불변인 List를 MutableList로 새로 생성하여 _numbers
가 참조한다. backing property를 사용하여 numbers
를 호출하면 _numbers
를 겉보기에는 외부에서 건드릴 수 없는 List형으로 리턴한다. 만약 외부에서 List를 컨트롤하고 싶으면 클래스에 구현되어 있는 add()
와 clear()
함수를 사용하면 된다.
하지만 다음 코드를 보자
fun main() {
val numbers = Numbers(listOf(1, 2, 3, 4, 5, 6))
val numbers2 = numbers.numbers as MutableList
numbers2[0] = 7 // 첫 번째 요소를 7로 바꾸고
numbers2.add(8) // 8을 추가한다
println(numbers.numbers) // [7, 2, 3, 4, 5, 6, 8]
}
MutableList로 형변환하여 사용하니 바꾸고 싶지 않았던 numbers.numbers
도 바뀌어 있는 모습을 확인할 수 있다.
// Collections.kt
public interface MutableList<E> : List<E>, MutableCollection<E> { // ...
List와 MutableCollection을 상속받는 MutableList의 형태도 확인할 수 있다. List에 구현되어 있지 않은 add()
와 같은 함수가 MutableList에는 구현되어 있고 numbers가 MutableList 출신이기 때문에, List로 왔지만 다운캐스팅을 하면 사용할 수 있는 것이다.
// Java Decompile
public final class MainKt {
public static final void main() {
Numbers numbers = new Numbers(CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6}));
List var10000 = numbers.getNumbers();
if (var10000 == null) {
throw new NullPointerException("null cannot be cast to non-null type kotlin.collections.MutableList<kotlin.Int>");
} else {
List numbers2 = TypeIntrinsics.asMutableList(var10000);
numbers2.set(0, 7);
numbers2.add(8);
List var2 = numbers.getNumbers();
System.out.println(var2);
}
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
Java로 디컴파일 해보면, Kotlin 코드에서 MutableList든 List든 모두 Java에서는 List로 처리하고 있다.
결국, 또 같은 주소를 참조하고 있기 때문에 발생하는 문제이다.
class Numbers(numbers: List<Int>) { // backing property
private val _numbers: MutableList<Int> = numbers.toMutableList()
val numbers: List<Int>
get() = _numbers.toList() // toList()를 쓴다!
fun add(number: Int) {
_numbers.add(number)
}
fun clear() {
_numbers.clear()
}
}
numbers
를 호출하면, toList()
를 사용하여 새로운 리스트를 반환해주면 된다. 이렇게 처리한다면 원본은 바뀌지 않고, 외부에서는 필요하면 다운캐스팅하여 리스트를 변환하여 사용할 수 있을 것이다.
먼저 var number
을 갖는 Number
클래스를 만들었다. Numbers
는 방금 사용한 코드와 그대로이지만 편의를 위해 함께 첨부한다.
class Number(var number: Int) {
override fun toString(): String = number.toString()
}
class Numbers(numbers: List<Number>) {
private val _numbers: MutableList<Number> = numbers.toMutableList()
val numbers: List<Number>
get() = _numbers.toList()
fun add(number: Number) {
_numbers.add(number)
}
fun clear() {
_numbers.clear()
}
}
fun main() {
val numberObjects = (1..6).map { Number(it) } // 1~6까지 Number 객체 모음
val numbers = Numbers(numberObjects)
val numbers2 = numbers.numbers as MutableList
println(numbers.numbers) // [1, 2, 3, 4, 5, 6]
(numberObjects as MutableList)[0].number = 7 // 원본 바꾸기
numbers2[1].number = 8 // numbers에서 가져온 number 바꾸기
println(numbers.numbers) // [7, 8, 3, 4, 5, 6]
}
Numbers
객체를 생성할 때 분명 새 리스트로 복사했으나, Element들은 복사되지 않고 원본 그대로를 참고하고 있기 때문에 일어나는 일이다. Element로 갖고 있는 가변 객체 역시 복사하여 넣어주어야 원본을 지킬 수 있을 것이다.
class Number(var number: Int) : Cloneable {
override fun toString(): String = number.toString()
public override fun clone(): Number {
return super.clone() as Number
}
}
class Numbers(numbers: List<Number>) {
// 하나하나 clone()으로 복사하기
private val _numbers: MutableList<Number> = numbers.map { it.clone() }.toMutableList()
val numbers: List<Number>
get() = _numbers.toList()
fun add(number: Number) {
_numbers.add(number)
}
fun clear() {
_numbers.clear()
}
}
위 예제에서는 Number
가 primitive type의 변수만 가지고 있기 때문에 비교적 쉽게 복사가 가능하지만, Collection이라도 가지고 있는 경우에는 또 다시 Element는 복사되지 않는 상황, 즉 깊은 복사가 안되는 상황이 발생하니 더 세세하게 구현해야 한다.
data class로 정의하면 toString()
, hashCode()
, equals()
, copy()
를 자동으로 구현되도록 한다.
data class Number(var number: Int) // data class로 변경
class Numbers(numbers: List<Number>) {
// copy() 함수 사용
private val _numbers: MutableList<Number> = numbers.map { it.copy() }.toMutableList()
val numbers: List<Number>
get() = _numbers.toList()
fun add(number: Number) {
_numbers.add(number)
}
fun clear() {
_numbers.clear()
}
}
복사를 해주는 copy()
함수를 사용한다면, 따로 구현 없이 예쁘게 코드를 짤 수 있다.