Kotlin Generics 정의



개인 광고 영역

kotlin의 Generics을 정리합니다. Java에서 사용하는 Generics과 동일하게 사용할 수 있지만, Kotlin은 Generics 정의한 클래스를 상속받을 때 명시적으로 지정해야 합니다.

다시 말해 java에서는 제네릭 정의를 하지 않아도 기본 Object로 만들어주지만, 코틀린에서는 명시적으로 꼭 적어주도록 만들었습니다.


Java Generics 정의하기

먼저 Java에서의 Generics 정의를 살펴보죠.

interface Generic<T> {
  void setItem(T item);
}

위와 같이 setItem의 실제 아이템 정보를 Generics 정의합니다. 이러한 Type은 데이터를 다양하게 사용하기 위해서 Generics으로 표현하게 됩니다.


Kotlin Generics 정의하기

이번에는 kotlin에서 Generic을 정의해보면 다음과 같습니다.

interface Generic<in T> {
  fun setItem(item: T)
}

Java의 interface 정의와 동일하며, T 타입을 Generics으로 선언하였습니다.

이를 상속받는 클래스에서는 다음과 같이 구현해주어야 합니다.

class Sample : Generic<Generic Type 정의> {

    override fun setItem(item: Generic Type 정의) {
        // 구현
    }
}

예를 들어 Generic Type을 Int로 지정하면 아래와 같이 Override 구현이 일어납니다.

class Sample : Generic<Int> {

    override fun setItem(item: Int) {
        // 구현
    }
}


Wildcard type argument

Java에는 와일드카드라 불리는 부분이 있습니다. Kotlin에서는 in/out을 명시해야 하는데 이러한 부분을 이해하기 위해서 Java의 와일드카드를 먼저 알아보려고 합니다.

이러한 형태는 보통 List 내부 구현체에서 많이 볼 수 있는데 다음과 같이 addAll의 Collection에 extends로 정의한 것을 볼 수 있습니다. (extends는 리스트에서 아이템을 읽기만 가능합니다.)

public boolean addAll(Collection<? extends E> c) {
    throw new RuntimeException("Stub!");
}

코틀린은 Generics을 정의할 때 in/out을 자동으로 표현할 수 있도록 워닝 메시지를 출력해줍니다. option + enter을 함께 눌러서 in/out을 자동으로 표현할 수 있으니 참고하세요.

아래의 코드에서는 자동으로 in의 키워드가 붙었습니다. in/out이 가리키는 문법을 먼저 java와 kotlin을 통해서 살펴보겠습니다.

interface Generic<in T> {
  fun setItem(item: T)
}


Wildcard type argument - java

먼저 Java의 와일드카드를 살펴보면, 크게 extends와 super로 구분되어 있습니다. 각각의 의미는 아래와 같죠.

  • T : read/write 모두 가능
  • ? extends T : read만 가능한 서브타입 와일드카드
  • ? super T : write만 가능한 슈퍼 타입 와일드카드

read만 가능한 extends와 write만 가능한 super 2가지가 있습니다. read/write 모두를 해야 하면 와일드카드가 존재하지 않겠죠.


Wildcard type argument - java 샘플

아래의 interface<T>을 ArrayList에서 2개의 아이템을 추가하고, 이를 Wildcard 정의한 메소드에 넘겨서 각각을 처리해보겠습니다.

interface Output<T> {
  boolean isArgument(T argument);
}

아래와 같이 ArrayList에 interface 2개를 담아보겠습니다.

ArrayList<Output<String>> items = new ArrayList<>();
items.add(new Output<String>() {
    @Override
    public boolean isArgument(String argument) {
        return true;
    }
});
items.add(new Output<String>() {
    @Override
    public boolean isArgument(String argument) {
        return false;
    }
});


Wildcard type argument - ? extends T

? extends T는 읽기 전용 Wildcard입니다.

읽기 만 가능하기 때문에 수정이 불가능하여 ArrayList의 아이템을 읽을 수만 있습니다. 이 경우 보통 아이템을 복사해야 하는 경우에 사용하는 게 좋죠.

