본문 바로가기

Language/Kotlin

[Kotlin] 8장. 제네릭과 배열 1 - 제네릭

728x90

제네릭

제네릭은 클래스 내부에서 사용할 자료형을 나중에 인스턴스를 생성할 때 확정한다.

객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안정성을 높이고 형 변환의 번거로움이 줄어든다.

제네릭의 일반적인 사용법

앵글 브래킷(<>) 사이에 형식 매개변수를 넣어 선언하며, 형식 매개변수는 하나 이상 지정할 수 없다.

 

 

box3,4 처럼 생성자에서 유추될 수 있는 자료형이 있다면 선언된 자료형을 생략하고 호출이 가능하다.

null 제어

제네릭은 기본적으로 null이 가능한 형태로 선언된다.

 

 

자료형에 ?를 붙여서 선언하게 되면 해당 값은 null이 들어갈 수 있다는 표시이다. 따라서 obj 객체에는 null 값이 들어가면 에러로 처리되지만, obj2 객체는 null 값이 들어가도 정상적으로 동작한다.

null을 제한하기 위해서는 제네릭에 사용되는 자료형은 Any 타입으로 제한하여 ? 키워드를 사용할 수 없게 만들면 된다.

obj4를 보면 NotNull 클래스가 Any 타입으로 자료형을 제한했기 때문에 ? 를 붙인 자료형에 에러 표시가 나오는것을 알 수 있다.

제네릭 함수, 메서드

제네릭을 함수 또는 메서드에 사용하기 위해서는 앞쪽에 <T> 와 같이 형식 매개변수를 지정해 주어야 한다.

 

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

fun <T> sample1(arg: T): T? { ... }

fun <K, V> sample2(key: K, value: V): Unit { ... }

제네릭 람다식

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

 

fun <T> add(a: T, b: T): T {
	return a + b // 자료형을 결정할 수 없으므로 오류
}

 

하지만, 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨줄 때 결정되므로 해당 문제를 해결할 수 있다.

 

 

{ a, b -> a + b } 는 add 함수가 실행될 때 넘겨지는 인자이므로 연산식을 함수 선언부에 직접 구현하지 않고 전달한다. 따라서 자료형을 특정하지 않아도 실행이 가능하게 된다.

result2는 람다식 부분은 따로 구분하여 사용하는 방식이다.

자료형 제한하기

Kotlin에서 자료형을 제한하기 위해서는 형식 매개변수 다음에 콜론(:)과 자료형을 기입하면 형식 매개변수의 자료형이 제한된다.

Int, Long, Double형과 같이 숫자형으로만 제한하기 위해서 Number 형을 사용하면 다음과 같게 선언이 가능하다.

 

class Calc<T: Number> {
	fun plus(arg1: T, arg2: T): Double {
		return arg1.toDouble() + arg2.toDouble()
	}
}

 

이와 같이 선언하면, 해당 클래스를 사용할 때 숫자형을 제외한 자료형을 사용한다면 오류가 발생하게 된다.

 

함수에서 자료형을 제한하기 위해서는 함수 선언시 앞쪽에 형식 매개변수를 지정해 줄 때 위의 클래스와 마찬가지로 콜론과 자료형을 기입해주면 된다.

 

fun <T: Number> add(a: T, b: T, op: (T, T) -> T) : T {
	return add(a,b)
}

다수 조건의 형식 매개변수 제한하기

형식 매개변수의 자료형을 제한할 때 하나가 아닌 여러개의 조건에 맞춰 제한하고자 할 때, where 키워드를 사용하면 된다. where 키워드를 통해 지정된 제한을 모두 포함하는 경우에만 허용되게 된다.

 

interface InterfaceA
interface InterfaceB

class HandlerA: InterfaceA, InterfaceB
class HandlerB: InterfaceA

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

fun main() {
	val obj1 = ClassA<HandlerA>() // 객체 생성 가능
	val obj2 = ClassA<HandlerB>() // InterfaceB가 없으므로 객체 생성 불가능
}

 

가변성의 3가지 유형

  • 공변성(Covariance) : T'가 T의 하위 자료형이면, C<T'>는 C<T>의 하위 자료형이다. 생산자 입장의 out 성질
  • 반공변성(Contravariance) : T'가 T의 하위 자료형이면, C<T>는 C<T'>의 하위 자료형이다. 소비자 입장의 in 성질
  • 무변성(Invariance) : C<T> 와 C<T'>는 아무 관계가 없다. 생산자 + 소비자

무변성

형식 매개변수에 in이나 out등으로 공변성이나 반공변성을 따로 지정하지 않으면 무변성으로 제네릭 클래스가 선언된다. 이 경우, 자료형의 상하관계를 잘 따졌어도 무변성이므로 타입 캐스팅이 되지 않고 자료형 불일치 오류를 발생시킨다.

 

공변성

형식 매개변수의 상하 자료형 관계가 성립. out 키워드를 사용한다.

 

 

공변성은 하위 자료형을 상위 자료형으로 할당할 수 있게 해준다. 따라서, anys와 같이 Any 타입에 Int 타입을 할당할 수는 있지만, Int 타입의 하위에 있는 Nothing 타입에 Int 타입을 할당할 수는 없다.

반공변성

상하 자료형 관계가 반대가 된다. in 키워드를 사용한다.

 

 

공변성의 예제와 반대로, 상위 자료형을 하위 자료형으로 할당할 수 있게 해준다. Nothing 타입이 Int 타입의 하위이므로 Int 타입을 Nothing 타입에 할당할 수 있으며, Any 타입은 Int 타입의 상위이므로 할당이 불가능하다.

 

자료형 프로젝션

사용 지점 변성

사용 지점 변성이란 클래스를 선언하면서 클래스 자체에 가변성을 지정하는 방식으로 클래스에 in/out 을 지정하는 것.

 

claa Box<T>(var item: T)

fun <T> sample(box: Box<out A>) {
	val obj: A = box.item
	println(obj)
}

 

이처럼 사용하고자 하는 요소의 특정 자료형에 in/out을 지정해 변성을 제한하는 것을 자료형 프로젝션 이라고 한다.

스타 프로젝션

위의 예제에서 Box의 자료형이 Box<Any?>가 되면 모든 자료형을 담을 수 있게 된다. 반면, Box<*>는 어떤 자료형이라도 들어올 수 있으나, 구체적으로 자료형이 결정되고 난 후에는 그 자료형과 하위 자료형의 요소만 담을 수 있도록 제한할 수 있다.

이렇게 in/out을 정하지 않고 스타(*)를 통해 지정하는 방법을 스타 프로젝션이라고 한다.

 

 

스타 프로젝션을 사용할 때, in으로 정의되어 있는 형식 매개변수의 경우 in Nothing 으로 간주하고, out으로 정의되어 있는 형식 매개변수의 경우 out Any?로 간주한다.

Nothing 클래스는 최하위 자료형으로 아무것도 가지고 있지 않은 클래스이다. 보통 아무것도 존재하지 않는 값을 표현할 때 사용한다.

refied 자료형

결정되지 않은 제네릭 자료형은 컴파일 시간에 접근 가능하나, 함수 내부에서 사용하려면 다음과 같이 지정해야 한다.

 

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

 

만약에 reified로 형식 매개변수 T를 지정하면 실행 시간에 접근할 수 있어 Class<T> 형태로 넘기지 않아도 되며 일반 클래스처럼 사용할 수 있다.

 

inline fun <reified T> sample()

 

 

728x90