모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다.
제네릭(Generic)
: 클래스 내부에서 사용할 자료형을 인스턴스를 생성할 때 확정하는 것
자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검색해 적당한 자료형을 선택할 수 있도록 함
→ 객체 자료형의 안정성을 높이고, 형 변환의 번거로움이 줄어듦
제네릭의 사용 방법
- 앵글 브래킷(<>) 사이에 형식 매개변수(하나 이상)를 넣어 선언
- 형식 매개변수 : 자료형을 대표하는 T와 같이 특정 영문의 대문자로 사용하며 나중에 필요한 자료형으로 대체됨
- 일종의 규칙처럼 사용되는 이름 (변경가능)
- E (Element), K(Key), N(Number), T(Type), V(Value), S(두번째), U(세번째), V(네번째)...
class Box<T>(t: T) {
var name = t
}
fun main() {
val box1: Box<Int> = Box<Int>(1)
val box2: Box<String> = Box<String>("Hello")
val box3 = Box(3)
println(box1.name)
println(box2.name)
}
- 객체를 생성할 때 자료형이 결정됨
- 객체 생성 시, 생성자에서 유추될 수 있는 자료형이 있다면 <Int>나 <String>은 생략 가능
[ 제네릭 클래스 ]
: 형식 매개변수를 1개 이상 받는 클래스
위의 예시에서 Box<T> 도 제네릭 클래스
// 불가능
class MyClass<T> {
var myProp: T
}
// 가능
class MyClass<T>(val myProp: T) { } // 주 생성자
class MyClass<T> {
val myProp: T
constructor(myProp: T) { // 부 생성자
this.myProp = myProp
}
}
- 클래스 프로퍼티에 사용하는 경우 클래스 내부에서는 사용할 수 없음 → 자료형이 특정되지 못하기 때문에 인스턴스 생성 X
- 주 생성자나 부 생성자에 형식 매개변수를 지정하여 사용 가능
- 객체 인스턴스를 생성할 때 명시적으로 자료형 지정 가능
[ 제네릭 함수 / 메서드 ]
: 형식 매개변수를 받는 함수나 메서드
함수가 호출될 때 컴파일러가 자료형을 추론할 수 있고, 이 자료형은 반환 자료형과 매개변수 자료형에 사용할 수 있음
fun <형식 매개변수[, ...]> 함수 이름(매개변수: <매개변수 자료형>[, ...]): <반환 자료형>
fun <T> find(a: Array<T>, Target: T): Int {
for(i in a.indices) {
if (a[i] == Target) return i
}
return -1
}
fun main() {
val arr1: Array<String> = arrayOf("Apple", "Banana", "Cherry")
val arr2: Array<Int> = arrayOf(1, 2, 3, 4)
println("arr.indeces ${arr1.indices}")
println(find<String>(arr1, "Cherry"))
println(find(arr2, 2)) // 자료형이 특정되어 있는 경우, 생략 가능
}
[ 제네릭과 람다식 ]
형식 매개변수로 선언된 함수의 매개변수를 연산할 때는 자료형을 결정할 수 없기 때문에 오류가 발생함
→ 자료형을 나중에 넘겨서 결정하는 방식을 사용 : 람다식
→ 람다식을 매개변수로 받으면 자료형을 결정하지 않아도 실행 시 람다식 본문을 넘겨줄 때 결정되므로 문제 해결 가능
fun <T> add(a: T, b: T, op: (T, T) -> T): T {
return op(a, b)
}
fun main() {
val result = add(2, 3, {a, b -> a+b})
println(result)
var sumInt: (Int, Int) -> Int = {a, b -> a+b}
var sumInt2 = {a: Int, b: Int -> a+b}
println(add(2, 3, sumInt))
println(add(2, 3, sumInt2))
}
- 람다식 {a, b -> a+b}은 add() 함수가 실행될 때 넘겨지는 인자
- 연산식을 함수 선언부에 직접 구현하지 않고 전달하는 방법
- 함수의 형식 매개변수의 자료형을 특정하지 않아도 사용 가능
자료형 제한하기
제네릭 클래스나 메서드가 받는 형식 매개변수를 특정한 자료형으로 제한할 수 있음
자바에서는 extends나 super를 사용해 자료형을 제한했지만, 코틀린에서는 콜론(:)을 사용해 제한함
[ 형식 매개변수의 null 제어 ]
제네릭의 형식 매개변수는 기본적으로 null이 가능한 형태로 선언됨
class GenericNull<T> {
fun EqulityFunc(arg1: T, arg2: T) {
println(arg1?.equals(arg2))
}
}
fun main() {
val obj = GenericNull<String>() // non-null
obj.EqulityFunc("Hello", "World")
val obj2 = GenericNull<Int?>() // null이 가능한 형식
obj2.EqulityFunc(null, 10)
}
- GenericNull<Int?> : null을 허용하도록 자료형에 ? 기호를 사용함
- obj2.EqualityFunc(null, 10) : null을 사용하고 있으므로 코드에서 안전한 작업을 위해 ?.를 사용
- null인 경우 equals()로 비교하지 않고 null을 반환함
- class GenericNull<T: Any> : null을 허용하지 않는 방법
- 형식 매개변수를 null이 아닌 Any로 제한하여 null을 지정할 수 없게 함
[ 클래스와 함수에서 형식 매개변수의 자료형 제한 ]
- 클래스 : class Calc <T: Number>
- 함수 : fun <T: Number> addLimit(a: T, b: T, op: (T, T) -> T) : T {...}
예시에서는 Number 로 자료형을 제한함.
따라서 Number형이 아닌 String과 같이 자료형이 맞지 않으면 Type argument is not within its bouds와 같은 오류가 발생함
자료형을 제한할 때, 하나가 아닌 여러 개의 조건에 맞춰 제안할 때 → where 키워드 사용
interface InterfaceA
interface InterfaceB
class HandlerA: InterfaceA, InterfaceB
class HandlerB: InterfaceB
class ClassA<T> where T:InterfaceA, T:InterfaceB
fun <T> myMax(a: T, b: T): T where T:Number, T:Comparable<T> {
return if(a>b) a else b
}
fun main() {
val obj1 = ClassA<HandlerA>()
val obj2 = ClassA<HandlerB>() // 범위에 없으므로 오류
}
상·하위 형식의 가변성
가변성(Variance) : 형식 매개변수가 클래스 계층에 영향을 주는 것
[ 클래스와 자료형 ]
String은 클래스이자 자료형이지만, String?은 자료형일 뿐 클래스는 아님
List는 클래스이자 자료형이지만, List<String>은 자료형일 뿐 클래스는 아님
보통 클래스는 파생된 하위 클래스와 상위 클래스가 존재함
ex) Int는 Number의 하위 클래스
상위 클래스는 하위 클래스를 수용할 수 있기 때문에 Int형 변수는 Number형 변수로 할당되어 형변환이 이루어짐
ex) Int는 Int?의 하위 자료형
Int(하위 자료형)는 Int?(상위 자료형)에 할당하는 것이 가능함
[ 자료형 변환 ]
제네릭 클래스는 가변성을 지정하지 않으면 형식 매개변수에 상·하위 클래스가 지정되어도 자료형이 변환되지 않음
open class Parent
class Child : Parent()
class Cup<T>
fun main() {
val obj1: Parent = Child() // Parent 형식은 Child 자료형으로 변환될 수 있음
val obj2: Child = Parent() // 반대로 변환할 수는 없음
// 자료형 불일치
val obj3: Cup<Parent> = Cup<Child>()
val obj4: Cup<Child> = Cup<Parent>()
// 자료형 일치
val obj5 = Cup<Child>()
val obj6: Cup<Child> = obj5
}
- 상위 클래스는 하위 클래스 형으로 변환될 수 있음
- 제네릭 클래스에서는 형식 매개변수인 T에 상·하위 클래스를 지정하더라도 서로 관련이 없는 형식임
- obj3와 obj4 처럼 자료형 불일치 오류가 발생하게 됨
- obj5와 obj6과 같이 같은 자료형은 문제없이 할당 가능
[ 가변성의 3가지 유형 ]
무변성(Invariance)
: c<T>와 C<T'>는 아무 관계가 없다 (생산자+소비자)
class Box<T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 자료형 불일치 오류
val nothings: Box<Nothing> = Box<Int>(20) // 자료형 불일치 오류
}
- in이나 out으로 공변성이나 반공변성을 따로 지정하지 않으면 무변성으로 선언됨
- 상하 관계를 가지고 있는 Any, Int형 자료형과 상하 관계를 가지고 있는 Int형 자료형과 Nothing
- 상하 관계를 잘 따졌어도 Box<T>가 무변성이므로 자료형 불일치 오류 발생
공변성(Covariance)
: T'가 T의 하위 자료형이면, C<T'>는 C<T>의 하위 자료형이다 (생산자 입장의 out 성질)
class Box<out T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 객체 생성 가능
val nothings: Box<Nothing> = Box<Int>(20) // 자료형 불일치 오류
}
- 형식 매개변수의 상하 자료형 관계가 성립하고, 그 관계가 그대로 인스턴스 자료형 관계로 이어지는 경우
- out 키워드 : 형식 매개변수가 공변적으로 선언되는 것
- Int가 Any의 하위 자료형일 때, 형식 매개변수 T에 대해 공변적
- Any의 하위 클래스인 Int는 공변성을 가지므로 Box<Any>에 Box<Int> 자료형을 할당할 수 있음
- <Nothing>은 <Int>의 하위 자료형이 아니므로 오류 발생
반공변성(Contravariance)
: T'가 T의 하위 자료형이면, C<T>는 C<T'>의 하위 자료형이다. (소비자 입장의 in 성질)
class Box<in T>(val size: Int)
fun main() {
val anys: Box<Any> = Box<Int>(10) // 자료형 불일치 오류
val nothings: Box<Nothing> = Box<Int>(20) // 객체 생성 가능
}
- 자료형의 상하 관계가 반대가 되어 인스턴스의 자료형이 상위 자료형이 됨
- 공변성과 반대의 경우
- Box<Nothing> 자료형의 상위 자료형이 Box<Int> 이므로 객체를 생성할 수 있음
* 람다식에 대한 정리 포스팅
'Kotlin > Kotlin 프로그래밍' 카테고리의 다른 글
[Kotlin] 컬렉션(Collection) - Set, Map (0) | 2021.05.26 |
---|---|
[Kotlin] 컬렉션(Collection) - List (0) | 2021.05.24 |
[Kotlin] 문자열(String) (0) | 2021.05.21 |
[Kotlin] 배열(Array) (0) | 2021.05.18 |
[Kotlin] 다양한 클래스와 인터페이스 (3) (0) | 2021.05.13 |
[Kotlin] 다양한 클래스와 인터페이스 (2) (0) | 2021.05.10 |
[Kotlin] 다양한 클래스와 인터페이스 (1) (0) | 2021.04.21 |
[Kotlin] 프로퍼티와 초기화 (0) | 2021.04.20 |
댓글