책 정리/코틀린 인 액션
(코틀린 인 액션) 9장 제네릭스
긍.응.성
2022. 9. 5. 23:28
반응형
9장 제네릭스
9.1 제네릭 타입 파라미터
9.1.1 제네릭 함수와 프로퍼티
val authors = listOf("Dmitry", "Svetlana")
val readers: MutableList<String> = mutableListOf()
// val readers = mutableListOf<String>()
fun <T> List<T>.slice(indices: IntRange): List<T>
val <T> List<T>.penultimate: T
get() = this[size - 2]
println(listOf(1, 2, 3, 4).penultimate)
- 제네릭을 사용하면 타입 파라미터를 받는 타입을 정의할 수 있다.
<>
안에 들어가는 타입이 타입 인자가 된다.- 값이 있다면 타입을 명시하지 않아도 값을 통해 타입 인자를 추론할 수 있다.
- 코틀린은 로 타입을 허용하지 않는다. 항상 제네릭 타입의 인자를 정의해야 한다.
- 빈 리스트와 같이 값이 없는 타입 인자를 만들 때는 명시적으로 타입 인자를 선언해야 한다.
- 타입 파라미터를 사용하는 함수, 프로퍼티인 경우 사용할 타입 파라미터를 선언해야 한다.
val <T> x: T = TODO()
// ERROR: type parameter of property must be used in its receiver type
- 확장 프로퍼티만 제네릭하게 만들 수 있다. 일반 프로퍼티는 타입 파라미터를 가질 수 없다.
9.1.2 제네릭 클래스 선언
interface List<T> {
operator fun get(index: Int): T
}
class StringList: List<String> {
override fun get(index: Int): String { ... }
}
class ArrayList<T>: List<T> {
override fun get(index: Int): T { ... }
}
- 클래스 뒤에 타입 파라미터를 붙여 제네릭 클래스를 생성할 수 있다.
9.1.3 타입 파라미터 제약 (type parameter constraint)
클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다.
fun <T: Number> oneHalf(value: T): Double {
return value.toDouble() / 2.0
}
fun <T: Comparable<T>> max(first: T, second: T): T {
return if (first > second) first else second
}
fun main() {
println(oneHalf(3))
println(max("kotlin", "java"))
}
- 타입 파라미터의 이름 뒤에
:
을 표시하여 상한(upper bound)을 지정할 수 있다. - 상한 타입이 지정된 경우 타입 인자는 반드시 그 상한 타입이거나 그 상한 타입의 하위 타입이어야 한다.
fun <T> ensureTrailingPeriod(seq : T) where T : CharSequence, T : Appendable {
if (!seq.endsWith('.')) {
seq.append('.')
}
}
}
where
절과 이후 콤마(,
)로 구분하여 타입 인자들에 대하여 하나 이상의 상한을 지정할 수 있다
9.1.4 타입 파라미터를 널이 될 수 없는 타입으로 한정
class NullableProcessor<T> {
fun process(value: T) {
value?.hashCode()
}
}
class Processer<T : Any> {
fun process(value: T) {
value.hashCode()
}
}
- 상한을 정하지 않은 타입 파라미터는
Any?
를 상한으로 정한 것과 같다. Any
를 상한으로 지정하여 널이 될 수 없는 타입으로 한정할 수 있다.
9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터
- JVM의 제네릭스는 타입 소거(
type erasure
)로 인해 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않는다. - 코틀린은 함수를 inline으로 선언함으로써 타입 인자가 지워지지 않게 할 수 있으며, 이를 실체화
reify
라고 부른다. - 실체화한 타입 파라미터가 무엇인지, 어떠한 장점들이 있는지 알아보자.
9.2.1 실행 시점의 제네릭: 타입 검사와 캐스트
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)
// 두 리스트의 타입 인자 정보는 런타임에 지워지므로 List로만 인식된다
if (list1 is List<String> { ... }
// ERROR: Cannot check for instance of erased type: List<String>
if (list1 is List<*>) { ... }
- 코틀린 제네릭 타입 인자 정보는 자바와 동일하게 런타임에 지워진다.
- 실행 시점에는 타입 인자를 검사할 수 없다.
- 코틀린에선 로 타입을 사용할 수 없기에 타입 인자를 알 수 없는 제네릭 클래스의 타입은 스타 프로젝션(
*
)을 통해 표현 가능하다.
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected")
// Unchecked Cast Warning
println(intList.sum())
}
printSum(listOf(1, 2, 3)) //성공 6
printSum(listOf("a", "b", "c")) // ClassCastException: String cannot be cast to Number
// as? 캐스트는 성공하고 sum 수행 시 Number로 캐스팅 시도하려다 오류가 발생
printSum(setOf(1, 2, 3)) // IllegalArgumentException: List is expected
as
,as?
캐스팅을 통해 제네릭 타입을 표현할 수 있지만 캐스팅은 항상 성공하기에 런타임에 예외가 발생할 수 있다.
9.2.2 실체화한 타입 파라미터를 사용한 함수 선언
inline fun <reified T> isA(value: Any) = value is T
println(isA<String>("abc")) // true
println(isA<String>(123)) // false
- 타입 파라미터 앞에
reified
를 붙여 실체화한 타입 파라미터로 지정할 수 있다. - 인라인 함수의 타입 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알 수 있다.
- 컴파일러는 인라인 함수를 호출하는 부분의 타입 인자를 알 수 있기에, 구체적인 클래스를 참조하는 바이트 코드를 생성해 넣을 수 있다(타입 인자가 소거되지 않는다).
9.2.3 실체화한 타입 파라미터로 클래스 참조 대신
val bookRepositoryImpl = ServiceLoader.load(BookRepository::class.java)
inline fun <reified T> loadService(): ServiceLoader<T> {
return ServiceLoader.load(T::class.java)
}
val bookRepositoryImpl = loadService<BookRepository>()
- 실체화한 타입 파라미터를 통해
java.lang.Class
타입 인자를 파라미터로 받는 API를 간략화할 수 있다.
9.2.4 실체화한 타입 파라미터의 제약
타입 파라미터로 할 수 있는 경우
- 타입 검사와 캐스팅 (
is, !is, as, as?
) - 코틀린 리플렉션 API (
::class
) - 코틀린 타입에 대응하는
java.lang.Class
참조 얻기(::class.java
) - 다른 함수를 호출할 때 타입 인자로 사용
할 수 없는 케이스
- 타입 파라미터 클래스의 인스턴스 생성
- 타입 파라미터 클래스의 동반 객체 메서드 호출하기
- 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
- 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를
reified
로 지정하기
9.3 변성: 제네릭과 하위 타입
- 변성(variance) : 기저 타입이 같고 타입 인자가 다른 여러 타입의 관계를 설명하는 개념
- 공변, 무공변, 반공변
9.3.1 변성이 있는 이유: 인자를 함수에 넘기기
fun addAnswer(list: MutableList<Any>) {
list.add(42)
}
val strings = mutableListOf("abc", "bac")
addAnswer(strings)
println(strings.maxBy { it.length })
// ClassCastException: Integer cannot be cast to string
MutableList<Any>
가 필요한 곳에MutableList<String>
을 넘기면 안된다- 위와 같은 경우 타입이 잘못되었다고 컴파일러에서 알려준다
Type mismatch: inferred type is MutableList<String> but MutableList<Any> was expected
클래스, 타입, 하위 타입
클래스와 타입
- 제네릭 클래스가 아닌 클래스에서는 클래스 이름을 타입으로 쓸 수 있다.
- 코틀린은 클래스 이름에
?
를 붙여 널이 될 수 있는 타입을 표현할 수 있다. - 모든 코틀린 클래스는 적어도 둘 이상의 타입을 구성할 수 있다
- List는 타입이 아니고 클래스이다.
List<Int>
,List<String?>
,List<List<String>>
은 타입이다.
하위 타입
- 타입 A 값이 필요한 모든 장소에 타입 B를 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이다.
상위 타입
- A 타입이 B 타입의 하위 타입이라면 B는 A의 상위 타입이다.
- 컴파일러가 변수를 대입하거나 함수 인자 전달 시 매번 하위 타입 검사를 수행하기 때문에 타입의 상위, 하위 관계는 중요하다.
9.3.3 공변성: 하위 타입 관계를 유지
open class Animal
class Cat: Animal()
var h = Herd<Animal>()
var h2 = Herd<Cat>()
h = h2 // type mismatch
// 타입 파라미터 앞에 out을 넣어주어 공변적으로 만든다
- 타입 파라미터 앞에
out
을 선언해주어 타입을 공변적으로 만들 수 있다. out
키워드는 공변성을 부여하는 것에 더해 사용을 제한하는 기능도 가지고 있다.out으로
선언한 타입 파라미터는 소비할 수 없고, 생산만 가능하다- 소비한다는 말은 파라미터로 받는다는 것을 의미한다.
- 생성한다는 말은 타입 파라미터를 반환한다는 것을 의미한다.
- 자바의
<? extends T>
와 같다.
class Herd<out T> {
fun get(i: Int): T { ... }
fun add(a: T) { ... }
// Type parameter T is declared as 'out' but occurs in 'in' position in type T
}
- 생성자는 인스턴스를 생성할 때에만 호출되므로
out
조건에 해당되지 않는다.- 하지만
out
인 경우 파라미터는 반드시val
로 선언되어야 한다.
- 하지만
- 변성 규칙은 외부에서 클래스를 잘못 사용하는 걸 막기 위한 것이므로, private API는 적용되지 않는다
9.3.4 반공변성: 뒤집힌 하위 타입 관계
val anyComparartor = Comparator<Any> {
o1, o2 -> o1.hashCode() - o2.hashCode()
}
val strings = listOf("abc", "cba")
strings.sortedWith(anyComparartor) // String 타입에 Any 타입을 넣어도 안전하다
- 반공변성은 공변의 반대로 타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힐 때를 말한다.
in
키워드를 통해 타입을 반공변적으로 선언할 수 있다.in
으로 선언한 타입 파라미터는 소비만 가능하다.- 자바의
<? super T>
와 같다
9.3.5 사용 지점 변성: 타입이 언급되는 지점에서 변성 지정
- 선언 지점 변성: 클래스 선언하면서 지점에서 변성을 지정
- 사용 지점 변성: 사용하는 곳에서 변성 지정
fun <T> copyData(source: MutableList<out T>, destination: MutableList<in T>) {
for (item in source) {
destination.add(item)
}
}
- source의 원소를 destination으로 옮기는 함수이다.
- source의 원소는 읽기만, destination은 쓰기만 할것이므로 사용 제한을 두어도 된다.
source
는 for-in절에서 타입 인자의Iterator<T>
를 생산한다. 또한 source에서 T 타입을 소비하지 못하도록out
으로 변성을 지정한다destination
는add
함수를 통해T
타입 값을 소비한다. 또한 복사하는 하한 타입 이T
이며 인자의 상위 타입에 대해서도 대상 리스트 원소 타입으로 허용하기 위해in
으로 변성을 지정한다(예 source → MutableList, destination → MutableList)
9.3.6 스타 프로젝션: 타입 인자 대신 *
사용
val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
val chars = mutableListOf('a', 'b', 'c')
val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
unknownElements.add(42) // Error: Out-projected type 'MutableList<*>' prohibits the use of 'fun add(element: E): Boolean'
println(unknownElements.first()) //OK
- 여기서
MutableList<*>
는MutableList<out Any?>
처럼 동작한다. 타입을 모르는 리스트에 원소를 마음대로 넣을 수 없기 때문 - 하지만
MutableList<*>
와MutableList<Any?>
는 서로 다른 의미를 갖는다.MutableList<*>
: 구체적인 타입의 원소만을 담는 리스트지만, 그 원소의 타입을 정확히 모른다는 의미MutableList<Any?>
: 모든 타입의 원소를 담을 수 있다는 의미
- 자바의
?
에 대응된다.
반응형