제네릭을 이용하는 것은 런타임 오류를 컴파일 오류로 바꿀 수 있는 좋은 방법이다. 아래와 같이 Generic 을 이용하여 Collection 을 정의하면, String 이 아닌 타입의 원소가 추가될때 컴파일러가 오류를 인지할 것이다. 만약 Generic 으로 타입을 정의하지 않은 arrList2 의 케이스를 보면, 내부적으로 arrList2 는 Any 타입으로 정의 되어 있을 것이다.
fun main() {
val arrList: MutableList<String> = mutableListOf("str1", "str2", "str3")
arrList.add(Recipe("Soup")) // Type mismatch: inferred type is Recipe but String was expected
val arrList2 = mutableListOf(Recipe("Soup"), Recipe("KimChi"), Animal("Hippo"))
for (x in arrList) {
x.sayTitle() // Unresolved reference: sayTitle
}
}
data class Recipe (val title: String) {
fun sayTitle() {
println("this is $title recipe")
}
}
class Animal (val name: String) {
fun sayName() {
println("$name is Animal")
}
}
위에 예시로 들은 MutableList 가 어떤식으로 정의되어 있는지 보자, Generic 이 들어갈 자리에 E 라는 문자가 들어가 있고, 그 E 를 하위 메서드에서도 그대로 사용하는 모습이다. 그리고 List 인터페이스와 MutableCollection 을 상속하고 있다.
E 는 개발자가 MutableList 를 정의할때 사용할 실제 타입을 의미하고, 이 자료형이 add 메서드에까지 이어지므로, 개발자가 정의한 타입이 아니면 Type mismatch 오류가 나오는 것이다.
public interface MutableList<E> : List<E>, MutableCollection<E> {
override fun add(element: E): Boolean
//More code
}
그리고 이상을 보면 MutableList 는 Class 가 아닌 인터페이스이다. 우리가 mutableListOf() 함수를 사용하면 MutableList 인터페이스를 구현해서 반환하는 것이다. 그리고 이렇게 구현된 인터페이스에는 필요한 모든 함수와, 프로퍼티가 포함되어 있다.
public fun <T> mutableListOf(vararg elements: T): MutableList<T> = if (elements.size == 0) ArrayList() else ArrayList(ArrayAsCollection(elements, isVarargs = true))
Generic 임을 명시하는 문자에 대한 컨벤션이 있다.
일반적으로 'T'(for Type) 를, Collection 을 사용할때는 'E'(for Element) 를, Map 에 대해서는 'K'(Key) 와 'V'(Value) 를 이용한다.
이때, T : {someType} 을 이용해서 제네릭이 받을 수 있는 타입을 제한할 수 있다.
abstract class Pet (val name: String)
class Cat(name: String): Pet(name)
class Dog(name: String): Pet(name)
class Fish(name: String): Pet(name)
class Contest<T: Pet> {
private val scores: MutableMap<T, Int> = mutableMapOf()
fun addScore(t: T, score: Int = 0) {
if (score < 0)
return
if (scores[t] == null) {
scores[t] = score
} else {
scores[t] = scores.getValue(t) + score
}
}
fun getWinner(): MutableSet<T> {
val highScore = scores.values.maxOrNull()
if (highScore == null) {
println("no scores")
return mutableSetOf()
}
val winners: MutableSet<T> = mutableSetOf()
for ((t, score) in scores) {
if (score == highScore) winners.add(t)
}
return winners
}
}
fun main() {
val catContest = Contest<Cat>()
catContest.addScore(Cat("Ggomi"), 50)
catContest.addScore(Cat("Nabi"), 45)
val topCat = catContest.getWinner().first()
println(topCat.name)
}
null 도 담을 수 있는 generic
class M () { fun <T> myFun() : T? { return null } }
interface Retailer<T:Pet> {
fun sell(): T
}
class CatRetailer : Retailer<Cat> {
override fun sell(): Cat {
println("Sell Cat")
return Cat("")
}
}
class DogRetailer : Retailer<Dog> {
override fun sell(): Dog {
println("Sell Dog")
return Dog("")
}
}
fun main() {
val catRetailer1 = CatRetailer()
val petRetailer: Retailer<Pet> = CatRetailer() // this won't Compile, What about polymorphism?
}
위 예시에서 val petRetailer: Retailer<Pet> = CatRetailer()
이 당연히 작동해야 할것 같은데, 컴파일 되지 않는다. Retailer<Cat>, Retailer<Dog>
도 안되고, 오로지 Retailer<Pet>
만 가능하다. Generic 을 쓰면 다형성을 구현할 수 없는 걸까?
Generic supertype 이 subType 을 받을 수 있게 하기 위해서는 out 키워드를 앞에 써줘야한다. 위의 인터페이스에 out 키워드를 넣으면 컴파일 에러없이 프로그램이 잘 돌아간다.
out 키워드를 사용하면 해당하는 Generic 을 covariant 로 만든다. covariant 가 되면, supertype 자리에 subtype 을 넣을 수 있게 된다.
interface Retailer<out T:Pet> {
fun sell(): T
}
......
fun main() {
val catRetailer = CatRetailer()
val petRetailer: Retailer<Pet> = CatRetailer()
}
일반적으로 out 키워드는 Generic 을 return 하는 함수가 있는 class, interface 에서 사용하거나, Generic 타입의 val 변수가 있는 경우에 사용된다. 하지만 class, interface 가 Generic 을 함수 파라미터로 갖거나 var 프로퍼티의 타입으로 갖는 경우에는 사용할 수 없다.
여기서 왜 키워드가 out 인지 알 수 있다. out 키워드는 Generic 타입이 out 인 경우 (함수의 리턴타입, val는 read_only) 에만 사용할 수 있다.
수의사 클래스를 만들고 contest 에 프로퍼티로 넣자.
class Vet<T: Pet> {
fun treat(t: T) {
println("Threat Pet ${t.name}")
}
}
class Contest<T: Pet> (var vet: Vet<T>) { ... }
모든 pet 을 치료할 수 있는 수의사를 콘테스트에 배치하면 정말 좋지 않을까? 그런데 Type mismatch 컴파일 에러가 나온다. 상속체계상 Pet 하위에 Cat 이 있는 상황이다.그러면 이번에도 out 키워드가 필요한 걸까? 하지만 var 를 이용하는 Generic 타입의 프로퍼티가 있는 경우 out 키워드를 사용할 수 없다.
fun main() {
val petVet = Vet<Pet>()
val catVet = Vet<Cat>()
val dogVet = Vet<Dog>()
val catContest2 = Contest<Cat>(petVet) // Type mismatch
}
우리의 상황은 var Vet<Pet>
을 Contest<Cat>
의 프로퍼티로 만들고 싶은 것이다. 다른 말로 하자면, 우리는 Generic supertype 을 subtype 자리에 사용하고 싶은것이다. out 과는 완전 반대의 상황이다. 이때 Vet 클래스의 Generic 자리에 in 키워드를 이용하면 문제를 해결할 수 있다. in 키워드를 이용하면 Generic 의 type 을 contravariant
로 바꿔놓는다. ( out 은 covariant 이다 )
class Vet<in T: Pet> {
fun treat(t: T) {
println("Threat Pet ${t.name}")
}
}
fun main() {
val petVet = Vet<Pet>()
val catVet = Vet<Cat>()
val dogVet = Vet<Dog>()
val catContest2 = Contest<Cat>(petVet) // Type mismatch
}
보통 Generic 타입의 인자를 받는 함수가 있는 경우 in 이 사용된다. 하지만 out 이 유효한 경우에는 in 을 사용할 수 없다.
in 이 sueprtype 을 subtype 자리에 넣을 수 있도록 바꿔주는 건 알겠다. 하지만 모든 상황에서? 몇몇 상황에서만 그렇게 할 순 없을까?
class 나 interface 에 in 키워드를 사용하는 것은 global 하게 사용하게 된다. 하지만 local 로 사용하면 Contest<Cat>
에 Vet<T>
이 넘겨질 때에만 Vet<Pet>
이 Vet<Cat>
대신 사용될 수 있도록 할 수 있다. Vet 클래스에서 in 을 삭제하고, Contest 의 생성자에 in 을 추가하면 된다.
class Vet<T: Pet> { ... }
class Contest<T: Pet> (var vet: Vet<in T>) { ... }
Vet 클래스처럼 Generic type 이 in 이나 out 키워드가 모두 없는 경우 invariant
하다고 한다.