최종 수정 날짜 : 2022-03-30 17:32:45 +0900
Generics
- 제네릭(Generic)은 타입이 정해져있지 않은 클래스나 함수를 의미하며, 클래스가 인스턴스화 될 때 타입이 확정된다.
- 다양한 타입(자료형)을 다룰 수 있기 때문에
컬렉션
에서 자주 사용한다.- 컬렉션 : List, Map, 등..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Number<T>(private val value: T) {
fun getType() {
println(value!!::class.java.typeName)
}
}
fun main() {
val number = Number<Double>(3.0)
number.getType()
val number2 = Number(10)
number2.getType()
val number3 = Number<Boolean>(true)
number3.getType()
}
// 결과
// java.lang.Double
// java.lang.Integer
// java.lang.Boolean
제네릭 타입의 클래스를 생성하기 위해서는 단순히 클래스 뒤에
<타입>
형태를 취해 생성할 수 있다.위 예제의
number2
변수를 보면 따로 타입을 설정하지 않았는데, 이는 컴파일러가 유추할 수 있는 값이 생성자로 들어갔기 때문에 제거가 가능하다.제네릭에서 사용하는 형식 매개변수 이름은 아래와 같다.
- 아래 이름들을 사용해야한다는 강제성은 없지만 일종의 규칙처럼 사용되기 때문에 알아두면 좋다.
매개변수 이름 의미 E 요소 (Element) K 키 (Key) N 숫자 (Number) T 형식 (Type) V 값 (Value) R 리턴 (Return)
제네릭 함수 및 메소드
- 제네릭 함수는 아래와 같이 사용된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun <T> find(a: Array<T>, target: T): Int {
}
fun main() {
val array1 = arrayOf("Apple", "Banana")
val array2 = arrayOf(1, 2, 3)
println(find<String>(array1, "Banana"))
println(find(array2, 2))
}
// 결과
// 1
// 1
- 코틀린에서 사용하는 제네릭 함수들을 살펴본 결과 대부분 파일의 최상위에서 위와 같이 선언함을 알 수 있었다.
- 최상위 : 어떤 클래스에도 속해있지 않은 함수 또는 메소드
제네릭 클래스에서는 매개변수 이름(E, K, N, T, V 등)을 전달받기 때문에 위와 같이 제네릭 함수를 쓸 필요는 없지만, 최상위에 있는 제네릭 함수 또는 메소드에는 위처럼
fun
키워드 앞에 타입을 설정해주어야 하는 것으로 판단할 수 있다.- 제네릭 클래스와 마찬가지로
find
제네릭 함수 뒤에 값을 유추할 수 있다면<타입>
을 제거해서 사용이 가능하다.
타입 제한하기
- 제네릭으로 구현하게 되면 실수든 고의든 의도하지 않은 타입을 사용하게 될 수도 있다.
- 간단한 계산기로 예시를 들어보면 아래와 같다.
- 제네릭 함수나 메서드, 클래스에서는 연산(+, -, *, /)등을 수행할 수 없기 때문에 람다식으로 연산 후 전달하는 방법을 사용한다.
- 수행이 불가능한 이유는 자료형을 추론할 수 없기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calculator<T> {
fun add(a: T, b: T, op: (T, T) -> T): T {
return op(a, b)
}
}
fun main() {
val calculator = Calculator<Int>()
val data = calculator.add(10, 20) { a, b ->
a + b
}
println(data)
val calculator2 = Calculator<String>()
val data2 = calculator2.add("a", "b") { a, b ->
a + b
}
println(data2)
}
// 결과
// 30
// ab
우리가 생각하고 의도하는 계산기는 숫자를 더한 결괏값만 원하지만, 위와 같이
String
타입으로 제네릭 클래스를 인스턴스화 하더라도 막을 방법이 없다.이럴 때 숫자만 가능하도록 제약조건을 걸 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Calculator<T: Number> {
fun add(a: T, b: T, op: (T, T) -> T): T {
return op(a, b)
}
}
fun main() {
val calculator = Calculator<Int>()
val data = calculator.add(10, 20) { a, b ->
a + b
}
println(data)
val calculator2 = Calculator<String>() // <--- 오류 발생
val data2 = calculator2.add("a", "b") { a, b ->
a + b
}
println(data2)
}
특정 조건으로 타입 제한하기 (Where)
- 제네릭 타입으로 특정 조건을 만족하는 타입만 받고싶을 수 있다. 그럴 때
Where
키워드를 사용해 제한할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package generics
interface InterfaceA
interface InterfaceB
class ImplA : InterfaceA, InterfaceB
class ImplB : InterfaceA
fun <T> someFunction(): Int where T: InterfaceA, T: InterfaceB {
return 10
}
fun main() {
val implA = ImplA()
val implB = ImplB()
someFunction<ImplA>()
someFunction<ImplB>() // <--- 오류 발생
}
- 위 코드는 간단히
InterfaceA
,InterfaceB
를 상속받은ImplA
클래스와InterfaceA
만 상속받은ImplB
클래스를 이용해someFunction
제네릭 함수를 이용하려고 한 결과이다. 예시에서 볼 수 있듯이where
키워드로InterfaceA
와InterfaceB
를 상속받은 클래스만 허용하기 때문에 메인 함수의 두 번째someFunction
에서 에러가 발생하는 것을 알 수 있다.
가변성(Variance)의 유형
- 기본적으로 제네릭에서는 클래스 간의 상/하위 개념이 존재하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Parent
class Child : Parent()
class Box<T>
fun main() {
val value1: Parent = Child() // <--- 가능
val value2: Box<Parent> = Box<Child>() // <--- 불가능
// 에러 내용
// 타입이 일치하지 않습니다.
// 필요 항목:
// Box<Parent>
// 발견된 항목:
// Box<Child>
}
위 예시처럼 Child 클래스는 Parent 클래스의 모든 기능을 가지고 있으므로 선언이 가능(value1)하지만, 이것을 제네릭으로 표현(value2)하면 에러가 발생한다.
따라서 상/하위의 값을 클래스와 동일하게 사용하기 위해서는 가변성의 유형에 대해서 알아야 한다.
가변성의 유형은 아래와 같다.
공변성(Convariance)
: T’가 T의 하위 자료형이면, Box<T’>는 Box의 하위 자료형이다. (out) 반공변성(Contravariance)
: T’가 T의 하위 자료형이면 Box는 Box<T'>의 하위 자료형이다. (in) 무변성(Invariance)
: Box와 Box<T'>는 관계가 없다.
가변성의 관계를 다이어그램으로 나타내면 아래와 같다.
무변성(Invariance)
- 위 예시의
value2
가 무변성에 해당한다. 타입에in
이나out
으로 공변성 또는 반공변성을 선언하지 않을 경우 무변성으로 선언되어 상하관계와 상관없이 자료형 불일치가 발생한다.
공변성(Convariance)
- 예를 들어서 Int가 Any의 하위 자료형이므로 제네릭 타입 T에 대해 공변적이라고 한다.
1
2
3
4
5
6
class Box<out T>()
fun main() {
val anys: Box<Any> = Box<Int>()
val nothings: Box<Nothing> = Box<Int>() // <--- 에러 발생
}
- Int는 Any의 하위 자료형이므로 제네릭 클래스에
out
키워드를 선언해서 공변성을 적용할 수 있다. - 그러나 Int가 Nothing의 하위 타입이 아니므로 에러가 발생한다.
1
public class Nothing private constructor()
반공변성(Contravariance)
- 공변성의 상/하위 관계가 반대가 된다.
1
2
3
4
5
6
class Box<in T>()
fun main() {
val anys: Box<Any> = Box<Int>() // <--- 에러 발생
val nothings: Box<Nothing> = Box<Int>()
}
- Any는 Int의 하위 자료형이 아니므로 에러가 발생한다.
- 그러나 Nothing은 Int의 하위 자료형이므로
in
키워드를 적용해 반공변성을 적용할 수 있다.
- TODO
- reified 정리
- 프로젝션
- 가변성 예제 더 확인해보기
- [참고]
- https://kotlinlang.org/docs/generics.html
- Do It! 코틀린 프로그래밍 책
Comments powered by Disqus.