출처: https://www.boostcourse.org/mo234/lecture/154303?isDesc=false
제네릭이란, 자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있게 해주는 것이다. 보통 앵글 브래킷(<>) 사이에 형식 매개변수를 사용해 선언하며, 여기서 형식 매개변수는 자료형을 대표하는 용어로 T와 같이 특정 영문의 대문자를 사용한다.
package chap04.section1
class Box<T>(t: T) {
var name = t
}
fun main() {
val box1: Box<Int> = Box(1)
val box2: Box<String> = Box("Kildong")
println(box1.name)
println(box2.name)
}
위와 같이 box1, box2의 타입을 명시적으로 지정해주지 않아도 컴파일러는 객체의 타입을 추론할 수 있다. (Ctrl + Shift + P 단축키로 추론 타입 확인 가능)
형식 매개변수를 한 개 이상 받는 클래스를 제네릭 클래스라고 한다.
class MyClass<T> {
fun myMethod(a: T) {
...
}
}
프로퍼티에 지정하는 경우, 주 생성자나 부 생성자에 형식 매개변수를 지정해 사용한다.
class MyClass<T>(val myProp: T){ }
class MyClass<T> {
val myProp: T
constructor(myProp: T){
this.myProp = myProp
}
}
val a = MyClass<Int>(12)
println(a.myProp) // 12
println(a.javaClass) // MyClass
자식 클래스는 부모 클래스에 없는 것도 갖고 있기 때문에 자식 클래스에 부모 클래스를 할당하면 Type mismatch 에러가 발생한다. (다운 캐스팅 불가능)
반대로, 부모 클래스에 자식 클래스를 할당하는 것은 가능하다. 자식 클래스는 부모 클래스의 프로퍼티와 메서드를 그대로 물려받았기 때문이다. (업 캐스팅 가능)
package chap04.section1
open class Parent
class Child : Parent()
fun main() {
//val obj1: Child = Parent()
val obj2: Parent = Child()
}
이 방식이 제네릭에도 그대로 적용될까? 직접 해보면, obj3와 obj4 모두 type mismatch 에러가 발생한다는 걸 확인할 수 있다. 즉, 형식 매개변수는 기본적으로 서로 아무런 관련이 없는 무변성 (invariance)을 갖고 있다. (무변성이 무엇인지는 뒤에서 더 자세히 살펴볼 예정)
package chap04.section1
open class Parent
class Child : Parent()
class Cup<T>
fun main() {
//val obj1: Child = Parent()
val obj2: Parent = Child()
//val obj3:Cup<Child> = Cup<Parent>()
//val obj4:Cup<Parent> = Cup<Child>()
}
가변성을 주려면 in, out 등을 설정해야 한다.
제네릭의 형식 매개변수는 기본적으로 널이 가능한 형태로 선언된다. 널을 허용하지 않으려면, 특정 자료형으로 제한하면 된다.
package chap04.section1
class GenericNull<T>{
fun equalityFunc(arg1: T, arg2: T){
// arg1가 null이 아닐 때만 equals 함수 실행
println(arg1?.equals(arg2))
}
}
fun main() {
val obj = GenericNull<String>() // non-null로 선언
obj.equalityFunc("Hello", "World") // 널 허용 X
val obj2 = GenericNull<Int?>() // nullable로 선언
obj2.equalityFunc(null, 10) // 널 허용 O
}
false
null
// 매개변수와 리턴 타입에 사용됨.
fun <T> genericFunc(arg: T): T? { ... }
// 형식 매개변수가 여러 개인 경우
fun <K, V> put(key: K, value: V): Unit { ... }
package chap04.section1
fun <T> find(a: Array<T>, target: T): Int {
for(i in a.indices){ // 배열의 범위를 나타내는 indices
if(a[i] == target) return i
}
return -1
}
fun main() {
val arr1: Array<String> = arrayOf("Apple", "Banana", "Cherry", "Durian")
val arr2: Array<Int> = arrayOf(1, 2, 3, 4)
println("arr1.indices ${arr1.indices}")
println(find(arr1, "Apple"))
println(find(arr2, 2))
}
arr1.indices 0..3
0
1
형식 매개변수로 선언된 함수의 매개변수로 연산을 하려면?
fun <T> add(a: T, b: T): T {
//return a + b // 타입을 알 수 없어서 에러 발생
}
아래처럼 람다식을 사용하면 위의 문제를 해결할 수 있다!
package chap04.section1
fun <T> add(a: T, b: T, op: (T, T) -> T): T{
return op(a, b)
}
fun main() {
val result = add(2, 3) { a, b -> a + b }
println(result)
}
형식 매개변수를 특정한 자료형으로 제한할 수 있다.
package chap04.section1
class Calc<T: Number> { // 클래스의 형식 매개변수 제한
fun plus(arg1: T, arg2: T): Double {
return arg1.toDouble() + arg2.toDouble()
}
}
fun main() {
val calc = Calc<Int>()
println(calc.plus(2, 5))
val calc2 = Calc<Double>()
val calc3 = Calc<Long>()
//val calc4 = Calc<String>()
println(calc2.plus(2.5, 3.4))
println(calc3.plus(10L, 20L))
}
fun <T: Number> addLimit(a: T, b: T, op: (T, T) -> T): T {
return op(a, b)
}
val result = addLimit("abc", "def", {a, b -> a + b}) // error
가변성은 형식 매개변수가 클래스 계층에 어떤 영향을 미치는지 정의한다. 형식 A의 값이 필요한 모든 장소에 형식 B의 값을 넣어도 아무런 문제가 없다면, B는 A의 하위 형식이다. (ex. Int는 Number의 하위 클래스)
String과 String?은 완전히 다른 형태라는 걸 기억하자. String은 non-null 타입의 클래스이지만, String?는 그냥 nullable 타입이다. 클래스가 아니다.
상위 클래스는 하위 클래스를 수용할 수 있다. 이때 하위 자료형은 상위 자료형으로 자동 형변환 된다. (타입 캐스팅)
용어 | 의미 |
---|---|
공변성 (covariance) | T'이 T의 하위 자료형이면, C<T'>은 C<T>의 하위 자료형이다. 생산자 입장의 out 성질 |
반공변성 (contravariance) | T'이 T의 하위 자료형이면, C<T>는 C<T'>의 하위 자료형이다. 소비자 입장의 in 성질 |
무변성 (invariance) | C<T>와 C<T'>은 아무런 관계가 없다. 생산자 + 소비자 (디폴트) |
package chap04.section1
class Person<T>(val age: Int)
fun main() {
//val anys: Person<Any> = Person<Int>(10)
//val nothings: Person<Nothing> = Person<Int>(20)
}
package chap04.section1
class Person<out T>(val age: Int)
fun main() {
val anys: Person<Any> = Person<Int>(10) // 업 캐스팅
//val nothings: Person<Nothing> = Person<Int>(20)
}
package chap04.section1
class Person<in T>(val age: Int)
fun main() {
//val anys: Person<Any> = Person<Int>(10)
val nothings: Person<Nothing> = Person<Int>(20) // 다운 캐스팅
}
package chap04.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<T>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
//val b1: Box<Cat> = Box<Animal>()
//val b2: Box<Animal> = Box<Cat>()
val b3 = Box<Spider>()
}
무변성의 경우, 상하 관계가 없다.
package chap04.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<out T>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
//val b1: Box<Cat> = Box<Animal>()
val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
}
out 키워드를 사용하면 업 캐스팅이 가능하다.
package chap04.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<in T>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
val b1: Box<Cat> = Box<Animal>() // 다운 캐스팅
//val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
}
in 키워드를 사용하면 다운 캐스팅이 가능하다.
package chap04.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
class Box<out T: Animal>
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 ${a1.size} ${a1.poison}")
//val b1: Box<Cat> = Box<Animal>() // 다운 캐스팅
val b2: Box<Animal> = Box<Cat>() // 업 캐스팅
val b3 = Box<Spider>()
//val b4: Box<Number> = Box<Int>() // 업 캐스팅
}
T를 Animal 타입으로 지정하면 위와 같이 Int 타입은 에러가 발생한다.
package chap04.section1.limit
open class Animal(val size: Int){
fun feed() = println("Feeding...")
}
class Cat(val jump: Int): Animal(50)
class Spider(val poison: Boolean): Animal(1)
// T를 out으로 지정하면
class Box<out T: Animal>(val element: T){
// 반환 타입으로만 사용 가능
fun getAnimal(): T = element
// 메서드 매개변수와 같은 in 위치에는 사용 불가
// fun setAnimal(new: T){
// element = T
// }
}
fun main() {
val c1 = Cat(10)
val s1 = Spider(true)
var a1: Animal = c1
a1 = s1
println("s1 -> ${a1.size} ${a1.poison}")
val b2: Box<Animal> = Box<Cat>(Cat(10)) // 공변성
println("b2.element.size -> ${b2.element.size}") // 50
//val b1: Box<Cat> = Box<Animal>(Cat(10)) // 반공변성
}
T를 out으로 지정하면 반환 타입으로만 사용할 수 있고, in 위치에는 사용 불가능하다.
class Box<in T: Animal>(var item: T)
class Box<T>(var item: T)
fun <T> printObj(box: Box<out Animal>) {
val obj: Animal = box.item // item의 값을 얻을 수 있음. (get)
//box.item = Animal() // set을 하려면 in으로 지정해야 함.
println(obj)
}
Box 클래스를 사용하는 시점에서 box의 item을 얻느냐(get) 설정하느냐(set)에 따라 out/in을 결정한다.
이러한 자료형 프로젝션으로 자료의 안전성을 보장할 수 있다.
in/out을 정하지 않고 추후에 결정할 수 있다. 어떤 자료형이라도 들어올 수 있으나, 구체적으로 자료형이 결정되고 나서는 그 자료형과 하위 자료형의 요소만 담을 수 있도록 제한한다.
class InOutTest<in T, out U>(t: T, u: U){
//val propT: T = t // out 위치에 사용 불가
val propU: U = u
//fun func1(u: U) // in 위치에 사용 불가
fun func2(t: T) {
print(t)
}
}
fun starTestFunc(v: InOutTest<*, *>){
//v.func2(1) // Nothing으로 인자를 처리함.
print(v.propU)
}
종류 | 예시 | 가변성 | 제한 |
---|---|---|---|
out 프로젝션 | Box<out Cat> | 공변성 | 형식 매개변수는 세터를 통해 값을 설정하는 게 제한됨. |
in 프로젝션 | Box<in Cat> | 반공변성 | 형식 매개변수는 게터를 통해 값을 읽거나 반환하는 게 제한됨. |
스타 프로젝션 | Box<*> | 모든 인스턴스는 하위 타입이 될 수 있음. | in과 out은 사용 방법에 따라 결정됨. |
cf) reify: 구체화하다.
reified 자료형이 필요한 이유는?
fun <T> myGenericFun(c: Class<T>)
c: Class<T>
처럼 지정해야만 실행 시간에 사라지지 않고 접근 가능하다. inline fun <reified T> myGenericFun()
Object::class // KClass
Object::class.java // Class
package chap04.section1
inline fun <reified T> getType(value: Int): T {
println(T::class) // 실행 시간에 삭제되지 않고 사용 가능
println(T::class.java)
return when(T::class){ // 받아들인 제네릭 타입에 따라 반환
Float::class -> value.toFloat() as T
Int::class -> value as T
else -> throw IllegalStateException("${T::class} is not supported!")
}
}
fun main() {
val result = getType<Float>(10)
println("result = $result")
}
class java.lang.Float (Kotlin reflection is not available)
class java.lang.Float
result = 10.0