Home Generics
Post
Cancel

Generics

최종 수정 날짜 : 2022-03-30 17:32:45 +0900

Generics

  • 제네릭(Generic)은 타입이 정해져있지 않은 클래스나 함수를 의미하며, 클래스가 인스턴스화 될 때 타입이 확정된다.
  • 다양한 타입(자료형)을 다룰 수 있기 때문에 컬렉션에서 자주 사용한다.
    • 컬렉션 : List, Map, 등..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Number<T>(private val value: T) {
    fun getType() {
        println(value!!::class.java.typeName)
    }
}

fun main() {
    val number = Number<Double>(3.0)
    number.getType()

    val number2 = Number(10)
    number2.getType()

    val number3 = Number<Boolean>(true)
    number3.getType()
}

// 결과 
// java.lang.Double
// java.lang.Integer
// java.lang.Boolean
  • 제네릭 타입의 클래스를 생성하기 위해서는 단순히 클래스 뒤에 <타입> 형태를 취해 생성할 수 있다.

  • 위 예제의 number2 변수를 보면 따로 타입을 설정하지 않았는데, 이는 컴파일러가 유추할 수 있는 값이 생성자로 들어갔기 때문에 제거가 가능하다.

  • 제네릭에서 사용하는 형식 매개변수 이름은 아래와 같다.

    • 아래 이름들을 사용해야한다는 강제성은 없지만 일종의 규칙처럼 사용되기 때문에 알아두면 좋다.
    매개변수 이름의미
    E요소 (Element)
    K키 (Key)
    N숫자 (Number)
    T형식 (Type)
    V값 (Value)
    R리턴 (Return)

제네릭 함수 및 메소드

  • 제네릭 함수는 아래와 같이 사용된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun <T> find(a: Array<T>, target: T): Int {
}

fun main() {
    val array1 = arrayOf("Apple", "Banana")
    val array2 = arrayOf(1, 2, 3)

    println(find<String>(array1, "Banana"))
    println(find(array2, 2))
}

// 결과
// 1
// 1
  • 코틀린에서 사용하는 제네릭 함수들을 살펴본 결과 대부분 파일의 최상위에서 위와 같이 선언함을 알 수 있었다.
    • 최상위 : 어떤 클래스에도 속해있지 않은 함수 또는 메소드
  • 제네릭 클래스에서는 매개변수 이름(E, K, N, T, V 등)을 전달받기 때문에 위와 같이 제네릭 함수를 쓸 필요는 없지만, 최상위에 있는 제네릭 함수 또는 메소드에는 위처럼 fun 키워드 앞에 타입을 설정해주어야 하는 것으로 판단할 수 있다.

  • 제네릭 클래스와 마찬가지로 find 제네릭 함수 뒤에 값을 유추할 수 있다면 <타입>을 제거해서 사용이 가능하다.

타입 제한하기

  • 제네릭으로 구현하게 되면 실수든 고의든 의도하지 않은 타입을 사용하게 될 수도 있다.
  • 간단한 계산기로 예시를 들어보면 아래와 같다.
    • 제네릭 함수나 메서드, 클래스에서는 연산(+, -, *, /)등을 수행할 수 없기 때문에 람다식으로 연산 후 전달하는 방법을 사용한다.
    • 수행이 불가능한 이유는 자료형을 추론할 수 없기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Calculator<T> {
    fun add(a: T, b: T, op: (T, T) -> T): T {
        return op(a, b)
    }
}

fun main() {
    val calculator = Calculator<Int>()
    val data = calculator.add(10, 20) { a, b ->
        a + b
    }
    println(data)

    val calculator2 = Calculator<String>()
    val data2 = calculator2.add("a", "b") { a, b ->
        a + b
    }
    println(data2)
}

// 결과
// 30
// ab
  • 우리가 생각하고 의도하는 계산기는 숫자를 더한 결괏값만 원하지만, 위와 같이 String 타입으로 제네릭 클래스를 인스턴스화 하더라도 막을 방법이 없다.

  • 이럴 때 숫자만 가능하도록 제약조건을 걸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Calculator<T: Number> {
    fun add(a: T, b: T, op: (T, T) -> T): T {
        return op(a, b)
    }
}

fun main() {
    val calculator = Calculator<Int>()
    val data = calculator.add(10, 20) { a, b ->
        a + b
    }
    println(data)

    val calculator2 = Calculator<String>() // <--- 오류 발생
    val data2 = calculator2.add("a", "b") { a, b ->
        a + b
    }
    println(data2)
}

