[Kotlin in Action] 9장 제네릭스

Sdoubleu·2023년 4월 19일
0

Kotlin in Action

목록 보기
8/9
post-thumbnail

9장에서 다루는 내용

  1. 제네릭 함수와 클래스를 정의하는 방법
  2. 타입 소거와 실체화한 타입 파라미터
  3. 선언 지점과 사용 지점 변성
  • 실체화한 타입 파라미터를 사용하면 인라인 함수 호출에서 타입 인자로 쓰인 구체적인 타입을 실행 시점에 알 수 있다

  • 선언 지점 변성을 사용하면
    기저 타입은 같지만 타입 인자가 다른 두 제네릭 타입 Type<A>Type<B>가 있을 때 타입 인자 A/B의 상위/하위 타입 관계에 따라 두 제네릭 타입의 상위/하위 타입 관계가 어떻게 되는지 지정할 수 있다
    -> Type은 기저타입, A,B는 타입 인자

ex) List<Any>를 인자로 받는 함수에게 List<Int>타입의 값을 전달할 수 있을지 여부를 선언 지점 변성을 통해 지정할 수 있다

  • A가 B의 상위 타입(클래스)라면 A 타입의 인스턴스에 대해 성립하는 모든 규칙은
    B타입의 인스턴스에 대해서도 성립해야 한다

  • 사용 지점 변성(use-site variance)
    같은 목표(제네릭 타입 값 사이의 상하위 타입 관계 지정)를 제네릭 타입 값을
    사용하는 위치에서 파라미터 타입에 대한 제약을 표시하는 방식으로 달성한다


9.1 제네릭 타입 파라미터

  • 제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있다
    제네릭 타입의 인스턴스를 만들려면 타입 파라미터를 구체적인 타입 인자로 치환해야 한다

ex) Map 클래스는 키 타입과 값 타입을 타입 파라미터로 받으므로 Map<K, V>가 된다
인스턴스화를 만들려면 구체적으로 Map<String, Person>이라고 타입 파라미터를 타입 인자로 치환(구체적으로 명시) 해야 한다

9.1.1 제네릭 함수와 프로퍼티

리스트를 만들 때 특정 타입을 저장하는 리스트뿐 아니라 모든 리스트를 다룰 수 있는 함수를 원한다면 제네릭 함수를 작성해야 한다
-> 제네릭 함수를 호출할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야 한다

컬렉션을 다루는 라이브러리 함수는 대부분 제네릭 함수이다

// 제네릭 함수 호출하기
>>> val letters = ('a'..'z').toList()
>>> println(letters.slice<Char>(0..2)) <- 타입 인자를 명시적으로 지정
[a, b, c]
>>> println(letters.slice(0..2)) <- 컴파일러가 T는 Char이라고 추론
[a, b, c]


// 제네릭 고차 함수 호출
val authors = listOf("Kim", "Park")
val readers = mutableListOf<String>( ... )
fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>

>>> readers.filter { it !in authors }

람다 파라미터에 대해 자동으로 만들어진 변수 it의 타입은 T라는 제네릭 타입이다

컴파일러는 filter가 수신 객체인 reader의 타입이 List<String>이라는 사실을 알고 T가 String이라는 사실을 추론한다

  • 클래스/인터페이스 안에 정의된 메소드, 확장 함수 또는 최상위 함수에서
    타입 파라미터를 선언할 수 있다

  • 확장 함수에서는 수신 객체나 파라미터 타입에 대한 타입 파라미터를 사용할 수 있다

  • 제네릭 함수를 정의할 때와 마찬가지 방법으로
    제네릭 확장 프로퍼티를 선언할 수 있다

val <T> List<T>.penultimate: T <- "모든" 리스트 타입에 이 제네릭 확장 프로퍼티를 사용할 수 있다
	get() = this[size -2]

>>> println(listOf(1,2,3,4).penultimate) <- 타입 파라미터 T는 Int로 추론
3

확장 프로퍼티만 제네릭하게 만들 수 있다
일반 프로퍼티는 타입 파라미터를 가질 수 없다

9.1.2 제네릭 클래스 선언

  • 자바와 마찬가지로
    코틀린에서도 타입 파라미터를 넣은 꺽쇠 기호<>를 클래스/인터페이스 이름 뒤에 붙이면 클래스/인터페이스를 제네릭하게 만들 수 있다

  • 타입 파라미터를 이름 뒤에 붙이고 나면 클래스 본문 안에서 타입 파라미터를 다른 일반 타입처럼 사용할 수 있다

interface List<T> { <- List 인터페이스에 T라는 타입 파라미터를 정의한다
	operator fun get(index: Int): T <- 인터페이스 안에서 T를 일반 타입처럼 사용할 수 있다
}
  • 제네릭 클래스를 확장하는 클래스 or 제네릭 인터페이스를 구현하는 클래스를
    정의하려면 기반 타입의 제네릭 파라미터에 대해 타입 인자를 지정해야 한다
class StringList: List<String> { <- 이 클래스는 구체적인 타입 인자로 String을 지정해 List를 구현한다
	override fun get(index: Int): String = ... 
}

