# Table of Contents

# 제네릭

제네릭(Generic)클래스나 메서드 내부에서 사용할 데이터의 자료형을 외부에서 설정하는 것이다. 제너릭의 장점은 다음과 같다.

  • 타입에 종속되지 않은 유연한 로직
  • 타입 안정성
  • 자동 형변환

# 제네릭 클래스

제네릭 클래스를 정의할 땐 <>안에 임의의 문자를 명시한다. 이 문자를 타입 파라미터(Type Parameter)라고 하며, 타입 파라미터는 관습적으로 대문자 E(Element), T(Type), K(Key), V(Value)를 사용한다.

class Box<T> constructor(val item: T) 

이렇게 정의된 제네릭 클래스는 다음과 같이 사용한다. 타입 파리미터에는 어떠한 데이터 타입도 들어갈 수 있다.

var box1 = Box<Int>(1)
var box2 = Box<String>("Hello World.")
var box3 = Box<Person>(Person("Paul"))

제너릭을 사용한 아래 코드는

class Box<T> constructor(val item: T)

var box = Box<Int>(1)

컴파일 후 다음과 같이 변환된다.

class Box constructor(val item: Int)

var box = Box(1)

타입 파라미터는 여러 개를 사용할 수도 있다.

class Box<T, U> constructor(val item1: T, val item2: U)

var box1 = Box<Int, String>(1, "Hello World.")
var box2 = Box<String, Double>("Hello World.", 3.4)
var box3 = Box<Person, Phone>(Person("Paul"), Phone("Apple"))

# 제네릭 함수

제네릭은 클래스 뿐만 아니라 함수에도 사용할 수 있다.

# 함수의 파라미터

함수의 파라미터에 제네릭을 사용할 땐 키워드 fun함수 이름 사이에 타입 파라미터를 명시한다.

fun <T> printSomething(something: T) {
    println("Something: ${something.toString()}")
}
fun main() {
    printSomething<Int>(1)
	printSomething<String>("Hello World")
}

여러 개의 타입 파라미터를 사용할 수도 있다.

fun <T, K> printSomething(something1: T, something2: K) {
    println("Something1: ${something1.toString()}, Something2: ${something2.toString()}")
}
fun main() {
    printSomething<Int, String>(3, "Hello")		// Something1: 3, Something2: Hello
}

# 함수의 반환 값

함수의 반환 값에도 타입 파라미터를 사용할 수 있다.

fun <T, K> getSomething(something: T): K {
    return something as K
}
fun main() {
    val result = getSomething<Double, Int>(1.5)
    println(result)	// 1
}

# 타입 제한

타입 제한을 사용하면 타입 파라미터에 들어올 실제 값의 자료형을 제한할 수 있다. 타입 제한은 <T: Superclass> 형태로 사용한다.

클래스에서의 타입 제한은 다음과 같이 사용할 수 있다.

class Person<T: Phone> constructor(val phone: T)

<T: Phone>가 바로 타입을 제한하는 코드다. 타입 파리미터 T에는 다음 클래스의 인스턴스만 올 수 있다.

  • Phone클래스
  • Phone클래스를 상속받는 클래스
open class Phone()  // Phone 클래스
open class Galaxy(): Phone() {}  // Phone 클래스를 상속하는 Galaxy 클래스
open class Laptop()  // Phone 클래스를 상속하지 않는 Laptop 클래스
var person1 = Person<Phone>(Phone())    // Success
var person2 = Person<Galaxy>(Galaxy())  // Success
var person3 = Person<Laptop>(Laptop())  // Error

<T: Superclass> 대신 키워드 where를 사용할 수 있다.

class Person<T> constructor(val phone: T) where T: Phone

메소드에서 타입 제한은 다음과 같이 사용할 수 있다.

fun <T: String> printSomething(something: T) {
    println("Something: ${something.toString()}")
}
fun main() {
	printSomething<String>("Hello World")  // Success
    printSomething<Int>(1)  // Error
}

마찬가지로 키워드 where로 타입 제한을 할 수 있다.

