본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin] 제네릭(Generic)

by 주 녕 2021. 5. 18.
728x90

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

제네릭(Generic)

: 클래스 내부에서 사용할 자료형을 인스턴스를 생성할 때 확정하는 것

자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검색해 적당한 자료형을 선택할 수 있도록 함

→ 객체 자료형의 안정성을 높이고, 형 변환의 번거로움이 줄어듦

 

제네릭의 사용 방법

  • 앵글 브래킷(<>) 사이에 형식 매개변수(하나 이상)를 넣어 선언
  • 형식 매개변수 : 자료형을 대표하는 T와 같이 특정 영문의 대문자로 사용하며 나중에 필요한 자료형으로 대체됨
    • 일종의 규칙처럼 사용되는 이름 (변경가능)
    • E (Element), K(Key), N(Number), T(Type), V(Value), S(두번째), U(세번째), V(네번째)...
class Box<T>(t: T) {
    var name = t
}

fun main() {
    val box1: Box<Int> = Box<Int>(1)
    val box2: Box<String> = Box<String>("Hello")
    val box3 = Box(3)
    println(box1.name)
    println(box2.name)
}
  • 객체를 생성할 때 자료형이 결정됨
  • 객체 생성 시, 생성자에서 유추될 수 있는 자료형이 있다면 <Int>나 <String>은 생략 가능

 

[ 제네릭 클래스 ]

: 형식 매개변수를 1개 이상 받는 클래스

위의 예시에서  Box<T> 도 제네릭 클래스

// 불가능
class MyClass<T> {
    var myProp: T
}

// 가능
class MyClass<T>(val myProp: T) { }  // 주 생성자
class MyClass<T> {
    val myProp: T
    constructor(myProp: T) {  // 부 생성자
        this.myProp = myProp
    }
}
  • 클래스 프로퍼티에 사용하는 경우 클래스 내부에서는 사용할 수 없음 → 자료형이 특정되지 못하기 때문에 인스턴스 생성 X
  • 주 생성자나 부 생성자에 형식 매개변수를 지정하여 사용 가능
    • 객체 인스턴스를 생성할 때 명시적으로 자료형 지정 가능

 

[ 제네릭 함수 / 메서드 ]

: 형식 매개변수를 받는 함수나 메서드

함수가 호출될 때 컴파일러가 자료형을 추론할 수 있고, 이 자료형은 반환 자료형과 매개변수 자료형에 사용할 수 있음

 

fun <형식 매개변수[, ...]> 함수 이름(매개변수: <매개변수 자료형>[, ...]): <반환 자료형>

fun <T> find(a: Array<T>, Target: T): Int {
    for(i in a.indices) {
        if (a[i] == Target) return i
    }
    return -1
}

fun main() {
    val arr1: Array<String> = arrayOf("Apple", "Banana", "Cherry")
    val arr2: Array<Int> = arrayOf(1, 2, 3, 4)

    println("arr.indeces ${arr1.indices}")
    println(find<String>(arr1, "Cherry"))
    println(find(arr2, 2))  // 자료형이 특정되어 있는 경우, 생략 가능
}

 

[ 제네릭과 람다식 ]

형식 매개변수로 선언된 함수의 매개변수를 연산할 때는 자료형을 결정할 수 없기 때문에 오류가 발생함

→ 자료형을 나중에 넘겨서 결정하는 방식을 사용 : 람다식

→ 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨줄 때 결정되므로 문제 해결 가능

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)
    
    var sumInt: (Int, Int) -> Int = {a, b -> a+b}
    var sumInt2 = {a: Int, b: Int -> a+b}
    println(add(2, 3, sumInt))
    println(add(2, 3, sumInt2))
}
  • 람다식 {a, b -> a+b}은 add() 함수가 실행될 때 넘겨지는 인자
    • 연산식을 함수 선언부에 직접 구현하지 않고 전달하는 방법
    • 함수의 형식 매개변수의 자료형을 특정하지 않아도 사용 가능

 


자료형 제한하기

제네릭 클래스나 메서드가 받는 형식 매개변수를 특정한 자료형으로 제한할 수 있음

자바에서는 extends나 super를 사용해 자료형을 제한했지만, 코틀린에서는 콜론(:)을 사용해 제한

 

[ 형식 매개변수의 null 제어 ]

제네릭의 형식 매개변수는 기본적으로 null이 가능한 형태로 선언됨

class GenericNull<T> {
    fun EqulityFunc(arg1: T, arg2: T) {
        println(arg1?.equals(arg2))
    }
}

fun main() {
    val obj = GenericNull<String>()  // non-null
    obj.EqulityFunc("Hello", "World")
    
    val obj2 = GenericNull<Int?>()  // null이 가능한 형식
    obj2.EqulityFunc(null, 10)
}
  • GenericNull<Int?> : null을 허용하도록 자료형에 ? 기호를 사용함
    • obj2.EqualityFunc(null, 10) : null을 사용하고 있으므로 코드에서 안전한 작업을 위해 ?.를 사용
    • null인 경우 equals()로 비교하지 않고 null을 반환함
  • class GenericNull<T: Any> : null을 허용하지 않는 방법
    • 형식 매개변수를 null이 아닌 Any로 제한하여 null을 지정할 수 없게 함

 

[ 클래스와 함수에서 형식 매개변수의 자료형 제한 ]

  • 클래스 : class Calc <T: Number>
  • 함수 : fun <T: Number> addLimit(a: T, b: T, op: (T, T) -> T) : T {...}

