클래스 혹은 메서드를 사용할 때 타입을 명시하는 기법
// 제네릭 클래스
| 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 |
|
// 타입 유추에 의한 제네릭
| 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인스턴스 생성시 타입지정을 하지 않고 생성자에 바로 문자열 파라메터를 전달해도 에러가 나지 않고 타입 유추로 인해 정상 컴파일&실행 된다. // 여러 개의 형식 타입 선언
| 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을 허용한다. 따라서 만일 위와 같이 선언하였다면 이는 로 선언한 것과 같다. 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만 가능하고 상속받은 자식 타입을 부모 타입에 대입하여 사용 가능하게 할 수 있다.
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만 가능하므로 해당 제네릭의 파라메터에서 관련 타입 맴버는 접근할 수 없기에 오류가 발생한다. ‘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 |
|
스타 프로젝션(Projection)은 제네릭 타입을 <*>로 표현하는 것을 의미한다. 스타 프로젝션은 제네릭 타입을 모른다는 의미이다. 나중에 정확한 타입으로 이용되기는 하지만 지금은 어떤 제네릭 타입이 지정될지 모른다는 의미로 사용된다. 스타 프로젝션은 제네릭의 선언 측에서는 사용할 수 없고 이용축에서만 사용할 수 있다. class MyClass<*> <- 오류 class MyClass2<T> fun myFun(arg: MyClass2<*>) { } // 이렇게 제네릭을 이용하는 축에서 사용 | cs |
|
_<Any?>와 _<*>의 차이 val list1: MutableList<Any?> = mutableListOf<Any>(10, 10.0, "test"); <- 오류 val list2: MutableList<*> = mutableListOf<Any>(10, 10.0, "test"); | cs |
|
list1은 Any? 제네릭타입에 Any타입 대입으로 오류가 발생하지만 list2는 제네릭 타입이 <*>이므로 Any타입을 발생해도 오류가 발생하지 않는다. 제네릭 타입을 실행 시점에 알아내기 위해서는 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 |
|
|