[Kotlin in Action 2/e] 11장 제네릭스

왕왕조현·2026년 2월 12일

Kotlin in Action 2/e

목록 보기
11/18
post-thumbnail

안녕하세요!

제네릭과 타입에 대한 개념에 대한 정리글로 돌아온 개발자 꿈나무 김조현입니다.

이번 글에서는 제네릭스를 중심으로 타입의 중요성까지 정리해보겠습니다.


제네릭스란?

제네릭스는 클래스나 메서드 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법입니다.

제네릭스를 사용하면 타입 파라미터를 받는 타입을 정의할 수 있습니다. 제네릭 타입의 인스턴스가 만들어질 때는 타입 파라미터를 구체적인 타입 인자로 치환합니다.

리스트를 다루는 함수를 작성하면 어떤 특정 타입을 저장하는 리스트 뿐만 아니라 모든 리스트를 다룰 수 있는 함수를 원할 것입니다. 이럴 때 필요한 것이 제네릭 함수입니다.


제네릭 함수란?

제네릭 함수는 그 자신이 타입 파라미터를 받으며, 제네릭 함수를 호출할 때는 반드시 구체적 타입으로 타입 인자를 넘겨야 합니다.

fun <T> List<T>.slice(indices: IntRange): List<T>

함수의 타입 파라미터 T가 수신 객체와 반환 타입에 쓰입니다. 이 함수를 호출할 때 타입 인자를 명시적으로 지정할 수 있지만 대부분의 경우 컴파일러가 타입을 추론할 수 있습니다.

fun main() {
	val letters = ('a'..'z').toList()
	println(letters.slice<Char>(0..2))
	// [a, b, c]
	println(letters.slice(10..13))
	// [k, l, m, n]
}

클래스나 인터페이스 안에 정의된 메소드, 최상위 함수, 확장 함수에서 타입 파라미터를 선언할 수 있으며, 수신 객체나 파라미터 타입에 타입 파라미터를 사용할 수 있습니다.

하지만 일반 프로퍼티는 타입 파라미터를 가질 수 없습니다. 클래스 프로퍼티에 여러 타입의 값을 저장할 수 없기 때문에 일반 프로퍼티는 여러 타입을 가질 수 있는 타입 파라미터를 가질 수 없습니다.

val <T> List<T>.penultimate: T
	get() = this[size - 2]

fun main() {
	println(listOf(1, 2, 3, 4).penultimate)
	// 3
}

제네릭 클래스를 선언하는 방법은?

타입 파라미터를 넣은 홑화살괄호(<>)를 클래스나 인터페이스 이름 뒤에 붙이면 해당 클래스나 인터페이스를 제네릭하게 만들 수 있습니다.

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

interface List<T> {
	operator fun get(index: Int): T
	// ...
}

제네릭 클래스를 확장하는 클래스를 정의하려면 기반 타입의 제네릭 파리미터에 대해 타입 인자를 지정해야 합니다. 이때 구체적인 타입을 넘길 수도 있고 타입 파라미터로 받은 타입을 넘길 수도 있습니다.

// 구체적인 타입인 String을 타입 인자로 지정함.
class StringList: List<String> {
	override fun get(index: Int): String = TODO()
	// ...
}

// 제네릭 타입 파라미터 T를 타입 인자로 지정함.
class ArrayList<T>: List<T> {
	override fun get(index: Int): T = TODO()
	// ...
}

타입 파라미터의 제약은?

타입 파라미터 제약은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능입니다. 예를 들어 sum 함수에Int나 Double타입은 합을 적용할 수 있지만 String은 적용할 수 없습니다.

이런 경우에 sum 함수가 타입 파라미터로 숫자 타입만을 허용하도록 정의하는 것입니다.

제약을 가하려면 타입 파라미터 이름 뒤에 콜론을 표시하고 그 뒤에 상계 타입을 적으면 됩니다.

fun <T: Number> List<T>.sum(): T

Number는 코틀린 표준 라이브러리에서 숫자 타입을 표현하는 모든 클래스의 상위 클래스이기 때문에 Int나 Double을 지정해도 괜찮습니다.

fun main() {
	println(listOf(1, 2, 3).sum())
	// 6
}

