[Kotlin] Ch4-1. 제네릭

leeeha·2022년 8월 30일
0

코틀린

목록 보기
21/29
post-thumbnail

출처: https://www.boostcourse.org/mo234/lecture/154303?isDesc=false

제네릭 (generic)

제네릭이란, 자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있게 해주는 것이다. 보통 앵글 브래킷(<>) 사이에 형식 매개변수를 사용해 선언하며, 여기서 형식 매개변수는 자료형을 대표하는 용어로 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 등을 설정해야 한다.


형식 매개변수의 Null 처리

제네릭의 형식 매개변수는 기본적으로 널이 가능한 형태로 선언된다. 널을 허용하지 않으려면, 특정 자료형으로 제한하면 된다.

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


제네릭 함수 (메서드)

  • 해당 함수나 메서드 앞쪽에 <T>와 같이 지정
  • 자료형은 함수가 호출될 때 컴파일러가 추론
  • 이 자료형은 리턴 타입과 매개변수 타입으로 사용될 수 있음.
// 매개변수와 리턴 타입에 사용됨. 
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)
}

자료형 제한하기

형식 매개변수를 특정한 자료형으로 제한할 수 있다.

  • 자료형의 사용 범위를 좁히기 위해서 자료형을 제한한다.
  • 자바에서는 extends나 super를 사용해 자료형을 제한할 수 있었다.
  • 코틀린은 콜론(:)과 자료형을 기입하면, 형식 매개변수 T의 자료형이 기입한 자료형으로 제한된다.
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 타입이다. 클래스가 아니다.

상하위 클래스

상위 클래스는 하위 클래스를 수용할 수 있다. 이때 하위 자료형은 상위 자료형으로 자동 형변환 된다. (타입 캐스팅)

가변성의 3가지 타입

용어의미
공변성 (covariance)T'이 T의 하위 자료형이면, C<T'>은 C<T>의 하위 자료형이다. 생산자 입장의 out 성질
반공변성 (contravariance)T'이 T의 하위 자료형이면, C<T>는 C<T'>의 하위 자료형이다. 소비자 입장의 in 성질
무변성 (invariance)C<T>와 C<T'>은 아무런 관계가 없다. 생산자 + 소비자 (디폴트)

무변성 (invariance)

  • 자료형 사이의 하위 자료형 관계가 성립하지 않음.
  • 코틀린에서는 따로 지정해주지 않으면 기본적으로 무변성

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)
}

공변성 (covariance)

  • 형식 매개변수 사이의 하위 자료형 관계가 성립
  • 하위 자료형 관계가 그대로 인스턴스 자료형 사이의 관계로 이어지는 경우
  • 하위 클래스의 자료형을 상위 클래스의 자료형으로 캐스팅 하는 경우 (업 캐스팅)
  • out 키워드를 사용해 정의
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)
}

반공변성 (contravariance)

  • 자료형의 상하 관계가 반대
  • 상위 클래스의 자료형을 하위 클래스의 자료형으로 캐스팅 하는 경우 (다운 캐스팅)
  • in 키워드를 사용해 정의
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 위치에는 사용 불가능하다.


자료형 프로젝션

declaration-site variance (선언 지점)

  • 클래스 자체에 가변성을 지정하는 방식으로 클래스에 in/out을 지정할 수 있다.
  • 선언하면서 지정하면 클래스의 공변성을 전체적으로 지정하는 것이어서, 클래스를 사용하는 장소에서는 따로 자료형을 지정해줄 필요가 없다.
class Box<in T: Animal>(var item: T) 

use-site variance (사용 지점)

  • 메서드의 매개변수 또는 제네릭 클래스를 생성할 때와 같이 사용 위치에서 가변성을 지정하는 방식
  • 형식 매개변수가 있는 자료형을 사용할 때마다 해당 변수를 하위 자료형이나 상위 자료형 중 어떤 자료형으로 대체할 수 있는지 명시해야 한다.
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)
}
  • in으로 정의되어 있는 형식 매개변수를 *로 받으면 in Nothing 타입으로 간주
  • out으로 정의되어 있는 형식 매개변수를 *로 받으면 out Any? 타입으로 간주
종류예시가변성제한
out 프로젝션Box<out Cat>공변성형식 매개변수는 세터를 통해 값을 설정하는 게 제한됨.
in 프로젝션Box<in Cat>반공변성형식 매개변수는 게터를 통해 값을 읽거나 반환하는 게 제한됨.
스타 프로젝션Box<*>모든 인스턴스는 하위 타입이 될 수 있음.in과 out은 사용 방법에 따라 결정됨.

reified 자료형

cf) reify: 구체화하다.

reified 자료형이 필요한 이유는?

fun <T> myGenericFun(c: Class<T>)

  • T 자료형은 실행 시간에 삭제된다.
  • 컴파일 시간에는 접근 가능하지만, 함수 내부에서 사용하려면 위의 코드에서 함수의 매개변수를 넣어 c: Class<T>처럼 지정해야만 실행 시간에 사라지지 않고 접근 가능하다.

inline fun <reified T> myGenericFun()

  • reified로 형식 매개변수 T를 지정하면 실행 시간에 접근 가능하다.
  • reified 자료형은 inline 함수에서만 사용할 수 있다. (컴파일러가 인라인 함수의 코드를 복사해 넣을 때 실제 자료형을 알 수 있기 때문에 실행 시간에도 사용할 수 있게 되는 것)

Class<T>와 KClass

Class<T>

  • .class 형태로 반환 받는 객체
  • Class라는 클래스는 원본 클래스에 대한 많은 메타 데이터를 가짐. (패키지명, 메서드, 필드, 구현된 인터페이스, 각종 검사 변수 등)

Object::class

  • 코틀린의 표현 방법으로 KClass를 나타냄.
  • 자바의 클래스를 가져오려면 .java 사용
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

profile
습관이 될 때까지 📝

0개의 댓글