fun <T> printSomething(something: T) where T: String {
    println("Something: ${something.toString()}")
}
fun main() {
	printSomething<String>("Hello World")  // Success
    printSomething<Int>(1)  // Error
}

키워드 where의 타입 파라미터가 여러 개일 때 각각에 대해 타입 제한을 할 수 있다는 것이다.

fun <T, K> printSomething(something1: T, something2: K) where T: String, K: Int {
    println("${something1.toString()}, ${something2.toString()}")
}
fun main() {
	printSomething<String, Int>("Hello World", 3)  // Success
	printSomething<Int, String>(3, "Hello World")  // Error
}

# in

키워드in은 Java 제네릭의 super와 유사하다. 키워드in제네릭의 타입 파라미터를 특정 클래스와 그 클래스의 조상 클래스로 제한한다.

아래 예제를 살펴보자. Programmer클래스는 Person를 상속하고 AppProgrammerProgrammer를 상속하고 있다.

open class Person constructor(var name: String)

open class Programmer constructor(name: String, var company: String) : Person(name)

class AppProgrammer constructor(name: String, company: String, var field: String) : Programmer(name, company)

이제 아래 코드를 살펴보자.

fun printArrayList(arrayList: ArrayList<in Programmer>) {
    println(arrayList.toString())
}

<in Programmer>를 통해 Programmer와 그 부모 클래스인 Person클래스만 들어올 수 있도록 하고 있다.

var people = arrayListOf<Person>()
printArrayList(people)          // Success

var people = arrayListOf<Programmer>()
printArrayList(programmers)     // Success

var people = arrayListOf<AppProgrammer>()
printArrayList(appProgrammer)   // Error

# out

키워드out은 Java 제너릭의 extends와 유사하다. 키워드out타입 파라미터의 실제 타입을 특정 클래스와 특정 클래스의 자손 클래스로 제한한다.

open class Person constructor(var name: String)

open class Programmer constructor(name: String, var company: String) : Person(name)

class AppProgrammer constructor(name: String, company: String, var field: String) : Programmer(name, company)

fun printArrayList(arrayList: ArrayList<out Programmer>) {
    println(arrayList.toString())
}
var people = arrayListOf<Person>()
printArrayList(people)          // Error

var people = arrayListOf<Prograammer>()
printArrayList(programmers)     // Success

var people = arrayListOf<AppProgrammer>()
printArrayList(appProgrammer)   // Success

# 타입 소거

제너릭의 타입 파라미터는 컴파일 타임에 존재하지만 런타임에는 접근할 수 없다. 바로 타입 소거 때문이다. 다음 예제를 살펴보자.

fun <T> printType(value: T) {

    when (T::class) {   // Error, Cannot use 'T' as reified type parameter. Use a class instead.
        String::class -> {
            print("value is String type.")
        }
        Int::class -> {
            print("value is Int type.")
        }
    }
}

var name: String = "Paul"
printType<String>(name)

위 코드는 T:class 형태로 클래스 타입을 읽으려 하고 있다. 그러나 이 코드를 컴파일하면 에러가 발생한다. 타입 파라미터인 T는 런타임에 접근할 수 없기 때문이다. 따라서 보통 다음과 같이 별도의 인자로 클래스 타입을 전달하여 사용한다.

fun <T: Any> printType(value: T, classType: KClass<T>) {
    when (classType) {
        String::class -> {
            print("value is String type.")
        }
        Int::class -> {
            print("value is Int type.")
        }
    }
}

var name: String = "Paul"
printType<String>(name, String::class)

하지만 키워드 reified를 타입 파라미터 앞에 붙여주고 inline함수 형태로 사용하면 Kclass를 인자로 전달할 필요없이 런타임에 타입 T에 접근할 수 있다.

inline fun <reified T: Any> printType(value: T) {
    when (T::class) {
        String::class -> {
            print("value is String type.")
        }
        Int::class -> {
            print("value is Int type.")
        }
    }
}

var name: String = "Paul"
printType<String>(name)