내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-07-09 07:18

제목

Kotlin] 코틀린 기초 09 - 제네릭


  • 제네릭

클래스 혹은 메서드를 사용할 때 타입을 명시하는 기법

// 제네릭 클래스

1
2
3
4
5
6
7
8
9
class MyClass<T> {
    var info: T? = null;
}
 
val obj1: MyClass<String> = MyClass<String>();
obj1.info = "test";
 
val obj2: MyClass<Int> = MyClass<Int>();
obj2.info = 111;
cs

// 타입 유추에 의한 제네릭

1
2
3
4
5
6
7
8
9
10
class MyClass<T>(no: T) {
    var info: T? = null;
}
 
val obj1: MyClass<Int> = MyClass<Int>(10);
obj1.info = 111;
 
 
val obj2 = MyClass("hello");
obj2.info = "world";
cs

obj2 프로퍼티에 MyClass인스턴스 생성시 타입지정을 하지 않고 생성자에 바로 문자열 파라메터를 전달해도 에러가 나지 않고 타입 유추로 인해 정상 컴파일&실행 된다.

// 여러 개의 형식 타입 선언

1
2
3
4
5
6
7
8
9
10
class MyClass<T, A> {
    var info: T? = null;
    var data: A? = null;
}
 
val obj1: MyClass<String, Number> = MyClass<String, Number>();
obj1.info = "test";
obj1.data = 111;  // Int타입 데이터
obj1.data = 111.10f;  //Float타입 데이터
obj1.data = 111.110;  // Double타입 데이터
cs

// 타입 제약 조건 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface IMyInterface1
interface IMyInterface2
 
class MyHandler1 : IMyInterface1, IMyInterface2 {}
class MyHandler2 : IMyInterface1 {}
open class MyHandler3 {}
class MyHandler4 : MyHandler3() {}
 
// IMyInterface1 && IMyInterface2인터페이스를 상속 받은 객체 타입만 가능
class MyClass1<T> where T : IMyInterface1, T: IMyInterface2 {
    //
}
 
// MyHandler3 타입 또는 MyHandler3을 상속 받은 객체 타입만 가능
class MyClass2<T> where T : MyHandler3 {
    //
}
 
val obj1: MyClass1<MyHandler1> = MyClass1<MyHandler1>();
val obj2: MyClass2<MyHandler4> = MyClass2<MyHandler4>();
cs
  • Null 불허 제약

제네릭의 형식 타입은 기본으로 Null을 허용한다.
따라서 만일

class MyClass<T>
cs

위와 같이 선언하였다면 이는

class MyClass<T: Any?>
cs

로 선언한 것과 같다. Any클래스는 코틀린에서 모든 객체의 최상위 객체이므로
Any?로 선언하므로 실사용 때 어떤 타입도 지정할 수 있고 Null도
허용한다는 의미이다.

class MyClass<T> {
    fun myFun(arg1: T, arg2: T) {
        // arg1파라메터는 null허용이기에 '?'로 사용해야 한다.
        println(arg1?.equals(arg2));
    }
}
 
val obj1: MyClass<String?> = MyClass<String?>();
obj1.myFun("test""test");
 
val obj2: MyClass<String?> = MyClass<String?>();
obj2.myFun(null"test");
cs

실행결과
true
null

Null을 허용하고 싶지 않을때는 <T: Any?>가 아닌 Any타입으로 명시하면 된다.

// null 허용하지 않는 제네릭 타입
class MyClass<T: Any> {
    fun myFun(arg1: T, arg2: T) {
        println(arg1.equals(arg2));
    }
}
val obj: MyClass<String> = MyClass<String>();
obj.myFun(null"test");  // 오류
cs


  • out 어노테이션 (read만 가능)

제네릭의 타입 선언시 ‘out’을 붙이면 해당 제네릭 파라메터는 read만 가능하고 상속받은 자식 타입을 부모 타입에 대입하여 사용 가능하게 할 수 있다.

class MyClass<T>(private val data: T) {
    fun myFun(arg: T): T {
        return data;
    }
}
fun some(arg: MyClass<Number>) {
    arg.myFun(20);
}
this.some(MyClass<Number>(10));
this.some(MyClass<Int>(10));  // 오류
cs

제네릭 형식이 Number타입이라 Int타입 사용은 오류 발생 하지만 제네릭 타입에 ‘out’을 붙이면 사용 가능하다.

class MyClass<T>(private val data: T) {
    fun myFun(arg: T): T {
        return data;
    }
}
 
fun some(arg: MyClass<out Number>) {
    arg.myFun(20);  <- 오류
}
 
this.some(MyClass<Number>(10));
this.some(MyClass<Int>(10));
cs

제네릭 타입에 out을 붙여 Int타입 제네릭으로 사용 가능하지만 out은 read만 가능하므로
해당 제네릭의 파라메터에서 관련 타입 맴버는 접근할 수 없기에 오류가 발생한다.

  • in 어노테이션 (write만가능)

‘out’과 반대로 write만 가능하고 부모 타입을 자식 타입에 대입하여 사용 가능하게 할 수 있다.