아래는 printAll을 정의하고, ArrayList에 Generics에 ? extends Output<String>으로 선언하였습니다. write을 시도하는 경우 즉시 문법 오류가 발생하는데, items.add(null);을 추가하는 경우에는 문제없이 들어갑니다.(허용되어서는 안될 것 같은데)

private void printAll(ArrayList<? extends Output<String>> items) {
    // Error...
    // items.add(new Output<String>() {
    //     @Override
    //     public boolean isArgument(String argument) {
    //         return false;
    //     }
    // });

    for (int i = 0; i < items.size(); i++) {
        if (items.get(i).isArgument("")) {
            System.out.println("item : " + items.get(i));
        }
    }
    items.add(null);
}

위의 코드에서 주석을 제거하면 아래와 같은 오류가 발생합니다.

java-extends-error

실행 결과는 아래와 같습니다.

items.get(0) // success - print
items.get(1) // success - Not print
items.add(null) // add success


Wildcard type argument - ? super T

이번에는 super를 살펴보겠습니다. super는 extends와 반대로 쓰기만 가능합니다.

아래와 같이 addItem? super Output<String>을 정의하였습니다. 이 경우 add만 가능하고 get은 접근할 수 없습니다.

private void addItem(ArrayList<? super Output<String>> items) {
    items.add(new Output<String>() {
        @Override
        public boolean isArgument(String argument) {
            return true;
        }
    });
    items.add(null);

    // Error...
    // for (int i = 0; i < items.size(); i++) {
    //     Output<String> item = items.get(i);
    //     if (item.isArgument("")) {
    //         System.out.println("item : " + items.get(i));
    //     }
    // }
}

위의 코드에서 주석을 제거하면 아래와 같은 오류가 발생합니다.

java-super-error

위의 테스트 결과는 다음과 같이 확인 가능합니다.

// printAll
items.add(new Output<String> ...) // add success
items.add(null) // add success


Wildcard type argument - ? extends T 유용하게 사용하기

Wildcard을 좀 더 유용하게 사용하기 위해서 Wildcard을 활용하여, 특정 아이템에 대한 filter 처리가 가능합니다. filter가 필요한 파라메터에 ? super Output<String>을 걸어서 읽기 전용으로 처리하고, items의 아이템을 출력해볼 수 있습니다.

private void printAll(ArrayList<Output<String>> items, Output<? super Output<String>> filter) {
    for (int i = 0; i < items.size(); i++) {
        if (filter.isArgument(items.get(i))) {
            System.out.println("item : " + items.get(i));
        }
    }
}

이렇게 하면 filter에 해당하는 argument을 뽑아와서 실제 true인 경우에만 print 하도록 만들어보았습니다.

public class Sample {
    interface Output<T> {
        boolean isArgument(T argument);
    }

    ArrayList<Output<String>> items = new ArrayList<>();

    private void printAll(ArrayList<Output<String>> items, Output<? super Output<String>> filter) {
        for (int i = 0; i < items.size(); i++) {
            if (filter.isArgument(items.get(i))) {
                System.out.println("item : " + items.get(i));
            }
        }
    }

    @Test
    public void test() {
        items.add(new Output<String>() {
            @Override
            public boolean isArgument(String argument) {
                return true;
            }
        });
        items.add(new Output<String>() {
            @Override
            public boolean isArgument(String argument) {
                return false;
            }
        });
        items.add(new Output<String>() {
            @Override
            public boolean isArgument(String argument) {
                return false;
            }
        });

        printAll(items, new Output<Output<String>>() {
            @Override
            public boolean isArgument(Output<String> argument) {
                return argument.isArgument("");
            }
        });
    }
}

여기까지가 java에서의 Wildcard에 대한 정의를 살펴보았고, 이를 바탕으로 kotlin의 in/out을 살펴보도록 하죠.


Kotlin의 in/out

