티스토리 뷰
728x90
1강 제네릭과 타입 파라미터
Cage 클래스
- 동물을 넣거나 꺼낼 수 있음
- methods
- getFirst() - 첫번째 동물을 가져옴
- put(animal) - 동물을 넣는다.
- moveFrom(cage) - 다른 cage에 있는 동물을 모두 가져온다.
Code
class Cage {
private val animals: MutableList<Animal> = mutableListOf()
fun getFirst(): Animal {
return this.animals.first()
}
fun put(animal: Animal) {
this.animals.add(animal)
}
fun moveFrom(cage: Cage) {
this.animals.addAll(cage.animals)
}
Animal 클래스
- 동물을 형상화한 클래스
Code
abstract class Animal (
val name: String
)
abstract class Fish(name: String) : Animal(name)
// 금붕어
class GoldFish(name: String) : Fish(name)
// 잉어
class Carp(name: String) : Fish(name
UML
@startuml
abstract class Animal {
- name: String
}
abstract class Fish extends Animal
class GoldFish extends Fish
class Carp extends Fish
@enduml
Carp(잉어)를 Cage에 넣고 뺴기
fun main() {
val cage = Cage()
cage.put(Carp("잉어"))
// 에러 발생!!
// getFirst()는 Animal을 return
val carp: Carp = cage.getFirst()
}
- 위 에러를 해결하는 가장 간단한 방법은
as
를 사용해 type casting을 하는것
fun main() {
val cage = Cage()
cage.put(Carp("잉어"))
val carp: Carp = cage.getFirst() as Carp
}
- BUT!! 위와같이 하는것은 위험함! Cage 내부에는 Carp만 들어있다는 보장이 없음
- 아래와 같은 에러는 런타임이 되어야 찾을 수 있음
fun main() {
val cage = Cage()
cage.put(GoldFish("금붕어")) // gold fish
val carp: Carp = cage.getFirst() as Carp // type cast error 발생!
}
- Safe Type Castring과 Elvis Operator
- 아래와 같은 방법을 사용할 수 있으니 IllegalArgumentException가 발생함
fun main() {
val cage = Cage()
cage.put(GoldFish("금붕어"))
val carp: Carp = cage.getFirst() as? Carp ?: throw IllegalArgumentException()
}
- 동일한 Cage 클래스이지만 잉어만 넣을 수 있는 Cage, 금붕어만 넣을 수 있는 Cage를 구분하면 어떨까?!
제너릭(Generic)
- 클래스 뒤에 타입 파라미터를 추가할때는 클래스 뒤에 <타입>을 적어주면 됨
Generic을 적용한 Cage 클래스
class CageWithTypeParam<T> {
private val animals: MutableList<T> = mutableListOf()
fun getFirst(): T {
return this.animals.first()
}
fun put(animal: T) {
this.animals.add(animal)
}
fun moveFrom(cage: CageWithTypeParam<T>) {
this.animals.addAll(cage.animals)
}
}
fun main() {
val cage = CageWithTypeParam<Carp>()
cage.put(Carp("잉어"))
// cage.put(GoldFish("금붕어")) // 금붕어는 넣을 수 없음
val carp: Carp = cage.getFirst() // 타입 캐스팅이 필요 없음
}
금붕어 Cage에 금붕어를 한 마리 넣고, 물고기 Cage에 금붕어를 옮기자! (moveForm 메소드 사용)
- 금붕어는 물고기의 하위 타입이기 때문에
fishCage
로 넣을수 있을것 같으나 에러가 발생함
fun main() {
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = CageWithTypeParam<Fish>()
fishCage.moveFrom(goldFishCage) // type mismatch error!!
}
2강 배열과 리스트, 제네릭과 무공변
상위 타입과 하위 타입의 의미
@startuml
class Number
class Int
Number <|-- Int
@enduml
- 함수 매개 변수에 대신 들어갈 수 있음
fun doSomething(num: Number) { ... }
val a: Int = 3
doSomething(a) // Int는 Number의 하위타입으로 가능!
2. 변수에 대신 들어갈 수 있음
```kotlin
val intNum: Int = 5
// 상위 타입 num에 하위 타입 intNum이 들어갔음
va num: Number = intNum
상속관계란 상위 타입이 들어가는 자리에 하위타입이 대신 위치 할 수 있음
이전코드 다시보기
fun main() {
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = CageWithTypeParam<Fish>()
fishCage.moveFrom(goldFishCage) // type mismatch error!!
}
CageWithTypeParam<Fish>
에CageWithTypeParam<GoldFish>
를 넣으려 하고 있음
CageWithTypeParam<Fish>
와 CageWithTypeParam<GoldFish>
관계
@startuml
agent "CageWithTypeParam<Goldfish>"
agent "CageWithTypeParam<Fish>"
@enduml
CageWithTypeParam<Fish>
와CageWithTypeParam<GoldFish>
는 아무 관계도 아님!- Cage2는 무공변(불공변) 하다!
무공변
- 왜 Fish와 Goldfish간의 상속 관계가 제네릭 클래스에서 유지되지 않을까?
JAVA의 배열과 리스트 비교
배열
- JAVA의 배열은 A객체가 B객체의 하위타입이면 A배열이 B배열의 하위타입으로 간주됨
- JAVA의 배열은 공변함!
@startuml agent "Object" agent "String" agent "Object[]" agent "String[]"
Object <-- String
"Object[]" <-- "String[]"
@enduml
- 자바의 배열은 **공변**하므로 아래와같은 코드가 가능함
```java
String[] str = new String[]{"a", "b", "c"};
Object[] objects = strs;
objects[0] = 1 // 여기서 문제 발생!! java.lang.ArrayStoreException: java.leng.Integer
- objects는 사실 String[]이기때문에 int를 넣을 수 없음, 때문에 런타임 에러가 발생!
- 타입-안전 하지 않은 코드가 발생함
List With Generic
- List는 무공변하므로 아래와 같은 코드는 원천적으로 불가능함!
- 배열보다 Type-Safe하게 코드를 작성 할 수 있음
List<String> strs = List.of("a", "b", "b"); List<Object> objs = strs; // Type Mismatch!
다시 코틀린 코드로 돌아와 생각해보기
fun main() {
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = CageWithTypeParam<Fish>()
fishCage.moveFrom(goldFishCage) // type mismatch error!!
}
- 위 코드를 동작하게는 못할까?
- CageWithTypeParam
와
CageWithTypeParam`의 상속 관계를 유지하게 된다면 위 코드는 동작이 가능!
3강. 공변과 반공변
- 공변: 클래스의 상속관계가 제너릭 클래스의 상속관계까지 이어지는 것
- 코틀린에서는 제너릭 앞에
out
키워드를 붙이면 공변하게 변함
// moveFrom 타입 파라미터 앞에 `out` 키워드 추가
fun moveFrom(cage: CageWithTypeParam<out T>) {
this.animals.addAll(cage.animals)
}
fun main() {
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = CageWithTypeParam<Fish>()
fishCage.moveFrom(goldFishCage) // 에러 사라짐
}
out을 타입 파라미터에 붙이면
out
을 붙이게 되면 데이터를 꺼낼수만 있음 (생산자 역할만 가능)
fun moveFrom(otherCage: CageWithTypeParam<out T>) {
otherCage.getFirst() // 성공
otherCage.put(this.getFirst()) // 에러발생
this.animals.addAll(otherCage.animals)
}
out 키워드를 붙이면 왜 꺼낼수만 있을까?
otherCage가 소비자 역할도 할수 있다면??
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = CageWithTypeParam<Fish>()
fishCage.put(Carp("잉어"))
fishCage.moveFrom(goldFishCage)
fun moveFrom(otherCage: CageWithTypeParam<out T>) {
otherCage.put(this.getFirst()) // 만약 가능하다고 하면...
}
otherCage.put(this.getFirst())
가 가능하다면- otherCage는 CageWithTypeParam이고 this는
CageWithTypeParam<Fish>
이지만 안에는 Carp("잉어")가 들어있음 - 즉, CageWithTypeParam에 Carp를 넣는 형식이 되어버리고 그러면 타입 안정성이 깨져버림
- otherCage는 CageWithTypeParam이고 this는
moveTo 함수
- moveFrom함수와 반대로 내가 가지고 있는걸 다 넘겨주는
moveTo
함수를 만들어보자
fun moveTo(otherCage: CageWithTypeParam<T>) {
otherCage.animals.addAll(this.animals)
}
- 금붕어 케이지의 모든 금붕어를 Fish 케이지로 옮겨보자!
val fishCage = CageWithTypeParam<Fish>()
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
goldFishCage.moveTo(fishCage) // 에러 발생!!
- moveTo(otherCaget: CageWithTypeParam) 함수에 CageWithTypeParam를 넣고싶다!
- 즉 CageWithTypeParam가 상위, CageWithTypeParam가 하위 타입! (반공변)
in 키워드를 붙여 반공변으로..
- kotlin에서는
in
키워드를 붙여 반공변으로 만들수 있음 in
이 붙은otherCage
는 데이터를 받을수만 있다!
fun moveTo(otherCage: CageWithTypeParam<in T>) {
// otherCage는 소비자(데이트를 받는)역할 만 할수 있음
otherCage.animals.addAll(this.animals)
}
val fishCage = CageWithTypeParam<Fish>()
val goldFishCage = CageWithTypeParam<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
goldFishCage.moveTo(fishCage) // 정상 동작!!
정리
- out: (함수 파라미터 입장에서) 생산자, 공변
- 타입 파라미터의 상속관계가 제네릭 클래스에서 유지됨
- in: (함수 파라미터 입장에서) 소비자, 반공변
- 타입 파라미터의 상속관계가 제네릭 클래스에서 반대로 유지됨
4강. 선언 지점 변성 / 사용 지점 변성
in
,out
을 통해 공변, 반공변을 만들어 주었음
out
을 이용해 상위타입 변수에 하위타입 변수 넣기
val goldFishCage: CageWithTypeParam<GoldFish> = CageWithTypeParam()
val fishCage: CageWithTypeParam<out Fish> = goldFishCage
제너릭 클랙스 자체를 공변하게 만들수 없나?
- 아래 코드에서
out
키워드를 빼고 쓸수는 없나? - 코틀린에서는 가능!
val goldFishCage: CageWithTypeParam<GoldFish> = CageWithTypeParam()
val fishCage: CageWithTypeParam<out Fish> = goldFishCage
CageWithTypeParam을 개조해보자!
생산만 하는 CageWithTypeParam (out)
CageWithTypeParam2
는 오직 2가지 기능만 가지고있음getFirst(): T
/getAll(): List<T>
CageWithTypeParam2
는 하나 또는 모든 데이터를 반환하는 역할만 함 (생산만 하는 클래스)- 타입 파라미터(
T
)가 반환타입에서만 사용됨
- 타입 파라미터(
class CageWithTypeParam2<T> {
private val animals: MutableList<T> = mutableListOf()
fun getFirst(): T {
return this.animals.first()
}
fun getAll(): List<T> {
return this.animals
}
val fishCage: CageWithTypeParam2<Fish> = CageWithTypeParam2()
val animalCage: CageWithTypeParam2<Animal> = fishCage // 에러 발생 (Type Mismatch)
- 위의코드는 아직 에러가 발생하지만 논리상으로는 문제가 없는코드 (animalCage는 Animal을 생산만하기 때문!!)
- fishCage (잉어, 금붕어) -> animalCage (잉어, 금붕어)
- animalCage의 겉은
CageWithTypeParam2<Animal>
이지만 실제는CageWithTypeParam2<Fish>
- 잉어나 금붕어를 Animal로 가져오는건 전혀 문제가 없음!!
- animalCage의 겉은
- 반대로, animalCage (잉어, 금붕어)가 소비소 하게 된다면?
animalCage (잉어, 금붕어).put(참새)
animalCage
는 겉으로는CageWithTypeParam2<Animal>
이기때문에 참새를 넣는것은 문제없어 보이나 실제로는CageWithTypeParam2<Fish>
이기때문에 RuntimeError 발생!
- fishCage (잉어, 금붕어) -> animalCage (잉어, 금붕어)
- 클래스 자체에
out
붙여주면 클래스를 공변하게 만들수 있음 T
는 생산만 가능하고 소비를 위한T
는 사용하지 못함class CageWithTypeParam2<out T> { private val animals: MutableList<T> = mutableListOf() fun getFirst(): T { return this.animals.first() } fun getAll(): List<T> { return this.animals } }
val fishCage: CageWithTypeParam2 = CageWithTypeParam2()
val animalCage: CageWithTypeParam2 = fishCage // 정상 동작!
// animalCage.put(참새) --> out은 소비가 불가능하기때문에 이러한 동작은 불가함
#### 소비만 하는 CageWithTypeParam (in)
- `CageWithTypeParam3`의 메서드는 모두 소비만 함
```kotlin
class CageWithTypeParam3<in T> {
private val animals: MutableList<T> = mutableListOf()
fun put(animal: T) {
this.animals.add(animal)
}
fun putAll(animal: List<T>) {
this.animals.addAll(animal)
}
}
라이브러리를 통한 예시
in
선언지점 예시
Comparable
의 경우 파라미터를 소비만 함
public interface Comparable<in T> {
public operator fun compareTo(other: T): Int
}
out
선언지점 예시
- 코틀린의
List
는 불변이기 때문에 데이터를 생산만 함
public interface List<out E> : Collection<E> {
...
}
- 그런데
contains()
또는containsAll()
의 경우는 타입파라미터E
를 받아야함 - 타입 안전하다고 보장할수 있는경우
@UnsafeVariance
를 사용해서 데이터를 소비할 수 있음public interface List<out E> : Collection<E> { override fun contains(element: @UnsafeVariance E): Boolean }
5강. 제네릭 제약과 제네릭 함수
Generic 제약
- 우리는 CageWithTypeParam에 Animal만 사용하고 싶다!
- 타입 파라미터에는 어떠한 클래스라도 들어올 수 있음
<T : Animal>
을 사용하면 타입 파라미터의 상한(upper bound)을 Animal로 지정 가능- 위
T
는 Animal의 하위클래스만 들어올 수 있음 class CageWithTypeParam4<T : Animal> { private val animals: MutableList<T> = mutableListOf() fun getFirst(): T { return this.animals.first() } fun put(animal: T) { this.animals.add(animal) } fun moveFrom(otherCage: CageWithTypeParam4<T>) { this.animals.addAll(otherCage.animals) } }
- 위
val a = CageWithTypeParam4() // 불가
val b = CageWithTypeParam4() // 불가
val c = CageWithTypeParam4() // 가능!
val d = CageWithTypeParam4() // 가능!
### 제한 조건을 여러개로 하고싶다면?
- 예시) T에 Animal만 들어올수 있고 Comaprable을 구현하고 있어야함
```kotlin
// 제약이 여러개 일때는 where을 사용함
class CageWithTypeParam4<T> where T : Animal, T : Comparable<T> {
private val animals: MutableList<T> = mutableListOf()
fun printAfterSorting() {
// Comparable을 구현한 Animal만 받기 떄문에 .sorted()를 바로 사용가능
this.animals.sorted()
.map { it.name }
.let { println(it) }
}
}
제너릭 제약을 Non-Null 타입 한정에 사용 가능
T
만 사용할 경우 Nullable한 타입도 넣을 수 있음class CageWithTypeParam6<T> { private val animals: MutableList<T> = mutableListOf() }
CageWithTypeParam<GoldFish?>() // 가능
- Non-Null 제약을 걸고 싶다면 `Any`를 설정함
```kotlin
class CageWithTypeParam6<T : Any> {
private val animals: MutableList<T> = mutableListOf()
}
CageWithTypeParam<GoldFish?>() // 불가능
제네릭 함수를 사용하면 유연한 코딩이 가능
- 예시) 두 리스트에 겹치는 원소가 하나라도 있는지 확인하는 함수
fun List<String>.hasIntersection(other: List<String>): Boolean {
return (this.toSet() intersect other.toSet()).isNotEmpty()
}
fun List<Int>.hasIntersection(other: List<Int>): Boolean {
return (this.toSet() intersect other.toSet()).isNotEmpty()
}
// 위와같이 유연하지 않은 함수를 `T`를 통해 유연하게 만들어 줄수 있음
fun <T> List<T>.hasIntersection(other: List<T>): Boolean {
return (this.toSet() intersect other.toSet()).isNotEmpty()
}
6강. 타입 소거와 Star Projection
타입 소거
- 제네릭이란 개념은 JDK초기 버전에는 존재하지 않았음 (java 1.5 부터 나옴)
- 여기서 문제점은 제네릭이 없는 버전과 있는버전의 호환성을 유지해야함
- **런타임때는 타입정보
<T>
를 제거함 (List<String> -> List
)
- **런타임때는 타입정보
- 반면, 코틀린의 경우는 초기부터 제네릭이 고려되어
raw type
객체를 만들 수 없음
val number: List = listOf(1, 2, 3) // ERROR! 타입 파라미터를 꼭 명시해야함
- 하지만 코틀린도 JVM위에서 동작하기에 런타임때는 타입 정보가 사라짐 (타입 소거)
타입 소거를 확인 하는 방법
- 아래 코드는 런타임때 String 정보가 사라지기에 List인지 알수 없음
fun checkStringList(data: Any) { // Cannot check for instance of erased type: List<String> if (data is List<String>) { // .... } }
start project
- start project을 통해 최소한 List인지는 확인 가능함
fun checkStringList(data: Any) { // data가 List인지는 확인 가능 if (data is List<*>) { // .... } }
- data가 List 타입이니 데이터를 가져올수는 있으나 타입은 알수 없어
Any?
로 반환됨 - 데이터를 넣을때는 어떤 타입인지 알수 없어 함부로 넣을 수 없음
fun checkList(data: Any) { if (data is List<*>) { val ele: Any? = data[0] // 가능 data.add(3) // 불가능 } }
- 제너릭 함수에서도 타입정보는 사라짐
fun <T> T.toSuperString() { val className = T::class.java.name // T는 제거되므로 클래스명을 가져올 수 없음 }
reified 키워드 + inline 함수
- 위 문제는 reified 키워드 + inline 함수로 해결할 수 있음
fun <T> List<*>.hasAnyInstance(): Boolean { return this.any { it is T } // T의 내용은 소거되기 때문에 현재 에러가 발생함 }
- inline: 함수의 본문이 함수 호출 부분에 복사/붙여넣기 됨
- 위처럼 함수 본문이 호출 부분에 들어가기 때문에
T
자체가 본문으로 옮겨져 타입파라미터로 간주되지 않음inline fun <reified T> List<*>.hasAnyInstance(): Boolean { return this.any { it is T } // 가능 }
- 위처럼 함수 본문이 호출 부분에 들어가기 때문에
reified 한계
reified
가 붙은T
는 인스턴스를 만들거나 comapanion object를 가져올 수 없음
7강. 제네릭 용어 정리 및 간단한 팁
- Generic 타입
- Raw 타입
- 변성: 제네릭 틀래스의 타입 파라미터에 따라 제네릭 클래스간의 상속관계가 어떻게 되는지 나타내는 용어
- 무공변: 타입 파라미터끼리 상속 관계라도 제네릭 클래스간에는 상속관계가 없다는 의미
- 제네릭 클래스는 기본적으로 무공변함
- 공변: 타입 파라미터끼리 상속 관계가 제네릭 클래스에도 동일하게 유지됨 (
out
) - 반공변: 타입 파라미터끼리 상속 관계가 제네릭 클래스에서는 반대로 유지됨 (
in
)
- 무공변: 타입 파라미터끼리 상속 관계라도 제네릭 클래스간에는 상속관계가 없다는 의미
- 선언 지점 변성: 클래스 자체를 공변/반공변하게 만드는 방법
- 사용 지정 변성: 특정 함수/변수에 공변/반공변을 만드는 방법
- 제네릭 제약: 제네릭 클래스의 타입 파라미터에 제약을 거는 방법
- 타입소거: JDK호환성을 위해 런타임때 제네릭 클래스의 타입 파라미터 정보가 지워지는 것
- kotlin에서는 inline + reified를 통해 타입소거를 일부 막을 수 있음
- star projection: 어떤 타입이든 들어갈 수 있음 (
List<*>
)
타입 파라미터 쉐도잉
- 똑같은
T
지만 클래스에서의T
와 함수의T
는 다른것으로 간주 됨 (like 지역, 전역 변수)class CageShadow<T: Animal> { fun <T: Animal> addAnimal(animal: T) { // 함수에서 사용하는 `T`와 클래스의 `T`를 가리게 됨 } }
val cage = CageShadow()
cage.addAnimal(GoldFish("금붕어"))
cage.addAnimal(Carp("잉어")) // 클래스 T가 함수 T에 가려져 Animal 하위 속성은 모드 들어가 버림
## 제네릭 클래스 상속
```kotlin
open class CageV1<T : Animal> { }
class CageV2<T : Animal> : CageV1<T>() {} // 같은 제약의 T를 넘겨줌
class CageV3 : CageV1<GoldFish>() {} // T 제약에 맞는 클래스를 직접 넣어줌
제네릭과 Type Alias
- 제네릭을 사용하면 클래스명이 길어지니 Type Alias를 사용하여 축약해 사용할 수 있음
위 내용은 인프런 코틀린 고급편강의를 시청하고 작성했습니다
인프런
728x90
'개발 언어 > 코틀린' 카테고리의 다른 글
인프런 - 코틀린 고급편 (3) 함수형 프로그래밍 활용 (0) | 2024.07.04 |
---|---|
인프런 - 코틀린 고급편 (2) 지연과 위임 (1) | 2024.07.01 |
코틀린 & Spring Boot 2.1 (0) | 2019.06.27 |
코틀린(Kotlin) | 람다(Lambda) (0) | 2019.06.24 |
코틀린(Kotlin) | 함수 (0) | 2019.06.24 |
댓글