티스토리 뷰

이전 2개의 포스팅에서 Covariance/Contravariance PECS 원칙에 대해 다루었다. 이번 포스팅에서는 마지막으로 Java와 Kotlin의 Covariance의 차이를 알아보려고 한다.

 

그러기 위해 먼저 Kotlin의 Collection과 MutableCollection을 알아보자.

Kotlin에서는 List, Set, Map과 같이 앞에 “Mutable”로 시작하지 않는 Collection은 immutable하다. 즉, read-only라는 의미이다.

그렇기 때문에 각 인터페이스를 확인해 보면 immutable 자료형은 제네릭 타입 파라미터에 out 키워드가 붙어있는 것을 확인할 수 있다.

public interface Collection<out E> : Iterable<E>

public interface List<out E> : Collection<E>
public interface Set<out E> : Collection<E>

public interface Map<K, out V>

 

그렇기 때문에 immutable한 Collection에 경우 Covariance가 적용된다. 즉, 다음과 같은 코드를 작성할 수 있다.

fun main() {
    val c: Collection<Person> = listOf<Student>(Student())
    val l: List<Person> = listOf<Student>(Student())
    val s: Set<Person> = setOf<Student>(Student())
    val m: Map<Int, Person> 
        = mapOf<Int, Student>(1 to Student(), 2 to Student())
}

 

하지만 MutableCollection인 MutableList, MutableSet, MutableMap과 같은 mutable 타입은 읽고 쓸 수 있다.

그렇다면 각 인터페이스가 어떻게 정의되어 있는지 보면 아래 코드처럼 exact한 E 타입만 들어올 수 있는 Invariance임을 알 수 있다.

public interface MutableCollection<E> : Collection<E>, MutableIterable<E>

public interface MutableList<E> : List<E>, MutableCollection<E>
public interface MutableSet<E> : Set<E>, MutableCollection<E>

public interface MutableMap<K, V> : Map<K, V>

 

또한 위 코드를 보면 MutalbleList<>는 List<>를 상속 받고 있고, MutableSet<>은 Set<>을, MutableMap<>은 Map<>을 각각 상속 받고 있음을 알 수 있다.

그렇기 때문에 자식 타입을 부모 타입으로 변환 할 수 있는 객체지향 원칙을 따라 각각의 mutable collection들은 immutable collection으로 변환이 가능하다.

하지만 반대는 불가능하다.

fun main() {
    // mutable collection을 immutable collection로 만드는 것은 가능
    val c: Collection<Person> = mutableListOf<Student>(Student())
    val l: List<Person> = mutableListOf<Student>(Student())
    val s: Set<Person> = mutableSetOf<Student>(Student())
    val m: Map<Int, Person> 
        = mutableMapOf<Int, Student>(1 to Student(), 2 to Student())

    // immutable collection을 mutable collection로 만드는 것은 불가능
    // error
    val c2: MutableCollection<Person> = listOf<Student>(Student())
    // error
    val l2: MutableList<Person> = listOf<Student>(Student())  
    // error
    val s2: MutableSet<Person> = setOf<Student>(Student())      
    // error
    val m2: MutableMap<Int, Person> 
        = mapOf<Int, Student>(1 to Student(), 2 to Student())
}

 

MutableCollection은 Invariance이기 때문에 당연히 아래와 같은 코드는 에러를 발생시킨다.

fun main() {
    // error
    val c: MutableCollection<Person> = mutableListOf<Student>(Student())
    // error
    val l: MutableList<Person> = mutableListOf<Student>(Student())
    // error
    val s: MutableSet<Person> = mutableSetOf<Student>(Student())
    // error
    val m: MutableMap<Int, Person> 
        = mutableMapOf<Int, Student>(1 to Student(), 2 to Student())
}

 

 

이제 본격적으로 Kotlin과 Java에서의 Covariance 차이점을 알아보기 위해 예시 클래스를 작성해 보자.

[Kotlin] TimeSeries

class TimeSeries<E> {
    private val date2Data: MutableMap<Date, E> = mutableMapOf()

    fun add(element: E) = date2Data.put(Date(), element)
    fun addAll(elements: Collection<E>) = elements.forEach{ add(it) }
}

이 코드에서 보면 TimeSeries는 Invariance하다는 것을 알 수 있다.

 

[Java] JavaTimeSeries

class JavaTimeSeries<E> {
    private final Map<Date, E> date2Data = new HashMap<>();