class MyClass<T>(private val data: T) {
    fun myFun(arg: T): T {
        return data;
    }
}
 
fun some(arg: MyClass<in Int>) {
    arg.myFun(20);
}
 
 
this.some(MyClass<Number>(10));
this.some(MyClass<Int>(10));
cs

제네릭 형식이 Int타입이면서 ‘in’을 붙였기에 해당 제네릭 타입에 Int클래스의 부모인 Number클래스 사용이 가능하다.
in과 out의 또 다른 예제 이다.

interface IOutput<T> {
    fun isArgument(argument: T): Boolean;
}
 
class MyClass1: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("test")) true else false;
}
 
class MyClass2: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("aaa")) true else false;
}
 
fun printAll(items: ArrayList<IOutput<String>>) {
    val obj2 : MyClass2 = MyClass2();
    items.add(obj2);
    items.indices
        .filter { items[it].isArgument("aaa") }
        .forEach { println("item : ${items[it].isArgument("aaa")}"); };
}
 
val items = ArrayList<IOutput<String>>();
val obj1 : MyClass1 = MyClass1();
val obj2 : MyClass2 = MyClass2();
 
items.add(obj1);
items.add(obj2);
 
this.printAll(items);
cs

실행 결과
item : true
item : true

printAll메서드의 제네릭 파라메터 items의 제네릭 타입 IOutput은 어노테이션이 붙지 않아 자유롭게 해당 배열에 아이템을 추가하고, 읽을 수 있다.
하지만 ‘out’를 붙이면 read만 가능하기에 items.add구문에서 오류가 발생한다.

interface IOutput<T> {
    fun isArgument(argument: T): Boolean;
}
 
class MyClass1: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("test")) true else false;
}
 
class MyClass2: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("aaa")) true else false;
}
 
fun printAll(items: ArrayList<out IOutput<String>>) {
    val obj2 : MyClass2 = MyClass2();
    items.add(obj2);  // 오류
    items.indices
        .filter { items[it].isArgument("aaa") }
        .forEach { println("item : ${items[it].isArgument("aaa")}"); };
}
 
val items = ArrayList<IOutput<String>>();
val obj1 : MyClass1 = MyClass1();
val obj2 : MyClass2 = MyClass2();
 
items.add(obj1);
items.add(obj2);
 
this.printAll(items);
cs

‘in’을 사용했을 때 는 write만 가능하기에 반대로 items.add구문은 오류가 없지만 배열을 읽는 부분은 오류가 발생한다.

interface IOutput<T> {
    fun isArgument(argument: T): Boolean;
}
 
class MyClass1: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("test")) true else false;
}
 
class MyClass2: IOutput<String> {
    override fun isArgument(argument: String): Boolean = if (argument.equals("aaa")) true else false;
}
 
fun printAll(items: ArrayList<in IOutput<String>>) {
    val obj2 : MyClass2 = MyClass2();
    items.add(obj2);
    items.indices
        .filter { items[it].isArgument("aaa") }  <- 오류
        .forEach { println("item : ${items[it].isArgument("aaa")}"); };  <- 오류
}
 
val items = ArrayList<IOutput<String>>();
val obj1 : MyClass1 = MyClass1();
val obj2 : MyClass2 = MyClass2();
 
items.add(obj1);
items.add(obj2);
 
this.printAll(items);
cs
  • Star-Projections (*)

스타 프로젝션(Projection)은 제네릭 타입을 <*>로 표현하는 것을 의미한다.
스타 프로젝션은 제네릭 타입을 모른다는 의미이다. 나중에 정확한 타입으로 이용되기는
하지만 지금은 어떤 제네릭 타입이 지정될지 모른다는 의미로 사용된다. 스타 프로젝션은 제네릭의 선언
측에서는 사용할 수 없고 이용축에서만 사용할 수 있다.

class MyClass<*>  <- 오류
class MyClass2<T>
 
fun myFun(arg: MyClass2<*>) { }  // 이렇게 제네릭을 이용하는 축에서 사용
cs

_<Any?>와 _<*>의 차이

val list1: MutableList<Any?> = mutableListOf<Any>(1010.0"test"); <- 오류
val list2: MutableList<*> = mutableListOf<Any>(1010.0"test");
cs

list1은 Any? 제네릭타입에 Any타입 대입으로 오류가 발생하지만
list2는 제네릭 타입이 <*>이므로 Any타입을 발생해도 오류가 발생하지 않는다.

  • 인라인 메서드와 reified

제네릭 타입을 실행 시점에 알아내기 위해서는 reified키워드를 이용해야 한다. Reified는 인라인 메서드에서만 사용할 수 있다.

fun <T>some(arg: Any) {
    if(arg is T) {
        println("true");
    }
    else {
        println("false");
    }
}
cs

위 코드는 “Cannot check for instance of erased type: T”오류가 발생한다.
제네릭 타입 대상으로 as와 is 연산자를 사용헐려면 reified키워드를 이용한다.

inline fun <reified T>some(arg: Any) {
    if(arg is T) {
        println("true");
    }
    else {
        println("false");
    }
}
cs


출처1

출처2