본문 바로가기
Kotlin Language/Kotlin 기본 문법

Kotlin(코틀린) - 제네릭(Generic)

by Classic Master 2023. 12. 7.
728x90

제네릭(Generic)

  • 타입을 매개변수로 받아서 코드를 작성할 수 있게 하는 프로그래밍 기법입니다. 즉, 함수나 클래스를 선언할 때, 구체적인 타입을 지정하는 대신에 추상적인 타입 매개변수를 사용하여 타입에 독립적인 코드를 작성할 수 있도록 해줍니다.
  • 자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 자료형을 선택할 수 있게 해주는 것입니다. 앵글 브래킷 <> 사이에 형식 매개변수를 사용해 선언하며, 여기서 형식 매개변수는 자료형을 대표하는 용어로 T와 같이 특정 영문의 대문자를 사용합니다.

형식 매개변수 이름 규칙

  • 앵글 브래킷<> 사이에 형식 매개변수를 넣어 선언합니다.
    자료형을 대표하는 T와 같이 특정 영문의 대문자로 사용하며 나중에 피룡한 자료형으로 대체합니다
  •  E(Element), K(Key), N(Number), T(Type), V(Value) ,S(두번째) , U(세번째), V(네번째)

제네릭 3가지 타입

무공변성 (Invariance)

  • 무공변성은 제네릭 타입의 서로 다른 매개변수 간 하위 타입 관계가 성립하지 않음을 의미합니다.
  • 즉, 제네릭 타입 A<T>와 A<U> 간에는 T와 U 사이에 하위 타입 관계가 성립하지 않습니다.
class Box<T>(val item: T)
val stringBox: Box<String> = Box("Hello")
val anyBox: Box<Any> = stringBox // 에러 발생
  • Box<String>은 Box<Any>의 하위 타입이 아니므로 무공변성입니다.

공변성 (Covariance)

  • 공변성은 제네릭 타입 간의 하위 타입 관계가 유지되는 경우를 의미합니다.
  • 이는 클래스의 타입 매개변수에 out 키워드를 사용하여 선언합니다.
interface Producer<out T> {
    fun produce(): T
}
val stringProducer: Producer<String> = object : Producer<String> {
    override fun produce(): String = "Hello"
}
val anyProducer: Producer<Any> = stringProducer // 가능
  • Producer<String>은 Producer<Any>의 하위 타입이므로 공변성이 성립합니다.

반공변성 (Contravariance)

  • 반공변성은 제네릭 타입 간의 하위 타입 관계가 역전되는 경우를 의미합니다.
  • 이는 클래스의 타입 매개변수에 in 키워드를 사용하여 선언합니다.
interface Consumer<in T> {
    fun consume(item: T)
}
val anyConsumer: Consumer<Any> = object : Consumer<Any> {
    override fun consume(item: Any) {
        println(item)
    }
}
val stringConsumer: Consumer<String> = anyConsumer // 가능

 

  • Consumer<Any>은 Consumer<String>의 하위 타입이므로 반공변성이 성립합니다.

제네릭 함수 선언

// 매개변수와 리턴 타입에 사용됨. 
fun <T> genericFunc(arg: T): T? { ... }

// 형식 매개변수가 여러 개인 경우
fun <K, V> put(key: K, value: V): Unit { ... }
  • 해당 함수나 메서드 앞쪽에 <T>와 같이 지정합니
  • 자료형은 함수가 호출될 때 컴파일러가 추론합니다
  • 이 자료형은 리턴 타입과 매개변수 타입으로 사용될 수 있습니다.

 

제네릭 클래스 선언

class Box<T>(val item: T)
  • <T>는 클래스 Box에 대한 타입 매개변수로 사용되었습니다. Box 클래스는 어떤 타입의 객체도 담을 수 있는 상자를 나타냅니다.

 

제네릭 타입 경계

fun <T : Number> addNumbers(a: T, b: T): Double {
    return a.toDouble() + b.toDouble()
}
// 사용 예시
val result = addNumbers(3, 4) // T는 Int로 추론되며, 결과는 Double
  • <T : Number>는 타입 매개변수 T가 Number 클래스의 하위 타입이어야 함을 나타냅니다.

 

 

제네릭 사용시

fun <T> printItem(item: T) {
    println(item)
}
// 사용 예시
printItem("Hello, Kotlin!") // T는 String으로 추론됨
printItem(42) // T는 Int로 추론됨
  • Box 클래스와 printItem 함수를 사용할 때, 실제 타입을 명시하지 않고 제네릭 타입을 사용할 수 있습니다.

제네릭 코드 예시

공변성과 제네릭 람다식

class Box<out T>(val item: T)

fun <T> produceItem(producer: () -> T): Box<T> {
    return Box(producer())
}

val stringBox: Box<String> = produceItem { "Hello" }
val anyBox: Box<Any> = stringBox // 가능
  • Box 클래스는 공변성을 갖도록 선언되었습니다. 
  • 따라서 out 키워드를 사용하여 타입 매개변수 T가 출력 위치에서만 사용됩니다.
    produceItem 함수는 람다식을 인자로 받아 이를 이용하여 Box를 생성합니다.
    stringBox는 Box<String> 타입으로 생성되었고, anyBox는 Box<Any> 타입으로 대입이 가능합니다. 이는 Box가 공변성을 갖기 때문입니다

 

반공변성과 제네릭 람다식

class Box<in T> {
    var item: T? = null
}
fun <T> consumeItem(consumer: (T) -> Unit, item: T) {
    consumer(item)
}
val anyConsumer: (Any) -> Unit = { item: Any -> println(item) }
val stringConsumer: (String) -> Unit = anyConsumer // 가능

val stringBox = Box<String>()
consumeItem(stringConsumer, "Hello")
  • Box 클래스는 반공변성을 갖도록 선언되었습니다.
  •  따라서 in 키워드를 사용하여 타입 매개변수 T가 입력 위치에서만 사용됩니다
    consumeItem 함수는 람다식과 아이템을 인자로 받아 람다식을 실행합니다.
    anyConsumer는 Any 타입의 람다식으로, 이를 통해 stringConsumer에 String 타입의 람다식을 대입할 수 있습니다.
    stringBox는 Box<String> 타입으로 생성되었고, consumeItem 함수를 통해 stringConsumer를 실행하여 "Hello"를 출력합니다.

 

제네릭 람다식을 받는 함수

fun <T> manipulateItem(item: T, manipulator: (T) -> T): T {
    return manipulator(item)
}
val uppercaseManipulator: (String) -> String = { it.toUpperCase() }
val result: String = manipulateItem("hello", uppercaseManipulator)
println(result) // 출력: HELLO
  • manipulateItem 함수는 제네릭 타입의 아이템과 해당 아이템을 조작하는 람다식을 인자로 받아 조작된 결과를 반환합니다.
    uppercaseManipulator는 String 타입의 람다식으로, 문자열을 대문자로 변환하는 동작을 수행합니다.
    manipulateItem 함수를 통해 "hello"를 uppercaseManipulator로 조작하면 "HELLO"가 반환되어 결과로 출력됩니다.
fun <T> manipulateItem(item: T, manipulator: (T) -> T): T {
    return manipulator(item)
}
  • fun <T> 함수가 제네릭 함수임을 나타냅니다. 함수의 타입 매개변수로 T를 사용합니다.
    manipulateItem(item: T, manipulator  (T) -> T) 함수는 item과 manipulator라는 두 개의 매개변수를 받습니다.
    item 제네릭 타입 T의 아이템으로, 함수가 조작할 값입니다.
    manipulator (T) -> T 타입의 람다식을 받는데, 이는 T 타입을 받아서 T 타입을 반환하는 함수입니다.
    return manipulator(item) manipulator 함수에 item을 전달하여 실행하고, 그 결과를 반환합니다.
    manipulator(item) 전달된 item을 manipulator 함수에 적용한 결과를 나타냅니다.
    return 해당 결과를 함수의 반환 값으로 돌려줍니다.
728x90
반응형