제네릭스(Generics)는 클래스나 함수를 정의할 때 타입을 확실히 정하지 않는 것을 말합니다. 그렇기 때문에 다양한 타입으로 클래스를 여러개 정의하지 않아도 됩니다. Generic 함수 및 클래스를 정의하는 방법에 대해서 알아보겠습니다.
또, Invariance(불변성)과 Covariance(공변성)이란 용어도 있습니다. Generics에서 이 용어들이 어떤 것을 의미하는지 알아보고 in/out 키워드에 대해서 알아보겠습니다.
Generic 함수 정의
Generic 함수를 정의할 때, 타입이 정해지지 않은 변수는 함수 이름 앞에 <T>
처럼 정의되어야 합니다.
아래 코드는 타입 T 변수 num1과 num2를 더하고 타입 T 변수를 리턴하는 함수입니다.
fun <T> addNumbers(num1: T, num2: T): T {
return (num1.toDouble() + num2.toDouble()) as T
}
위 함수는 아래 처럼 호출할 수 있습니다.
fun main(args: Array<String>) {
println(addNumbers(10, 20)) // 결과: 30
println(addNumbers(10.1, 20.1)) // 결과: 30.200000000000003
}
T는 타입이 정해지지 않았기 때문에 어떤 타입이든 올 수 있습니다.
Generic 클래스 정의
Generic 클래스를 정의할 때 타입이 정해지지 않은 변수는 클래스 이름 다음에 <T>
가 온다는 것입니다.
아래 코드는 임의의 타입 T를 받는 Rectangle 클래스 입니다. T의 타입은 설정되지 않았기 때문에 어떤 타입이든 올 수 있습니다.
class Rectangle<T>(val width: T, val height: T) {
}
아래 코드는 위에서 정의한 Rectangle 클래스를 생성하는 코드입니다. 객체를 생성할 때 Rectangle<Double>
처럼 T의 타입이 무엇인지 써줘야 합니다.
fun main(args: Array<String>) {
val rec = Rectangle<Double>(10, 20)
val rec1 = Rectangle<String>("aa", "bb")
}
하지만 코틀린은 전달된 인자로 부터 T의 타입을 추론하기 때문에 아래와 같이 Rectangle
만 써줘도 됩니다.
명시적으로 T의 타입을 써주고 싶으시면 위처럼 써주시면 됩니다.
fun main(args: Array<String>) {
val rec = Rectangle(10, 20)
val rec1 = Rectangle("aa", "bb")
}
만약 두개 이상의 다른 타입의 변수를 Generic으로 정의하려면 <T, K>
처럼 두개의 변수를 써주면 됩니다.
class Rectangle<T, K>(val width: T, val height: T, val name: K) {
}
Constraints(제한, 제약)
위에서 정의한 클래스의 문제점은 Rectangle("aa", "bb")
와 같이 숫자가 아닌 인자도 허용이 된다는 것입니다.
우리는 width와 height 변수가 숫자만 허용되도록 만들고 싶습니다.
아래 코드에서 <T: Number>
는 super type이 Number인 객체만 T로 받도록 허용합니다.
class Rectangle<T: Number>(val width: T, val height: T) {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
아래 코드에서 Rectangle을 생성할 때, 인자가 String인 경우 컴파일 에러가 발생하고 Int와 Double 일 때 객체가 생성된 것을 볼 수 있습니다. (Int와 Double는 Number 클래스를 상속받았습니다)
fun main(args: Array<String>) {
val rec = Rectangle(10, 20)
val rec1 = Rectangle(10.5, 20.5)
// val rec2 = Rectangle("aa", "bb") // compile error
}
2개 이상의 Constraints
바로 위에서 T에 Number만 가능하도록 제약을 주었습니다. 추가로 더 많은 제약을 줄 수 있습니다.
아래 코드에서 T는 Number를 상속받고, Comparablewhere
를 사용해야 합니다.
class Rectangle<T>(val width: T, val height: T)
where T: Number, T: Comparable<T> {
fun getArea(): T {
return (width.toDouble() * height.toDouble()) as T
}
}
클래스는 1개의 클래스만 상속받을 수 있기 때문에, 2개 이상의 제약은 1개의 클래스와 1개 이상의 인터페이스가 됩니다.
Invariance(불변성)
Double
은 Number
를 상속하고, Double
의 Super class는 Number
입니다.
class Double : Number, Comparable<Double>
하지만 Rectangle<Dobule>
의 Super class는 Rectangle<Number>
가 아닙니다.
두개의 타입이 서로 상속 관계이지만, Generic 클래스의 상속 관계는 아니라는 것을 Invariance(불변성)라고 합니다.
코틀린에서는 Generic의 모든 타입은 Invariance입니다. Invariance의 반대말은 Covariance인데, in/out 키워드로 Generics를 Covariance로 변경할 수 있습니다. in/out 키워드는 아래에서 설명합니다.
Covariance(공변성, 함께 변하는 속성)
Covariance(공변성)은 Invariance(불변성)의 반대입니다.
Number
가 Double
의 super class일 때 Rectangle<Number>
가 Rectangle<Dobule>
의 super class이면, 이것을 Covariance(공변성)라고 합니다.
out 키워드
out 키워드는 두개의 타입이 Invariance일 때, Covariance로 만들어줍니다.
즉, out 키워드는 Rectangle<Number>
가 Rectangle<Dobule>
의 super class가 되도록 합니다.
아래 코드는 Rectangle<Double>
객체를 Rectangle<Number>
에 대입하는 코드입니다. 이 코드는 빌드에러가 발생합니다.
class Rectangle<T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
val derivedClass = Rectangle<Double>(10.5, 20.5)
val baseClass : Rectangle<Number> = derivedClass
}
하지만, T
앞에 out
을 붙여주면 컴파일이 됩니다. 바로 out
이 타입의 상속구조가 Generic의 상속구조와 같다는 것을 정의하였기 때문입니다.
컴파일러는 Rectangle<Double>
가 Rectangle<Number>
의 하위 클래스라고 인식하고 있습니다.
class Rectangle<out T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
val derivedClass = Rectangle<Double>(10.5, 20.5)
val baseClass : Rectangle<Number> = derivedClass
}
Contravariance(반공변성)
Contravariance는 Covariance의 반대 방향으로 공변성 조건을 만족하는 것을 말합니다.
위의 예제에서, Number
가 Double
의 super class일 때 Rectangle<Dobule>
가 Rectangle<Number>
의 super class라면 Contravariance(반공변성)라고 합니다.
in 키워드
in
키워드는 out 키워드의 반대입니다.
in
은 타입의 상위/하위 클래스 구조가 Generic에서는 반대 방향의 상위/하위 클래스 구조를 갖는다는 것을 정의합니다.
즉, 두 타입의 관계가 Contravariance임을 나타낼 때 사용합니다.
예제를 보시면, 아래 코드는 T 앞에 in
을 정의하였습니다. 그래서 Rectangle<Double>
이 Rectangle<Number>
의 상위 클래스가 되었습니다.
class Rectangle<in T: Number>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
val baseClass = Rectangle<Number>(10.5, 20.5)
val derivedClass : Rectangle<Double> = baseClass
}
정리
Generics로 함수와 클래스를 정의하는 방법에 대해서 알아보았습니다. Invariant, Invariant, Covariant의 관계를 UML로 표현하면 아래처럼 표현할 수 있습니다. Invariant만 관계가 없고, 나머지는 화살표 방향이 상속 관계입니다.
참고
Related Posts
- Kotlin - 배열에서 최소 값, 최대 값 찾기
- Kotlin - 2차원 배열 선언, 초기화 방법
- Kotlin - 배열 선언, 초기화 방법
- Kotlin - 리스트, 배열 길이 가져오기
- Kotlin - 리스트에서 최대, 최소 값 찾기
- Kotlin - for 반복문, 배열/리스트 순회
- Kotlin - Timer, 주기적으로 함수 실행
- Kotlin - sleep, 쓰레드 몇 초 지연
- Kotlin - Thread 생성 및 실행
- Kotlin에서 정규표현식 사용하기
- Kotlin - 문자열 길이 계산
- Kotlin - 문자열 비교 방법(equals, ==, compareTo)
- Kotlin - 2개의 배열 하나로 합치기
- Kotlin - 2개의 List 하나로 합치기
- Kotlin - 디렉토리의 모든 파일 리스트 출력
- Kotlin - 리스트 정렬 방법 (sort, sortBy, sortWith)
- Kotlin - 문자열 뒤집기 (Reverse String)
- Kotlin - 랜덤 숫자 생성 (Random, SecureRandom)
- Kotlin - Range, 숫자 범위 표현
- Kotlin - 음수를 양수로 변환, math.abs()
- Kotlin - List를 Set로 변환
- Kotlin - Set를 List로 변환
- Kotlin - 문자열에서 숫자(int)만 추출하는 방법
- Kotlin - Map을 List로 변환하는 방법
- Kotlin - File, Directory가 존재하는지 확인
- Kotlin - List를 Map으로 변환
- Kotlin - List의 중복 요소 제거
- Kotlin - List를 Array로 변환
- Kotlin - 엘비스 연산자 (Elvis Operation)
- Kotlin - Array를 List로 변환
- Kotlin - String을 Float으로 변환
- Kotlin - String을 Double으로 변환
- Kotlin - String을 Int로 변환
- Kotlin - String을 Long으로 변환
- Kotlin - String Null 또는 Empty 체크