    void add(E element) { date2Data.put(new Date(), element); }
    void addAll(Collection<E> elements) { for (E e : elements) add(e);
}

Kotlin 코드와 비교했을 때 date2Data의 타입이 다른 것 이외에는 큰 차이가 없다.

또한 JavaTimeSeries도 Invariance하다.

 

 

이제 두 코드를 사용하는 코드를 보자.

 

[Kotlin] main

fun main() {
    val events: TimeSeries<Person> = TimeSeries<Person>()
    events.add(Student())       // covariance
    events.add(Person())
    //events.add(Being())       // error!

    val students: List<Student> = listOf(Student(), Student())
    events.addAll(students)

    val students2: List<Student> = mutableListOf(Student(), Student())
    events.addAll(students2)

    val students3: MutableList<Student> = mutableListOf(Student(), Student())
    events.addAll(students3)

    val javaEvents: JavaTimeSeries<Person> = JavaTimeSeries<Person>()
    javaEvents.add(Student())         // covariance
    javaEvents.add(Person())
    //javaEvents.add(Being())         // error!

    javaEvents.addAll(students)
    javaEvents.addAll(students2)
    //javaEvents.addAll(students3)    // error!
}

TimeSeries<Person> 타입의 events 객체를 만든 뒤 add() 메서드의 인자로 Student, Person타입의 인스턴스는 넣을 수 있지만, Person의 부모 클래스인 Being타입 인스턴스는 넣을 수 없다.

 

이제 Collection<E>를 인자로 받는 addAll() 메서드를 알아보자. 

List<Student> 타입의 students는 Collection<Person>의 자식 타입이므로 인자로 넣을 수 있다.

List<Student> 타입의 students2는 MutableList로 초기화가 되었지만 List<Student> 타입으로 변환되었기 때문에 들어갈 수 있다.

MutableList<Student> 타입의 student3는 List<Student>의 자식타입이고 List<Student>는 Collection<Person>의 자식 타입이므로 인자로 넣을 수 있다.

 

하지만 JavaTimeSeries<Person>의 addAll()을 보면 조금 다르다. 

JavaTimeSeries<Person>의 addAll()의 파라미터 타입은 Collection<E>이다.

Java의 Collection은 mutable과 immutable 구분이 명확하지 않다. 

그렇기 때문에 addAll()의 인자로 immutable이 확실한 데이터가 들어오는 것은 문제가 없지만, mutable한 데이터가 들어오면 컴파일 에러가 발생한다.

 

즉, Kotlin의 addAll()의 인자 파라미터는 immutable한 Collection<E> 타입이기 때문에 mutable한 데이터가 들어와도 immutable하게 바뀌기 때문에 문제가 되지 않았다. 하지만 Java의 addAll()의 인자 파라미터는 mutable일 수도 있는 Collection 타입이기 때문에 mutable한 데이터가 들어오면 문제가 되는 것이다.

 

 

이번에는 Java의 사용 코드를 살펴보자.

 

[Java] main

public static void main(String[] args) {
    List<Student> students = Arrays.asList(new Student(), new Student());

    JavaTimeSeries<Person> javaEvents = new JavaTimeSeries<>();
    javaEvents.add(new Student());        // covariance
    javaEvents.add(new Person());
    //javaEvents.add(new Being());        // error!

    //javaEvents.addAll(students);        // error!

    TimeSeries<Person> eventsKt = new TimeSeries<>();
    eventsKt.add(new Student());          // covariance
    eventsKt.add(new Person());
    eventsKt.addAll(students);            // covariance
}

Java에서도 마찬가지로 JavaTimeSeries<Person> 타입의 events 객체를 만든 뒤 add() 메서드의 인자로 Student, Person타입의 인스턴스는 넣을 수 있지만, Person의 부모 클래스인 Being타입 인스턴스는 넣을 수 없다.

 

Java에는 mutableList와 List가 구분되지 않기 때문에 List를 만들어서 addAll() 메서드의 인자로 넣으면 에러가 발생한다.

이는 위에 Kotlin 사용 코드 설명에서도 이야기했듯이 Java의 addAll()의 인자 파라미터는 mutable일 수도 있는 Collection 타입이기 때문에 여기에 mutable 데이터가 들어오면 에러가 발생하는 것이다.

 

반면에 Java에서 TimeSeries을 사용하는 코드에서 addAll() 메서드에 인자로 mutable한 List를 넣는 것은 가능하다. Kotlin 사용 코드에서도 MutableList 객체가 인자로 들어갈 수 있었던 것과 동일한 원리이다.

 

 

이제 Java에서 mutable한 데이터를 addAll() 메서드의 인자로 넣을 수 있도록 코드를 변경해보자. 이는 단 한 줄만 바꾸면 해결된다.

[Java] JavaTimeSeries

class JavaTimeSeries<E> {
    private final Map<Date, E> date2Data = new HashMap<>();

    void add(E element) { date2Data.put(new Date(), element); }
    void addAll(Collection<? extends E> elements) { 
        for (E e : elements) add(e); 
    }
}

기존 JavaTimeSeries 클래스의 어느 부분이 바뀌었을까? 그건 바로 Collection<? extends E> elements 이 부분이다.

addAll() 메서드로 들어오는 인자를 Upper Bound Wildcard를 이용하면 read-only로 immutable한 타입으로 변한다.

이렇게만 바꾸면 기존 예제 코드에서 JavaTimeSeries의 addAll() 메서드의 인자로 mutable한 데이터를 넣어서 에러가 났던 코드들이 정상적으로 동작하는 것을 확인할 수 있다.

이제 Kotlin처럼 Java의addAll() 메서드에도 들어오는 인자를 immutable한 데이터로 변환해 주기 때문에 mutable한 데이터가 들어와도 문제가 없는 것이다.

 

[Kotlin] main

fun main() {
    val students3: MutableList<Student> = mutableListOf(Student(), Student())

    val javaEvents: JavaTimeSeries<Person> = JavaTimeSeries<Person>()
    javaEvents.addAll(students3)      // 해결!
}

 

[Java] main

public static void main(String[] args) {
    List<Student> students = Arrays.asList(new Student(), new Student());

    JavaTimeSeries<Person> javaEvents = new JavaTimeSeries<>();
    javaEvents.addAll(students);        // 해결!
}

 

 

이렇게 해서 Kotlin의 Covariance와 Contravariance에 대한 정리를 마치려고 한다.

이 부분은 보는 것만 해서는 이해하기 어려울 수 있으니 이를 확실히 이해하고 싶다면 코드를 작성하고 직접 테스트하며 확인해 보는 것을 권한다.

'Mobile > Kotlin' 카테고리의 다른 글

[Kotlin] PECS 원칙  (0) 2020.03.26
[Kotlin] Covariance & Contravariance  (0) 2020.03.26
[Kotlin]의 apply, with, also, let, run 구분  (0) 2020.03.24
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함