class ArrayList<T>: List<T> { 
	override fun get(index: Int): T = ... <- ArrayList의 제네릭 타입 파라미터 T를 List의 타입 인자로 넘긴다
}

---------------------------------------------------------------

ArrayList의 T랑 List의 T는 다르다
둘은 전혀 다른 타입 파라미터이다

class ArrayList<generic>: List<T> { 
	override fun get(index: Int): T = ...}
라고 써도 무방하다
  • 하위 클래스에서 상위 클래스에 정의된 함수를 오버라이드하거나 사용하려면
    타입 인자 T를 구체적 타입 String으로 치환해야 한다
    -> fun get(index: Int): String

  • 클래스가 자기 자신을 타입 인자로 참조할 수 있다

interface Comparable<T> {
	fun compareTo(other: T):Int
}

class String: Comparable<String>{
	override fun compareTo(other: String): Int = ...
}

↪ String 클래스는 제네릭 Comparable 인터페이스를 구현하면서
그 인터페이스의 타입 파라미터 T로 String 자신을 지정한다

9.1.3 타입 파라미터 제약

  • 타입 파라미터 제약
    클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능

ex) sum 함수를 예를 들면
List<Int> , List<Double>에는 함수를 적용할 수 있지만
List<String> 등에는 적용할 수 없다

  • 제약을 가하려면 타입 파라미터 이름 뒤에 콜론(:)을 표시하고 그 뒤에 상한 타입을 적으면 된다

-> 타입 파라미터 T에 대한 상한(Upper bound)을 정하고 나면 T 타입의 값을
그 상한 타입의 값으로 취급할 수 있다

  • 드물지만 타입 파라미터에 대해 둘 이상의 제약을 가하는 경우
fun <T> ensureTrailingPeriod(seq: T)
	where T: ChareSequence, T: Appendable {
    ...
    }

9.1.4 타입 파라미터를 널이 될 수 없는 타입으로 한정

  • 상한을 정하지 않은 타입 파라미터는 결과적으로 Any?를 상한으로 정한 파라미터와 같다
class Processor<T> {
	fun process(value: T) {
    	value?.hashCode() <- value는 널이 될 수 있다, 안전한 호출을 사용해야 한다
    }
}

val nullableStringProcessor = Processor<String?>() 
↪ 널이 될 수 있는 타입인 string?이 T를 대신한다

nullableStringProcessor.process(null)
  • 널 가능성을 제외한 아무런 제약도 필요없다면 Any? 대신 Any를 상한으로 사용
class Processor<T: Any> {
	fun process(value: T) {
    	value?.hashCode() <- T 타입의 value는 null이 될 수 없다
    }
}

9.2 실행 시 제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터

  • JVM의 제네릭스는 보통 타입 소거를 사용해서 구현
    -> 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다

  • 함수를 inline으로 만들면 타입 인자가 지워지지 않게 할 수 있다
    -> 실체화(reify)라고도 부른다

9.2.1 실행 시점의 제네릭: 타입 검사와 캐스트

  • 코틀린 제네릭 타입 인자 정보는 런타임에 지워진다
    -> 제니릭 클래스 인스턴스가 생성할 때 쓰인 타입 인자에 대한 정보를 유지X
    ex) List<String> 객체를 만들고 그 안에 문자열을 넣어도 실행 시점엔
    그 객체를 List로만 볼 수 있다

  • 타입 소거로 생기는 한계
    -> 실행 시점에 타입 인자를 검사할 수 없음

실행 시점에 어떤 값이 List인지는 알 수 있으나 String에 대한 List인지 Person에 대한 List인지 알 수 없음

  • 제네릭 타입 소거의 소소한 장점
    저장해야 하는 타입 정보의 크기는 줄어들어서 메모리 사용량이 줄어듦

  • 타입 파라미터가 2개 이상이라면 모든 타입 파라미터에 스타 프로젝션(*) 를 포함시켜야 한다
    ex) if(value is List<*> { ... }

🛠️9.2.2 실체화환 타입 파라미터를 사용한 함수 선언

  • 인라인 함수의 타입 파라미터는 실체화되므로
    실행 시점에 인라인 함수의 타입 인자를 알 수 있다

  • 함수가 커지면 실체화한 타입에 의존하지 않는 부분을 별도의 일반 함수로
    뽑는 것이 낫다

reified를 사용하는 이유와 예시

9.2.3 실체화한 타입 파라미터로 클래스 참조 대신

9.2.4 실체화한 타입 파라미터의 제약

  • 실체화한 타입 파라미터를 사용할 수 있는 경우
  1. 타입 검사와 캐스팅(is, !is, as, as?)
  2. 10장에서 설명할 코틀린 리플렉션 API(::class)
  3. 코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)
  4. 다른 함수를 호출할 대 타입 인자로 사용
  • 사용할 수 없는 경우
  1. 타입 파라미터 클래스의 인스턴스 생성하기
  2. 타입 파라미터 클래스의 동반 객체 메소드 호출하기
  3. 실체화한 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
  4. 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기

9.3 변성: 제네릭과 하위 타입

  • 변성(variance)
    List<String>List<Any>와 같이 기저 타입이 같고
    타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념