kotlin에서는 명시적으로 super/extends 대신 in/out 키워드를 제공하는데, 각각의 정의와 매칭은 아래와 같습니다.

  • T : 별도의 Wildcard 정의가 없이 read/write 모두 가능
  • in T : Java의 ? super T와 같음. input의 약자이며 write 만 가능
  • out T : Java의 ? extends T와 같음. output의 약자이며 read 만 가능

kotlin에서도 java처럼 테스트하기 위해 generics interface을 아래와 같이 정의하였습니다.

interface Output<T> {
    fun isArgument(argument: T): Boolean
}

사용하는 ArrayList의 아이템을 다음과 같이 2개 정의하였습니다.

class ExampleUnitTest {

    val items = ArrayList<Output<String>>()

    init {
        items.add(object : Output<String> {
            override fun isArgument(argument: String) = false
        })
        items.add(object : Output<String> {
            override fun isArgument(argument: String) = true
        })
    }
}


Kotlin - out : ? extends T (java)

먼저 out을 살펴보죠. out은 java의 ? extends T와 매칭 되며, read only를 만들게 됩니다.

outoutput의 약자인데 다음과 같이 printAll을 구현해보았습니다.

private fun printAll(items: ArrayList<out Output<String>>) {
    // Error...
    // items.add(object : Output<String> {
    //    override fun isArgument(argument: String) = false
    // })

    items.indices
            .filter { items[it].isArgument("") }
            .forEach { println("item : " + items[it]) }
    // items.add(null) - Error
}

위의 코드에서 주석을 제거하면 아래와 같은 오류가 발생합니다. 문법상 add 자체가 불가능하기 때문에 null을 포함하든 하지 않든 오류가 발생합니다.

kotlin-out-error

위의 예제의 결과는 아래와 같습니다.

items.get(0) // Success - print
items.get(1) // Success - not print


Kotlin - in : ? super T (java)

이번에는 write만 가능한 in입니다. java의 ? super T에 매칭 됩니다.

아래와 같이 addItem에 in을 정의하였습니다.

private fun addItem(items: ArrayList<in Output<String>>) {
     items.add(object : Output<String> {
        override fun isArgument(argument: String) = false
     })

    // items.indices
    //        .filter { items[it].isArgument("") }
    //        .forEach { println("item : " + items[it]) }
    // items.add(null)
}

위의 코드에서 주석을 제거하면 아래와 같은 오류가 발생합니다. in은 쓰기만 가능하기에 read 시도 시 오류가 발생하며, kotlin은 기본적으로 null을 허용하지 않습니다. 그래서 list에 null을 허용하지 않기에 add(null)을 할 수 없습니다.

![kotlin-in-error]{: .align-center}{: .image-scale-80}

위의 실행 결과는 아래와 같습니다.

// printAll
items.add(object : Output<String> ...) // add success


Kotlin - in : ? super T (java) 유용하게 사용하기

in을 사용하여 filter를 걸어볼 수 있습니다. java에서 했던 것과 동일하게 처리하려면 아래와 같이 전체 코드를 확인할 수 있습니다.

class ExampleUnitTest {

    private val items = ArrayList<Output<String>>()

    init {
        items.add(object : Output<String> {
            override fun isArgument(argument: String) = false
        })
        items.add(object : Output<String> {
            override fun isArgument(argument: String) = true
        })
    }

    private fun printAll(items: ArrayList<Output<String>>, filter: Output<in Output<String>>) {
        items.indices
                .filter { filter.isArgument(items[it]) }
                .forEach { println("item : " + items[it]) }
    }

    @Test
    fun test() {
        printAll(items, object : Output<Output<String>> {
            override fun isArgument(argument: Output<String>): Boolean {
                return argument.isArgument("")
            }
        })
    }
}

interface Output<T> {
    fun isArgument(argument: T): Boolean
}


in/out을 모두 사용하지 않는 경우

in/out은 코틀린에서는 명시적으로 적용하게 됩니다. 하지만 아래의 경우는 read/write을 모두 하기 때문에 Generic 정의시에 별도로 처리하지 않습니다.

class Array<T>(val size: Int) {
  fun get(index: Int): T { /* ... */ }
  fun set(index: Int, value: T) { /* ... */ }
}