또한 타입 파라미터 T에 대한 상계를 정하고 나면 T 타입의 값을 그 상계 타입의 값으로 취급할 수 있어서 상계 타입에 정의된 메소드를 T 타입 값에 대해 호출할 수 있습니다.

fun <T: Number> oneHalf(value: T): Double {
	return value.toDouble() / 2.0
}

fun main() {
	println(oneHalf(3))
	// 1.5
}

명시적으로 표시하여 널이 될 수 있는 타입 인자 제외시키는 방법은?

아무런 상계를 정하지 않은 타입 파라미터는 Any?를 상계로 정한 파라미터와 같습니다.

타입 T에 물음표가 붙어있지 않더라도 해당하는 타입 인자로 널이 될 수 있는 타입을 사용할 수도 있습니다.

만약 항상 널이 될 수 없는 타입만 타입 인자로 받게 만들려면 타입 파라미터에 Any를 상계로 하는 제약을 가해야 합니다.

class Processor<T: Any> {
	fun process(value: T) {
		value.hashCode()
	}
}

Any 뿐만 아니라 다른 널이 될 수 없는 타입을 사용해 상계를 정해도 타입 파라미터가 널이 아닌 타입으로 제약됩니다.


자바와 상호운용할 때는 제네릭 타입을 ‘널이 될 수 없음’으로 표시해야 한다

자바에서는 어노테이션을 활용해 특정 부분에서만 널이 될 수 없도록 지정할 수 있습니다. 하지만 코틀린에서는 이런 제약을 직접 변경할 수 없기에 자바의 코드와는 다른 결과를 가져올 수 있습니다.

import org.jetbrains.annotations.NotNull;

public interface JBox<T> {
	/**
	* 널이 될 수 없는 값을 박스에 넣는다.
	*/
	void put(@NotNull T t);
	/**
	* 널 값이 아닌 경우 값을 박스에 넣고
	* 널 값인 경우 아무것도 하지 않는다.
	*/
	void putIfNotNull(T t);
}
class KBox<T: Any>: JBox<T> {
	override fun put(t: T) { /* ... */ }
	override fun putIfNotNull(t: T) { /* 문제 생김 */ }
}

이런 문제를 해결하기 위해 코틀린은 타입을 사용하는 지점에서 절대로 널이 될 수 없다고 표시하는 방법을 제공합니다. 이런 표시는 문법적으로 T & Any 로 표현됩니다.

class KBox<T: Any>: JBox<T> {
	override fun put(t: T & Any) { /* ... */ }
	override fun putIfNotNull(t: T) { /* ... */ }
}

실행 시점 제네릭스 동작은?

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


실행 시점에 제네릭 클래스의 타입 정보를 찾을 때 한계는?

위에서 설명한 것처럼 제네릭 타입 인자 정보는 런타임에 지워집니다. 예를 들어 각 String 과 Int 타입의 원소를 가지는 리스트가 있다면 두 List 객체가 어떤 타입을 원소로 저장하는지 실행 시점에는 알 수 없습니다. 이런 점 때문에 타입 인자를 다룰 때 한계가 생깁니다.

val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)

다음으로 타입 소거로 인해 실행 시점에 타입 인자를 검사할 수 없습니다. 예를 들어 리스트가 문자열로 이뤄진 리스트인지 정수로 이뤄진 리스트인지 is 검사를 통해 타입 인자로 지정한 타입을 검사할 수 없는 것입니다.

fun readNumbersOrWords(): List<Any> {
	val input = readln()
	val words: List<String> = input.split(",")
	val numbers: List<Int> = words.mapNotNull { it.toIntOrNull() }
	return numbers.ifEmpty{ words }
}

fun printList(l: List<Any>) {
	when(l) {
		is List<String> -> println("Strings: $l")
		is List<Int> -> println("Integers: $l")
	}
}

fun main() {
	val list = readNumbersOrWords()
	printList(list)
}

그렇기에 printList에서 오류가 발생합니다. 즉, 실행 시점에 리스트임은 확실히 알 수 있지만 어떤 타입의 원소가 들어있는 리스트인지는 알 수 없습니다.

다만 저장해야 하는 타입 정보의 크기가 줄어 애플리케이션의 전체 메모리 사용량이 줄어든다는 장점도 있습니다.

