Kotlin

제네릭

kakaroo 2022. 5. 8. 16:59
반응형

제네릭이란 형식 타입 혹은 임의 타입입니다.

자바나 코틀린 같은 대부분 프로그래밍 언어는 변수(혹은 객체) 타입을 명확하게 선언해야 합니다. String, Int 등의 타입을 명시해야 하는데, 때로는 클래스, 함수, 변수 등을 선언할 때 타입을 예측할 수 없거나 하나의 타입으로 고정할 수 없는 경우가 있습니다. 그렇다고 타입을 선언하지 않을 수는 없습니다. 이때 사용하는 것이 제네릭입니다.

article logo

 

클래스나 함수, 변수 등을 선언할 때 제네릭으로 형식타입을 선언하고, 실제 이용할 때 정확한 타입을 부여합니다.

제네릭을 가장 쉽게 볼 수 있는 곳이 컬렉션 타입입니다.

val array = arrayOf("Kim", 10, true)

배열에 대입하는 데이터 타입은 문자열이나 숫자 등 다양할 수 있습니다. 그런데, 코틀린에서는 대입하는 모든 데이터의 타입이 선언되어 있어야 하는데, 어떻게 위의 예처럼 arrayOf() 함수에 문자열과 숫자 등 서로 다른 데이터를 대입할 수 있을까요? 그 이유는 arrayOf() 함수가 제네릭으로 선언되었기 때문입니다.

arrayOf 정의 함수

 

val array2 = arrayOf<Int>(10, 20)

T: Type parameter

Int : Type argument

 


사용 방법

1. 클래스 내에서 사용

클래스 선언 부분에 형식 타입<T>을 선언해야 합니다.

class MyClass<T> {
	var data: T? = null
}

class MyClass2<T, A> {
	var data1: T? = null	//프로퍼티 변수의 제네릭
    var data2: A? = null
    
    fun myFun(arg: T): A? {	//프로퍼티 함수의 제네릭
    	return data2
    }
}

val obj: MyClass2<String, Int> = MyClass2()
obj.data1 = "Kim"
obj.data2 = 10

 

2. 최상위 레벨의 함수에 사용

fun <T> myGeneric(arg: T) : T? {
	return null
}

 

* 최상위 레벨에 선언하는 프로퍼티에는 사용할 수 없습니다.

 


제네릭 제약

제네릭을 이용할 때 특정 타입만 지정할 수 있도록 제약하는 것을 의미합니다.

예를 들어 제네릭으로 선언하려는 클래스가 수치 계산과 관련된 더하기, 빼기 등의 작업을 수행한다면 이 클래스를 이용하는 곳에서, Int, Double 등의 타입을 지정하여 이용하면 됩니다. 그런데 만약 String 으로 지정하여 이용하면 클래스에서 수치 계산 작업이 불가능해집니다. 이 때는 지정할 수 있는 타입을 Number의 하위타입으로 제한해야 합니다.

class MathUtil<T: Number> {		//<T: Number>로 제네릭 제약 선언
	fun plus(arg1: T, arg2: T): Double {
    	return arg1.toDouble() + arg2.toDuble()
    }
}

 

Null 불허 제약

Null 을 허용하고 싶지 않을 때는 <T: Any>로 선언해야 합니다.

제네릭의 형식 타입은 기본적으로 Null을 허용합니다. class MyClass<T> 로 선언하였다면 class MyClass<T: Any?> 로 선언한 것과 같습니다. Any? 로 선언하므로 실사용 때 어떤 타입도 지정할 수 있고, Null도 허용한다는 의미입니다.

 

 


Variance

클래스 상속 관계에서 Sub 클래스 객체는 Super 클래스 타입의 객체로 캐스팅 될 수 있습니다.

open class Super
class Sub: Super()

val obj: Super = Sub()

 

클래스의 상하위 관계에 의한 캐스팅을 제네릭에서 그냥 사용할 수는 없습니다. 제네릭은 타입이지, 클래스가 아니므로 MyClass<Sub>로 명시한 타입을 MyClass<Super> 타입에 대입할 수는 없습니다. 

 

 

out annotation (covariance)

>> 하위 타입을 상위타입으로 사용

open class Super
class Sub: Super()

class MyClass<out T>	//out annotation 사용

val obj1 = MyClass<Sub>()
val obj2: MyClass<Super> = obj1

val obj3 = MyClass<Super>()
val obj4: MyClass<Sub> = obj3	//error

 

제네릭의 형식 타입을 out 어노테이션으로 선언할 때 이용방법을 정리하면 다음과 같습니다.

  • 하위 제네릭 타입을 상위 제네릭 타입에 대입 가능
  • 상위 제네릭 타입을 하위 제네릭 타입에 대입 불가능
  • 함수의 반환 타입으로 선언 가능
  • 함수의 매개변수 타입으로 선언 불가능
  • val 프로퍼티에 선언 가능
  • var 프로퍼티에 선언 불가능

 

제네릭 타입을 형 변환할 수 있다면 사용 편의성이 좋아집니다. out을 이용한 제네릭 Variance가 주는 편리함은 List와 MutableList의 차이를 보면 이해할 수 있습니다.

val mutableList1: MutableList<Int> = mutableList(10,20)
val mutableList2: MutableList<Number> = mutableList1	//error

val immutableList1: List<Int> = listOf(10,20)
val immutableList2: List<Number> = immutableList1	//ok