Generics Function

Generics의 정의는 class가 아닌 Function에서도 가능합니다. Function에서는 아래와 같이 정의할 수 있으며, 실제 사용할 때는 abc을 정의해서 사용할 수 있습니다. 또는 생략한 형태로도 사용 가능합니다.

private fun <T> abc(t: T) {
    // ...
}

// Use
abc<String>("ABC")
abc("ABC")

abc("ABC")에서처럼 Generics 정의를 생략하면 자동으로 변수에서 전달된 의미로 Generics을 파악하여 처리하게 됩니다.

아래는 createArrayList을 Generics을 정의하여 생성합니다. 이 경우 Generics을 정의하지 않으면, createArrayList에 접근할 수 없습니다.

private fun <T> createArrayList(): List<T> {
    return ArrayList()
}

// Use
createArrayList<String>()
createArrayList<Boolean>()

Java에서는 createArrayList을 호출할 때 별도의 Generics을 사용하지 않아도 동작하는데 기본 Object을 가리키고 있기 때문입니다. 그래서 kotlin에서 Java처럼 Object 리스트를 만들려면 createArrayList<Any>()을 이용할 수 있습니다.


Kotlin Generics 추론

kotlin의 Generics을 사용하면 타입을 생략하거나, 하지 못하는 경우가 있습니다.

Type을 생략 가능한 경우 : Generic을 선언하고, 변수로 즉시 사용하는 경우

class Foo<out T>(val t : T)

@Test
fun test() {
    val foo1 = Foo<String>("foo1") // 생략 가능
    val foo2 = Foo("foo2")  // 선언 가능
}

정의하면 IDE에서 아래와 같이 제거하도록 안내해줍니다.

kotlin-remove-explicit

생략이 불가능한 경우 : 별도의 변수에 T를 담지 않는다면 생략이 불가능하게 됩니다.

class Foo<T>

@Test
fun test() {
    val foo1 = Foo<String>() // 선언 가능
    val foo2 = Foo()  // 선언 불가능
}

위와 같이 구현하면 아래와 같이 오류가 발생합니다.

kotlin-error-explicit

Foo 클래스에서 T에 대한 변수 선언이 없어서 오류가 발생합니다. 다만 Java에서는 정의하지 않으면 자동으로 Object 타입을 만들어주기 때문에 문제가 없지만 Kotlin은 기본 정의가 필요합니다.

위의 코드를 정상적으로 Java와 같은 형태로 만들기 위해서는 아래와 같이 Foo<Any>()를 정의해주면 Java의 Object 정의와 같아지게 됩니다.

class Foo<T>

@Test
fun test() {
    val foo1 = Foo<String>() // 선언 가능
    val foo2 = Foo<Any>()  // 선언 가능
}


Star-Projections

Generics을 사용할 때 에 String/Int 형태를 정의하지 않고, `*`을 사용할 수 있습니다.

예를 들면 아래와 같은 arrayList 출력 메소드가 있다고 해보죠.

class ExampleUnitTest {

    private fun printArrayList(list: List<String>) {
        list.forEach {
            println("it $it")
        }
    }

    @Test
    fun test() {
        printArrayList(mutableListOf("AA", "BB", "CC"))
    }
}

만약 printArrayList에 Int을 출력해야 한다면 어떻게 할 수 있을까요?

이렇게 추가해서 처리할 수 있을 겁니다. 하지만 오버라이드가 불가능하여 오류가 발생할 겁니다.

private fun printArrayList(list: List<Int>) {
    list.forEach {
        println("it $it")
    }
}

간단하게 List의 제네릭에 <*>을 추가하거나 <Any>을 추가해주면 자바에서는 Object 정의하는 것과 같습니다. 그래서 mutableListOf(1, 2, 3) 또는 mutableListOf(1.0, 2.0, 3.0)에 무관하게 모두 printArrayList을 사용할 수 있게 됩니다.

class ExampleUnitTest {

    private fun printArrayList(list: List<*>) {
        list.forEach {
            println("it $it")
        }
    }