as나 as? 캐스팅에도 제네릭 타입을 사용할 수 있습니다. 하지만 기저 클래스는 같지만 타입 인자가 다른 타입으로 캐스팅해도 캐스팅에 성공한다는 점을 조심해야 합니다. 실행 시점에서는 제네릭 타입의 타입 인자를 알 수 없기 때문에 항상 성공하는 것입니다. 이때는 컴파일러가 경고를 해줍니다.

fun printSum(c: Collection<*>) {
	val intList = c as? List<Int>
		?: throw IllegalArgumentException("List is expected")
	println(intList.sum()
}

fun main() {
	printSum(listOf(1, 2, 3))
	// 6
	printSum(setOf(3, 4, 5))
	// IllegalArgumentException: List is expected
	println(listOf("a", "b", "c"))
	// ClassCastException: String cannot be cast to Number
}

하지만 잘못된 타입의 원소가 들어있는 리스트를 전달하면 실행 시점에 오류가 발생합니다.

그치만 타입 정보가 주어진 경우에는 is 검사를 수행할 수 있습니다.

fun printSum(c: Collection<Int>) {
	when(c) {
		is List<Int> -> println("List sum: ${c.sum()}")
		is Set<Int> -> println("Set sum: ${c.sum()}")
	}
}

fun main() {
	printSum(listOf(1, 2, 3))
	// List sum: 6
	printSum(setOf(3, 4, 5))
	// Set sum: 12
}

타입 인자를 실체화 시키는 방법은?

타입 인자 정보가 실행 시점에 지워지는 코틀린 제네릭 타입을 피할 수 있는 방법은 인라인 함수를 사용하는 것이다. 인라인 함수를 사용하면 타입 파라미터가 실체화됩니다.

인라인 함수를 만들고 타입 파라미터를 reified로 지정하면 실행 시점에 검사할 수 있습니다.

inline fun <reified T> isA(value: Any) = value is T

fun main() {
	println(isA<String>("abc"))
	// true
	println(isA<String>(123))
	// false
}

인라인 함수에서만 실체화된 타입 인자를 쓸 수 있는 이유는?

컴파일러는 인라인 함수의 본문을 구현한 바이트코드를 그 함수가 호출되는 모든 지점에 삽입합니다. 즉 컴파일러는 실체화된 타입 인자를 사용해 인라인 함수를 호출하는 각 부분의 정확한 타입 인자를 할 수 있는 것입니다.

만들어진 바이트코드는 타입 파라미터가 아니라 구체적인 타입을 사용하므로 실행 시점에 벌어지는 타입 소거의 영향을 받지 않습니다.

다만 자바 코드에서는 reified 타입 파라미터를 사용하는 인라인 함수를 호출할 수 없다는 점을 주의해야합니다. 자바에서는 코틀린 인라인 함수를 다른 보통 함수처럼 호출하기 때문에 실제로 인라이닝되지 않기 때문입니다.


클래스 참조를 실체화된 타입 파라미터로 대신하기?

API에 대한 코틀린 어댑터를 구축하는 경우 실체화된타입 파라미터를 사용할 수 있습니다. 예를 들면 표준 자바 API인 ServiceLoader를 사용해 서비스를 읽어 들이려면 아래와 같이 호출해야 합니다.

val serviceImpl = serviceLoader.load(Service::class.java)

실체화된 타입 파라미터를 활용하면 더 읽기 쉬운 코드로 작성할 수 있습니다.

inline fun <reified T> loadService() {
	return ServiceLoader.load(T::class.java)
}

val serviceImpl = loadService<Service>()

실체화된 타입 파라미터가 있는 접근자를 정의하기

제네릭 타입에 대해 프로퍼티 접근자를 정의하는 경우 프로퍼티를 inline으로 표시하고 타입 파라미터를 reified로 하면 타입 인자에 쓰인 구체적인 클래스를 참조할 수 있습니다.

inline val <reified T> T.canonical: String
	get() = T::class.java.canonicalName

fun main() {
	println(listOf(1, 2, 3).canonical)
	// java.util.List
	println(1.canonical)
	// java.lang.Integer
}

실체화된 타입 파라미터의 제약은?

실체화된 타입 파라미터는 다음과 같은 경우에 사용할 수 있습니다.

  • 타입 검사와 캐스팅
  • 코틀린 리플렉션 API(::class)
  • 코틀린 타입에 대응하는 java.lang.Class를 얻기(::class.java)
  • 다른 함수를 호출할 때 타입 인자로 사용

하지만 아래와 같은 일은 할 수 없습니다.

  • 타입 파라미터 클래스의 인스턴스 생성하기
  • 타입 파라미터 클래스의 동반 객체 메소드 호출하기
  • 실체화된 타입 파라미터를 요구하는 함수를 호출하면서 실체화하지 않은 타입 파라미터로 받은 타입을 타입 인자로 넘기기
  • 클래스, 프로퍼티, 인라인 함수가 아닌 함수의 타입 파라미터를 reified로 지정하기

변성이란?

변성은 List<String>과 List<Any>같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념입니다.


인자를 함수에 넘겨도 안전한지 어떻게 알까?

변성은 인자를 함수에 넘겨도 안전한지 판단하게 해줍니다. 예를 들어 List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 절대로 안전합니다. String클래스는 Any를 확장하기 때문입니다.

fun printContents(list: List<Any>) {
	println(list.joinToString())
}

fun main() {
	printContents(listOf("abc", "bac"))
	// abc, bac
}

다만 리스트를 변경하는 함수일 경우 컴파일러가 호출을 금지하는 것을 볼 수 있습니다.

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

fun main() {
	val strings = mutableListOf("abc", "bac")
	addAnswer(strings)
	println(strings.maxBy{ it.length }) // 이 시점에서 예외가 발생할 것이다.
}

이 예제를 보고 MutableList<Any>가 필요한 곳에 MutableList<String>을 넘기면 안된다는 사실을 알 수 있습니다.


클래스, 타입, 하위 타입

제네릭 클래스가 아닌 클래스에서는 클래스 이름을 바로 타입으로 쓸 수 있습니다. 예를 들어 var x: String이라고 쓰면 String 클래스의 인스턴스를 저장하는 변수를 정의할 수 있습니다.

제네릭 클래스에서는 올바른 타입을 얻으려면 제네릭 타입의 타입 파라미터를 구체적인 타입 인자로 바꿔줘야 합니다. 예를 들어 List는 타입이 아니라 클래스입니다. 하지만 타입 인자를 치환한 List<Int>, List<String?> 등은 모두 타입입니다. 즉, 각각의 제네릭 클래스는 무수히 많은 타입을 만들어낼 수 있습니다.

이런 타입 사이의 관계를 파악하기 위해서는 하위 타입이라는 개념을 잘 알아야 합니다. 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입 B의 값을 넣어도 아무 문제가 없다면 타입 B는 타입 A의 하위 타입이라고 할 수 있습니다. 예를 들어 Int는 Number의 하위 타입이지만 String의 하위 타입은 아닌 것입니다.

하위 타입과 반대되는 개념을 상위 타입이라고 합니다. A 타입이 B 타입의 하위 타입이라면 B는 A의 상위 타입입니다.

하위 타입인지가 중요한 이유는 컴파일러는 변수 대입이나 함수 인자 전달 시 하위 타입 검사를 매번 수행하기 때문입니다.

fun test(i: Int) {
	val n: Number = i
	
	fun f(s: String) { /* ... */ }
	f(i)
	// 컴파일 오류가 발생합니다.
}

String은 CharSequence의 하위 타입인 것처럼 어떤 인터페이스를 구현하는 클래스의 타입은 그 인터페이스의 하위 타입입니다.

널이 될 수 없는 타입은 널이 될 수 있는 타입의 하위 타입입니다. 널이 될 수 없는 타입의 값은 널이 될 수 있는 타입의 변수에 저장할 수 있지만, 반대의 경우는 성립하지 않기 때문입니다.

val s: String = "abc"
val t: String? = s
// 성립한다.

즉, 제네릭 타입을 얘기할 때 특히 하위 클래스와 하위 타입의 차이가 중요해집니다.

어떤 제네릭 타입에 대해 서로 다른 두 타입 A와 B에 대해 MutableList<A>가 항상 MutableList<B>의 하위 타입도 아니고 상위 타입도 아닌 경우에 대해 무공변이라고 말합니다.

A가 B의 하위 타입이면 List<A>는 List<B>의 하위 타입이 되는 클래스나 인터페이스를 공변적이라고 말합니다.


공변성은 하위 타입 관계를 유지한다

공변적은 클래스는 제네릭 클래스에 대해 A가 B의 하위 타입일 때 Producer<A>가 Producer<B>의 하위 타입인 경우를 말합니다. 예를 들면 Cat이 Animal의 하위 타입이기 때문에 Producer<Cat>은 Producer<Animal>의 하위 타입입니다.

제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 넣어야 합니다.

interface Producer<out T> {
	fun produce(): T
}

클래스의 타입 파라미터를 공변적으로 만들면 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환값으로 사용할 수 있습니다.

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

class Herd<T: Animal> {
	val size: Int get() = { /* ... */ }
	operator fun get(i: Int): T { /* ... */ }
}

fun feedAll(animals: Herd<Animal>) {
	for (i in 0..<animals.size) {
		animals[i].feed()
	}
}

class Cat: Animal() {
	fun cleanLitter() { /* ... */ }
}

fun takeCareOfCats(cats: Herd<Cat>) {
	for (i in 0..< cats.size) {
		cats[i].cleanLitter()
	}
	// feedAll(cats)
}

feedAll에 Cat타입을 넘기면 타입 불일치 에러가 나타나게 됩니다. Herd 클래스의 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 떄문에 고양이 무리는 동물 무리의 하위 클래스가 아니게 된 것입니다.

Herd 클래스를 공변적인 클래스로 만들면 강제 캐스팅을 하지 않고 타입 불일치 문제를 해결할 수 있습니다.

class Herd<out T: Animal> {
	val size: Int get() = { /* ... */ }
	operator fun get(i: Int): T { /* ... */ }
}

fun takeCareOfCats(cats: Herd<Cat>) {
	for (i in 0..< cats.size) {
		cats[i].cleanLitter()
	}
	feedAll(cats)
}

하지만 타입 파라미터를 공변적으로 지정하면 클래스 내부에서 타입 안전성을 보장하기 위해 공변적 파라미터의 위치를 항상 아웃 위치에 놓는 방식으로 사용 방법을 제한합니다. 이는 클래스가 T 타입의 값을 생산할 수는 있지만 소비할 수는 없다는 의미입니다.

클래스 멤버를 선언할 때 타입 파라미터를 사용할 수 있는 지점은 인과 아웃 위치로 나뉩니다. T 함수가 반환 타입에 쓰인다면 T는 아웃 위치에 존재하는 것이고 T 타입의 값을 생산합니다. T가 함수의 파라미터 타입에 쓰인다면 T는 인 위치에 있으며 T 타입의 값을 소비합니다.

즉, 타입 파라미터 T에 붙은 out 키워드는 하위 타입 관계가 유지되고, T를 아웃 위치에서만 사용할 수 있다는 의미를 가집니다.

MutableList<T>를 타입 파라미터 T에 대해 공변적인 클래스로 선언할 수 없습니다. MutableList<T>에는 T를 인자로 받아 그 타입의 값을 반환하는 메소드가 있기 때문에 T가 인과 아웃 위치에 동시에 쓰이기 때문입니다.

inteface MutableList<T> : List<T>, MutableCollection<T> {
	override fun add(element: T): Boolean
}

위의 인터페이스에서 볼 수 있듯이 T가 인 위치에서 사용됩니다.

다만 생성자 파라미터는 인이나 아웃 위치 어느 쪽도 아닙니다. 변성은 제네릭 타입의 인스턴스 역할을 하는 클래스 인스턴스를 잘못 사용하는 일이 없게 방지하는 역할을 하는데 생성자는 나중에 호출할 수 있는 메소드가 아니기 때문에 위험할 여지가 없어 상관이 없는 것입니다.

하지만 var이나 val 키워드를 생성자 파라미터에 적는다면 게터나 세터를 정의하는 것과 같습니다. 그렇기에 val 프로퍼티는 아웃 위치, var 프로퍼티는 아웃과 인 모두에 해당합니다.


반공변성은 하위 타입 관계를 뒤집는다

반공변성은 공변성을 거울에 비친 상이라고 할 수 있습니다. 반공변 클래스의 하위 타입 관계는 그 클래스의 타입 파라미터의 상하위 타입 관계와 반대입니다.

interface Comparator<in T> {
	fun compare(e1: T, e2: T): Int { /* ... */ }
}

이 인터페이스의 메소드는 T 타입의 값을 소비하기만 하고, 이는 T가 in 위치에서만 쓰인다는 의미입니다. 따라서 T 앞에 in 키워드를 붙여야하는 것입니다.

반공변성은 어떤 클래스에 대해 타입 B가 타입 A의 하위 타입일 때 Consumer<A>가 Consumer<B>의 하위 타입인 관계가 성립하는 것을 의미합니다.


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

클래스를 선언하면서 변성을 지정하면 그 클래스를 사용하는 모든 장소에 변성 지정자가 영향을 끼치므로 편리합니다. 이런 방식을 선언 지점 변성이라고 부릅니다.

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

코틀린도 사용 지점 변성을 지원합니다. 따라서 타입 파라미터가 공변적인지 반공변적인지 선언할 수 없는 경우에도 특정 타입 파라미터가 나타나는 지점에서 변성을 정할 수 있습니다.


스타 프로젝션이란?

스타 프로젝션이란 제네릭 타입 인자 정보가 없음을 표현하고자할 때 을 사용해 표현합니다. 예를 들어 타입이 알려지지 않은 리스트를 List<>이라는 구문으로 표현할 수 있습니다.

조심해야할 점은 MutableList<>는 MutableList<Any?>와 같지 않습니다. MutableList<Any?>는 모든 타입의 원소를 담을 수 있음을 알 수 있는 리스트지만 MutableList<>는 어떤 정해진 구체적인 타입의 원소만을 담는 리스트지만 정확히 모른다는 사실을 표현하는 것입니다.

즉, MutableList<*>은 String과 같은 구체적인 원소를 저장하기 위해 만들어진 것입니다.

fun printFirst(list: List<*>) {
	if (list.isNotEmpty()) {
		println(list.first())
	}
}

fun main() {
	printFirst(listOf("Sveta", "Seb", "Dima", "Roman"))
	// Sveta
}

타입 별명

타입 별명은 복잡한 제네릭 타입이나 함수형 타입을 여러 곳에서 매번 반복해 사용하는 것을 피하고 싶을 때 유용하게 사용할 수 있습니다. 타입 별명은 typealias 키워드 뒤에 별명을 적어 타입 별명을 선언할 수 있습니다.

typealias NameCombiner = (String, String, String, String) -> String

val authorsCombiner: NameCombiner = { a, b, c, d -> "$a et al." }
val bandCombiner: NameCombiner = { a, b, c, d -> "$a, $b & The Gang" }

fun combineAuthors(combiner: NameCombiner) {
	println(combiner("Sveta", "Seb", "Dima", "Roman"))
}

fun main() {
	combineAuthors(bandCombiner)
	// Sveta, Seb & The Gang
	combineAuthors(authorsCombiner)
	// Sveta et al.
	combineAuthors{ a, b, c, d -> "$d, $c & Co." }
	// Roman, Dima & Co.
}

마무리입니다!

이번 글에서는 제네릭스의 개념부터 타입 별명까지 개념을 정리해봤습니다.

제네릭스라는 개념을 이번 장을 공부하며 처음 알게 되었습니다. 기존 예제에서 나오는 파라미터 타입 T가 무엇인지 이해하지 못했지만 이번 장을 통해 제네릭 함수에 대해 이해하게 되었습니다.

또한 컬렉션이 어떤 원리로 모든 타입을 가질 수 있는가에 대한 의문을 가지고 있었는데, 컬렉션과 관련된 대부분의 기능을 구현할 때 타입 파라미터를 활용해 String, Number 뿐만 아니라 커스텀 클래스에 대한 타입도 들어갈 수 있다는 것을 알게 되었습니다.

다음에는 어노테이션과 리플렉션에 대한 개념으로 돌아오겠습니다.

읽어주셔서 감사합니다!🙂‍↕️

profile
천천히, 꾸준히, 한 걸음씩

0개의 댓글