9.3.1 변성이 있는 이유: 인자를 함수에 넘기기

  • Any 클래스는 String 클래스를 확장한 것이므로
    Any 타입 값을 파라미터로 받는 함수에 String 값을 넘겨도 절대로 안전하지만

List 인터페이스의 타입 인자로 들어가는 경우 안정성을 말할 수 없다

fun addAnswer(list: MutableList<Any>) {
	list.add(42)
}

val strings = mutableListOf("abc","def")
addAnswer(strings) <- 이 줄이 컴파일 된다면
println(string.maxBy{ it.length }) <- 실행 시점에 예외 발생
>>> ClassCastException: Integer cannot be cast to String
  • 코틀린에서는 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전하지 못한 함수 호출을 막을 수 있다

함수가 읽기 전용 리스트라면 더 구체적인 타입의 원소를 갖는 리스트를 그 함수에 넘길 수 있다
-> 하지만! 리스트가 변경 가능하다면 그럴 수 없다

9.3.2 클래스, 타입, 하위 타입

  • 타입 사이의 관계를 논하기 위해 하위 타입상위 타입 개념을 알아야 한다

  • 하위 타입
    어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 문제가 없다면
    타입 B는 A의 하위 타입
    -> 상위 타입은 그 반대개념

  • 🔥한 타입이 다른 타입의 하위 타입인지가 왜 중요하지?
    -> 컴파일러는 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행하기때문에!

  • 널이 될 수 있는 타입은 하위 타입과 하위 클래스가 같지 않다

  • 널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입이다
    -> 하지만, 두 타입 모두 같은 클래스

제네릭 타입을 인스턴스화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변이라고 말한다

A가 B의 하위 타입이면
List<A>List<B>의 하위 타입이다
-> 그런 클래스나 인터페이스를 공변적(covariant)이라고 말한다

9.3.3 공변성: 하위 타입 관계를 유지

  • 코틀린에서 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면
    타입 파라미터 이름 앞에 out을 넣어야 한다
interface Producer<out T> {
	fun produce(): T
}

ex)

open class Animal {
	fun feed() { ... }
}

class Herd<T: Animal> {	 <- 타입 파라미터를 "무공변성"으로 지정
	...
}

fun feedAll(animals: Herd<Animal>) {
	...
}

class Cat: Animal() {
	...
}

fun takeCareOfCats(cats: Herd<Cat>) {
	feedAll(cats) <- 오류 발생
}

Herd 클래스의 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 때문에
고양이 무리는 동물 무리의 하위 클래스가 아니다
-> 타입 캐스팅을할 수도 있지만 그런 식으로 처리한다면 코드가 장황해지고 실수 발생
강제 캐스팅은 올바른 방법 ❌

// 올바른 예시
class Herd<out T: Animal> { <- T는 공변적이다
	...
}

fun takeCareOfCats(cats: Herd<Cat> {
	feedAll(cats) <- 캐스팅할 필요 없다
}
  • 모든 클래스를 공변적으로 만들 수는 없다
    -> 안전하지 못한 클래스도 있기 때문에

T가 함수의 반환 타입에 쓰인다면 T는 아웃 위치 -> 값을 생산
T가 함수의 파라미터 타입에 쓰인다면 T는 인 위치 -> 값을 소비

타입 파라미터 T에 붙은 out 키워드는
1. 공변성
하위 타입 관계가 유지
2. 사용 제한
T를 아웃 위치에서만 사용할 수 있다 -> 타입 안정성 보장

9.3.4 반공변성: 뒤집힌 하위 타입 관계

  • 반공변 클래스의 하위 타입 관계는 공변 클래스의 경우와 반대이다

타입 B가 타입 A의 하위 타입인 경우
Consumer<A>Consumer<B>의 하위 타입인 관계가 성립하면
제네릭 클래스 Consumer<T>는 타입 인자 T에 대해 반공변이다

공변성반공변성무공변성
Producer<out T>Consumer<in T>MutableList<T>
타입 인자의 하위 타입 관계가 제네릭 타입에서도 유지 된다타입 인자의 하위 타입 관계가 제네릭 타입에서 뒤집힌다하위 타입 관계가 성립하지 않는다
Producer<Cat>Producer<Animal>의 하위 타입이다Consumer<Animal>Consumer<Cat>의 하위 타입이다
T를 아웃 위치에서만 사용할 수 있다T를 인 위치에서만 사용할 수 있다T를 아무 위치에서나 사용할 수 있다

9.3.5 사용 지점 변성: 타입이 언급되는 지점에서 변성 지정

  • 선언 지점 변성
    클래스를 선언하면서 변성을 지정하면 그 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치게 하는 방식

  • 사용 지점 변성
    자바에서 타입 파라미터가 있는 타입을 사용할 때마다 해당 타입 파라미터를 하위 타입이나 상위 타입 중 어떤 타입으로 대치할 수 있는지 명시해야 하는 방식

9.3.6 스타 프로젝션: 타입 인자 대신 * 사용


📌참고자료

profile
개발자희망자

0개의 댓글