MutableList는 가변리스트이므로 add() 함수 등을 이용해 데이터를 추가할 수 있습니다. 선언 할 때 짖어한 제네릭 타입과 같은 타입의 데이터를 추가할 때는 문제가 없지만, 만약 다른 타입을 대입하면 MutableList 내부에서 이에 대한 대응이 불가능합니다. 그래서 MutableList 정의에 Variance가 적용되지 않았습니다.

 

이와 반대로 List는 불변 리스트로 초기 데이터 외에 데이터를 추가할 수 없습니다.  조금 더 가변적으로 제네릭 타입에 의한 형 변환을 허용할 수 있게 out 어노테이션이 정의되어 있습니다.

 

public interface MutableList<E> : List<E>, MutableCollection<E> { }
public interface List<out E> : Collection<E> { }

 

 

in annoation (contravariance)

대부분의 규칙이 out과 반대로 상위 제네릭 타입을 하위 제네릭 타입에 대입할 수 있게 해줍니다.

in 어노테이션으로 선언한 제네릭 형식타입을 이용하는 방법은 다음과 같습니다.

  • 하위 제네릭 타입을 상위 제네릭 타입에 대입 불가능
  • 상위 제네릭 타입을 하위 제네릭 타입에 대입 가능
  • 함수의 반환 타입으로 선언 불가능
  • 함수의 매개변수 타입으로 선언 가능
  • var 또는 val 프로퍼티에 선언 불가능
open class Super
class Sub: Super()

class MyClass<in T> {
	val data1: T? = null	//error
    var data2: T? = null	//error
    
    fun myFunc1(): T? {	//error
    	return null
    }
    fun myFunc2(arg: T) { }	//ok
}

val obj1 = MyClass<Super>()
val obj2: MyClass<Sub> = obj1

val obj3 = MyClass<Sub>()
val obj4: MyClass<Super> = obj3	//error

 

이용측 variance

지금까지 선언부에 variance 를 지정했지만, 이용하는 측에서 variance를 지정할 수도 있습니다.

아래 TestClass에는 annotation 이 없는 상태로 invariance 상태입니다. invariance는 제네릭 타입의 형 변환을 허용하지 않습니다. 아래처럼 some() 함수의 파라미터로 TestClass<Int> 으로 지정했기 때문에 TestClass<Number> 타입의 파라미터는 허용되지 않습니다.

 

in / out 어노테이션으로 이용측 variance도 가능해집니다.

아래처럼 in 어노테이션을 명시하면 variance가 가능해집니다.

 

out 어노테이션은 함수의 매개변수로 사용할 수가 없기 때문에 에러가 발생합니다.

 

 

스타(*) 프로젝션

제네릭 타입을 <*>로 표현하는 것을 의미합니다. 스타 프로젝션은 제네릭 타입을 모른다는 의미입니다. 나중에 정확한 타입으로 이용되기는 하지만, 지금은 어떤 제네릭 타입이 지정될지 모른다는 의미로 사용합니다. 스타 프로젝션은 제네릭의 선언 측에서는 사용할 수 없으며 이용측에서만 사용할 수 있습니다.

class MyClass1<*>	//error ; 선언측에서는 사용불가
class MyClass2<T>

fun myFun(arg: MyClass2<*>){ }	//스타 프로젝션 사용

 

 

제네릭과 as, is

제네릭 정보는 컴파일러를 위한 정보이며 컴파일이 완료된 후 실행 때는 제네릭 정보가 사라집니다.

컴파일 후 제네릭 정보 제거

 

func2() 함수의 매개변수 타입은 List<*> 입니다. 이곳에는 List<Int>, ListMDouble> 등 다양한 제네릭 타입의 List를 대입할 수 있습니다. if 문에서 is 연산자로 타입을 체크하는데 "Cannot check instance of erased type" 이라는 컴파일 에러가 발생합니다. 그 이유는 제네릭 정보가 컴파일 때 제거되므로 <*>타입이 <Int>타입인지 확인할 수가 없기 때문에 에러가 발생합니다.

fun func1(arg: List<Int>) {
	if(arg is List<Int>) {
    	println(arg.sum())
    }
}

fun func2(arg: List<*>) {
	if(arg is List<Int>) {	//error
    }
}

 

제네릭에서 as 연산자를 사용하는 예의 문제점입니다.

fun func3(arg: List<*>) {
    val intList = arg as List<Int>
    println(intList)		//ClassCastException ; java.lang.String cannot be cast to java.lang.Number
}

func3(listOf(10,20))
func3(listOf("hello","Kim"))	//error 유발

실행 시점에 타입을 점검하지 않다 보니 as에 의해 형 변환은 되는데 변환된 데이터를 이용하다가 에러가 발생합니다.

제네릭 타입이 실행 시점까지 남아 있지 않기 때문에 is 와 as 연산자를 사용할 때 문제가 발생할 수 있습니다.

이러한 제네릭의 문제점을 해결하기 위해 reified 키워드를 사용합니다.

아래 포스팅을 참조하세요.

http://kakaroo.tistory.com/69

 

inline 함수

인라인(inline) 키워드는 자바에서는 제공하지 않는 코틀린만의 키워드입니다. 이러한 인라인 키워드를 이용하여 함수를 만들고 이를 잘 활용한다면 다양한 이득을 얻을 수 있는 경우가 있습니다

kakaroo.tistory.com

 

 

반응형

'Kotlin' 카테고리의 다른 글

data class  (0) 2022.05.06
연산자 함수, 오버로딩  (0) 2022.05.05
위임자 Delegates (observable, vetoable )  (0) 2022.05.05
전개 연산자 *  (0) 2022.05.05
생성자 (constructor)  (0) 2022.04.30