예시에서는 Number 로 자료형을 제한함.

따라서 Number형이 아닌 String과 같이 자료형이 맞지 않으면 Type argument is not within its bouds와 같은 오류가 발생함

 

자료형을 제한할 때, 하나가 아닌 여러 개의 조건에 맞춰 제안할 때 → where 키워드 사용

interface InterfaceA
interface InterfaceB

class HandlerA: InterfaceA, InterfaceB
class HandlerB: InterfaceB

class ClassA<T> where T:InterfaceA, T:InterfaceB

fun <T> myMax(a: T, b: T): T where T:Number, T:Comparable<T> {
    return if(a>b) a else b
}

fun main() {
    val obj1 = ClassA<HandlerA>()
    val obj2 = ClassA<HandlerB>() // 범위에 없으므로 오류
}

 


상·하위 형식의 가변성

가변성(Variance) : 형식 매개변수가 클래스 계층에 영향을 주는 것

[ 클래스와 자료형 ]

String은 클래스이자 자료형이지만, String?은 자료형일 뿐 클래스는 아님

List는 클래스이자 자료형이지만, List<String>은 자료형일 뿐 클래스는 아님

 

보통 클래스는 파생된 하위 클래스와 상위 클래스가 존재함

ex) Int는 Number의 하위 클래스

상위 클래스는 하위 클래스를 수용할 수 있기 때문에 Int형 변수는 Number형 변수로 할당되어 형변환이 이루어짐

 

ex) Int는 Int?의 하위 자료형

Int(하위 자료형)는 Int?(상위 자료형)에 할당하는 것이 가능함

 

[ 자료형 변환 ]

제네릭 클래스는 가변성을 지정하지 않으면 형식 매개변수에 상·하위 클래스가 지정되어도 자료형이 변환되지 않음

open class Parent

class Child : Parent()

class Cup<T>

fun main() {
    val obj1: Parent = Child()  // Parent 형식은 Child 자료형으로 변환될 수 있음
    val obj2: Child = Parent()  // 반대로 변환할 수는 없음
    
    // 자료형 불일치
    val obj3: Cup<Parent> = Cup<Child>()
    val obj4: Cup<Child> = Cup<Parent>()
    
    // 자료형 일치
    val obj5 = Cup<Child>()
    val obj6: Cup<Child> = obj5
}
  • 상위 클래스는 하위 클래스 형으로 변환될 수 있음
  • 제네릭 클래스에서는 형식 매개변수인 T에 상·하위 클래스를 지정하더라도 서로 관련이 없는 형식임
    • obj3와 obj4 처럼 자료형 불일치 오류가 발생하게 됨
    • obj5와 obj6과 같이 같은 자료형은 문제없이 할당 가능

 

[ 가변성의 3가지 유형 ]

무변성(Invariance)

: c<T>와 C<T'>는 아무 관계가 없다 (생산자+소비자)

class Box<T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10)  // 자료형 불일치 오류
    val nothings: Box<Nothing> = Box<Int>(20)  // 자료형 불일치 오류
}
  • in이나 out으로 공변성이나 반공변성을 따로 지정하지 않으면 무변성으로 선언됨
  • 상하 관계를 가지고 있는 Any, Int형 자료형과 상하 관계를 가지고 있는 Int형 자료형과 Nothing
    • 상하 관계를 잘 따졌어도 Box<T>가 무변성이므로 자료형 불일치 오류 발생

 

공변성(Covariance)

: T'가 T의 하위 자료형이면, C<T'>는 C<T>의 하위 자료형이다 (생산자 입장의 out 성질)

class Box<out T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10)  // 객체 생성 가능
    val nothings: Box<Nothing> = Box<Int>(20)  // 자료형 불일치 오류
}
  • 형식 매개변수의 상하 자료형 관계가 성립하고, 그 관계가 그대로 인스턴스 자료형 관계로 이어지는 경우
  • out 키워드 : 형식 매개변수가 공변적으로 선언되는 것
  • Int가 Any의 하위 자료형일 때, 형식 매개변수 T에 대해 공변적
    • Any의 하위 클래스인 Int는 공변성을 가지므로 Box<Any>에 Box<Int> 자료형을 할당할 수 있음
    • <Nothing>은 <Int>의 하위 자료형이 아니므로 오류 발생

 

반공변성(Contravariance)

: T'가 T의 하위 자료형이면, C<T>는 C<T'>의 하위 자료형이다. (소비자 입장의 in 성질)

class Box<in T>(val size: Int)

fun main() {
    val anys: Box<Any> = Box<Int>(10)  // 자료형 불일치 오류
    val nothings: Box<Nothing> = Box<Int>(20) // 객체 생성 가능
}
  • 자료형의 상하 관계가 반대가 되어 인스턴스의 자료형이 상위 자료형이 됨
  • 공변성과 반대의 경우
    • Box<Nothing> 자료형의 상위 자료형이 Box<Int> 이므로 객체를 생성할 수 있음

 

 

 

* 람다식에 대한 정리 포스팅

 

Kotlin | 3장 : 함수와 함수형 프로그래밍 (2)

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 고차 함수와 람다식 다른 함수를 인자로 사용하거나 함수를 결과값으로 반환하는 함수 고차 함수의 형태 일반 함수를 인자

junyoung-developer.tistory.com

 

728x90

댓글