특정 조건으로 타입 제한하기 (Where)

  • 제네릭 타입으로 특정 조건을 만족하는 타입만 받고싶을 수 있다. 그럴 때 Where 키워드를 사용해 제한할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package generics

interface InterfaceA
interface InterfaceB

class ImplA : InterfaceA, InterfaceB
class ImplB : InterfaceA

fun <T> someFunction(): Int where T: InterfaceA, T: InterfaceB {
    return 10
}

fun main() {
    val implA = ImplA()
    val implB = ImplB()

    someFunction<ImplA>()
    someFunction<ImplB>() // <--- 오류 발생
}
  • 위 코드는 간단히 InterfaceA, InterfaceB를 상속받은 ImplA 클래스와 InterfaceA만 상속받은 ImplB 클래스를 이용해 someFunction 제네릭 함수를 이용하려고 한 결과이다. 예시에서 볼 수 있듯이 where 키워드로 InterfaceAInterfaceB를 상속받은 클래스만 허용하기 때문에 메인 함수의 두 번째 someFunction에서 에러가 발생하는 것을 알 수 있다.

가변성(Variance)의 유형

  • 기본적으로 제네릭에서는 클래스 간의 상/하위 개념이 존재하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Parent
class Child : Parent()
class Box<T>

fun main() {
    val value1: Parent = Child()    // <--- 가능
    val value2: Box<Parent> = Box<Child>() // <--- 불가능

    // 에러 내용
    // 타입이 일치하지 않습니다.
    // 필요 항목:
    // Box<Parent>
    // 발견된 항목:
    // Box<Child>
}
  • 위 예시처럼 Child 클래스는 Parent 클래스의 모든 기능을 가지고 있으므로 선언이 가능(value1)하지만, 이것을 제네릭으로 표현(value2)하면 에러가 발생한다.

  • 따라서 상/하위의 값을 클래스와 동일하게 사용하기 위해서는 가변성의 유형에 대해서 알아야 한다.

  • 가변성의 유형은 아래와 같다.

    1. 공변성(Convariance) : T’가 T의 하위 자료형이면, Box<T’>는 Box의 하위 자료형이다. (out)
    2. 반공변성(Contravariance) : T’가 T의 하위 자료형이면 Box는 Box<T'>의 하위 자료형이다. (in)
    3. 무변성(Invariance) : Box와 Box<T'>는 관계가 없다.

    가변성의 관계를 다이어그램으로 나타내면 아래와 같다.

    가변성

무변성(Invariance)

  • 위 예시의 value2가 무변성에 해당한다. 타입에 in이나 out으로 공변성 또는 반공변성을 선언하지 않을 경우 무변성으로 선언되어 상하관계와 상관없이 자료형 불일치가 발생한다.

공변성(Convariance)

  • 예를 들어서 Int가 Any의 하위 자료형이므로 제네릭 타입 T에 대해 공변적이라고 한다.
1
2
3
4
5
6
class Box<out T>()

fun main() {
    val anys: Box<Any> = Box<Int>()
    val nothings: Box<Nothing> = Box<Int>()  // <--- 에러 발생
}
  • Int는 Any의 하위 자료형이므로 제네릭 클래스에 out 키워드를 선언해서 공변성을 적용할 수 있다.
  • 그러나 Int가 Nothing의 하위 타입이 아니므로 에러가 발생한다.
    1
    
    public class Nothing private constructor()
    

반공변성(Contravariance)

  • 공변성의 상/하위 관계가 반대가 된다.
1
2
3
4
5
6
class Box<in T>()

fun main() {
    val anys: Box<Any> = Box<Int>()     // <--- 에러 발생
    val nothings: Box<Nothing> = Box<Int>()
}
  • Any는 Int의 하위 자료형이 아니므로 에러가 발생한다.
  • 그러나 Nothing은 Int의 하위 자료형이므로 in 키워드를 적용해 반공변성을 적용할 수 있다.

  • TODO
    • reified 정리
    • 프로젝션
    • 가변성 예제 더 확인해보기
  • [참고]
    • https://kotlinlang.org/docs/generics.html
    • Do It! 코틀린 프로그래밍 책
This post is licensed under CC BY 4.0 by the author.

[BOJ 2468] 안전 영역

[프로그래머스] 위장

Comments powered by Disqus.