    @Test
    fun test() {
        printArrayList(mutableListOf("AA", "BB", "CC"))
        printArrayList(mutableListOf(1, 2, 3))
    }
}

이러한 *은 저는 RecyclerView에서 많이 활용하는데 Generics 정의가 복잡하거나, onClick을 구현 시 Recycler Adapter에 Generics이 복잡하게 정의되어 있을 경우 유용하게 사용하고 있습니다.

아래 코드는 RecyclerView.Adapter<*>을 onClick에 함께 넘겨주는 코드입니다. 어떠한 타입이 올지 모르기에 아래와 같이 *을 추가하여 간단하게 처리할 수 있습니다.

abstract class AbstractArrayRecyclerAdapter<ITEM, VIEW_TYPE : RecyclerView.ViewHolder?> {
    /**
     * OnItemClickListener
     */
    override var onItemClickListener: OnItemClickListener? = null

    override fun setOnItemClickListener(listener: (RecyclerView.Adapter<*>, Int) -> Unit) {
        onItemClickListener = object : OnItemClickListener {
            override fun onItemClick(adapter: RecyclerView.Adapter<*>, position: Int) {
                listener(adapter, position)
            }
        }
    }
}

이러한 Star-Projections은 다음과 같이 매칭 된다고 문서상에 나와있는데 *의 실제 의도는 in/out Nothing/Any으로 표현됩니다.

예를 들여 interface Function<in IN, out OUT>을 정의했다고 생각해보면 아래와 같이 의도할 수 있습니다.

  • Function<*, String> 실제 의도 Function<in Nothing, String>
  • Function<Int, *> 실제 의도 Function<Int, out Any?>
  • Function<*, *> 실제 의도 Function<in Nothing, out Any?>


Generics 예외 사항

아래와 같이 List 2개를 복사하는 샘플을 정의해보았습니다. Any의 아이템을 Any 리스트로 다시 변형하는 샘플입니다.

fun copy(from: Array<Any>, to: Array<Any>) {
  assert(from.size == to.size)
  for (i in from.indices)
    to[i] = from[i]
}

그리고 다음과 같이 호출해보았습니다.

@Test
fun test() {
    val ints: Array<Int> = arrayOf(1, 2, 3)
    val any = Array<Any>(3, { 0 })
    copy(ints, any) // Error: expects (Array<Any>, Array<Any>)
}

Int 리스트를 Any에 접근시켜서 Type mismatch가 발생합니다.

원래대로라면 copy의 형태를 변경하여야 하나, 이 경우는 out을 추가하고, read만 가능하도록 만들어주면 간단하게 해결됩니다.

class ExampleUnitTest {

    fun copy(from: Array<out Any>, to: Array<in Any>) {
        assert(from.size == to.size)
        for (i in from.indices)
            to[i] = from[i]
    }

    @Test
    fun test() {
        val ints: Array<Int> = arrayOf(1, 2, 3)
        val any = Array<Any>(3, {0})
        copy(ints, any)
    }
}

copy이기 때문에 굳이 Any로 처리할 필요는 없습니다. Int list을 Int list로 복사하는 게 정상적이니 아래와 같이 처리하는 편이 더 좋습니다.

class ExampleUnitTest {

    fun <TYPE> copy(from: Array<TYPE>, to: Array<TYPE>) {
        assert(from.size == to.size)
        for (i in from.indices)
            to[i] = from[i]
    }

    @Test
    fun test() {
        val ints: Array<Int> = arrayOf(1, 2, 3)
        val any = Array(3, {0})
        copy(ints, any)
    }
}

마무리

Kotlin의 Generics 정의는 java보다 엄격합니다. 그래서 누락을 할 수 없고, 꼭 명시해주어야 합니다. 이러한 명시가 java에서보다는 귀찮을 수 있지만 익숙해지면 좋습니다. 오히려 java에서처럼 Object 정의가 아니기에 실제 사용할 때 Typecast에 구애받지 않고 편히 사용할 수 있습니다.

이 글은 기존 Generics 작성했던 글을 재 작성한 글입니다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments