본문 바로가기

Study/Kotlin

Kotlin / 제네릭 알아보기

반응형

1. 제네릭 알아보기

제네릭은 타입 매개변수를 지정해서 임의로 작성하고 호출할 때 타입 인자를 전달해서 처리하는 방식이다.

1. 제네릭 처리 기준

자료형을 특정 문자로 지정 -> 타입 매개변수와 타입 인자로 사용

  • 타입 매개변수(Type Parameter) : 클래스나 함수의 자료형을 임의의 문자로 지정해서 컴파일 타임에 자료형 점검을 할 때 사용
  • 타입 인자(Type Argument) : 객체 생성, 함수 호출할 때 실제 자료형을 지정해서 정해진 임의의 타입을 실제 타입으로 변경

제네릭 구성 가능 여부

  • 함수, 클래스, 추상 클래스, 인터페이스, 확장함수, 확장 속성
  • 하나의 객체만 만드는 object 정의, 동반 객체, object 표현식은 제네릭을 구성할 수 없다.

타입 매개변수와 타입 인자를 지정하는 위치

  • 타입 매개변수는 <> 괄호 안에 하나 이상 정의할 수 있다. 보통 대문자 T, R, P, U를 사용한다.
  • 함수, 확장함수, 확장속성은 fun, val/var 다음에 <> 괄호를 사용하여 타입 매개변수 작성한다.
  • 클래스, 인터페이스, 추상 클래스는 이름 다음에 <> 괄호를 사용하여 타입 매개변수 작성한다.

2. 제네릭 함수

함수의 매개변수와 반환자료형의 타입을 일반 문자로 지정해서 정의할 수 있다. 

이런 일반화한 함수를 제네릭 함수라고 한다.

 

fun <타입> 함수명(매개변수1: 타입, 매개변수2: 타입): String { // 제네릭 함수 정의
    return "매개변수1 = $매개변수1, 매개변수2 = $매개변수2" // 반환값 처리
}

fun <T> add1(x: T, y: T, op: (T, T) -> T): T = op(x, y)

println(함수명<String>("이은정", "은정")) // 매개변수1 = 이은정, 매개변수2 = 은정
println(add1<Int>(10, 10) { x, y -> x + y }) // 20

 

타입 매개변수의 매개변수와 반환 자료형 분리

제네릭 함수를 정의하다 보면 입력과 반환하는 결과가 다른 자료형일 때가 많다.

그래서 반환하는 자료형을 분리해서 타입 매개변수를 지정한다.

fun <T, R> sum(x: T, y: T, op: (T, T) -> R): R { // 두 개의 타입 매개변수 중 하나는 매개변수
    return op(x, y) // 하나는 반환 값 처리
}

println(sum(100, 200) { x, y -> x + y }) // 300

 

타입 매개변수에 특정 자료형을 제한하기

콜론을 붙인 후에 특정 자료형을 적으면 제네릭 함수의 타입 매개변수에 특정 자료형을 처리할 수 있도록 제한할 수 있다.

이 타입 매개변수는 지정된 자료형과 그 하위 자료형만 타입 인자로 처리할 수 있다.

fun <T : Number> sumA(x: T, y: T, action: (T, T) -> T): T { // 숫자 자료형만 처리 가능
    return action(x, y)
}

fun <T> suffix(str: T) where T : CharSequence, T : Appendable { // 문자 시퀀스와 추가가 가능
    str.append("코틀린") // 추가 메서드 처리
}

println(sumA(100, 200) { x, y -> x + y }) // 300
println(sumA(100.1, 200.1) { x, y -> x + y }) // 300.2
// println(sumA("봄","여름"){x,y->x+y}) // 자료형 제한으로 오류

var name = StringBuilder("사랑하자!!") // 갱신 가능한 문자열 빌더 객체 만들기
suffix(name) // 함수 호출해서 문자열 추가
println(name) // 사랑하자!!코틀린

 

3. 제네릭 확장함수와 제네릭 확장속성

확장함수 작성도 일반화해서 제네릭 함수로 정의한다.

 

타입 매개변수를 사용해서 확장함수 만들기

실제 클래스 이름 대신 타입 매개변수의 이름으로 리시버를 지정하고 함수의 매개변수나 반환 자료형을 타입 매개변수로 지정한다.

fun <T> T.map(block: (T) -> T): T { // 함수 표현식으로 내부 계산
    return block(this) // 숫자 자료형일 경우는 this가 숫자값
}

println(11.map { it * it }) // 121

4. 제네릭 클래스

클래스도 속성과 메서드의 자료형을 일반화하면 다양한 클래스로 객체를 생성하는 효과가 있다.

지정한 타입 매개변수는 속성의 자료형이나 메서드의 매개변수 또는 반환 자료형에 사용한다.

 

class Company(text: String) { // 일반 클래스 정의. 주 생성자는 매개변수 처리
    var x = text // 속성 정의

    init {
        println("초기화 => $x")
    }
}

class Company1<T>(text: T) { // 제네릭 클래스 정의와 매개변수 타입 지정
    var x = text

    init {
        println("초기화 => $x")
    }
}

val com: Company = Company("인공지능") // 초기화 => 인공지능
val com1 = Company1<Int>(12) // 초기화 => 12

5. 제네릭 인터페이스

여러 일반 인터페이스를 일반화해서 제네릭 인터페이스로 정의할 수 있다.

interface Animalable<T> { // 제네릭 인터페이스 정의 : 타입 매개변수
    val obj: T // 추상 속성과 추상 메서드에 타입 매개변수
    fun func(): T
}

class Dog {
    // 일반 클래스 정의
    fun bark() = "멍멍"
}

class AnimalImpl<T>(override val obj: T) : Animalable<T> { // 제네릭 클래스에서 제네릭 인터페이스 상속
    override fun func(): T = obj
}

val aimp = AnimalImpl("코끼리") // 문자열 전달 : 타입 추론으로 타입 인자 처리
println(aimp.func()) // 코끼리
val aimpdog = AnimalImpl(Dog())
println(aimpdog.func().bark()) // 멍멍

 

 

2. 변성 알아보기

1. 변성

제네릭으로 정의하면 해당 제네릭에 매칭되는 자료형만 대체되어 처리된다.

일반적인 자료형을 지정하듯이 상속관계까지 처리되려면 변성(Variance)을 지정해야 한다.

2. 공변성

제네릭 함수나 클래스 등의 타입 매개변수를 상속관계나 생산자 즉 데이터 변경 없이 처리하는 방식

out 애노테이션을 반드시 붙여야 한다.

class MyClass1<out T>

var x: MyClass1<Any> = MyClass1<Int>()
println(x.hashCode()) // 1324119927

3. 반공변성

타입 매개변수에 In을 지정해서 소비자로만 처리하는 방식이다.

이 방식은 공변성과 반대로 처리되어 데이터가 변경될 수 있다.

open class Animal1 // 슈퍼클래스 정의
class Dog1 : Animal1() // 서브클래스가 슈퍼클래스 상속

class Container<in T> // 반공변성을 가지는 클래스 정의

var a: Container<Dog1> = Container<Animal1>() // 하위 타입에 상위 타입 할당
println(Dog::class.supertypes) // 하위타입일 경우 슈퍼타입을 확인할 수 있다
println(a.javaClass.kotlin)

 

3. 리플렉션 알아보기

함수 참조나 멤버를 알아볼 때 JVM 내부의 정보를 가져와서 처리했다.

이런 정보를 가져와 처리할 수 있도록 지원하는 도구가 리플렉션(reflection)이다.

 

함수와 생성자 참조

함수와 클래스를 정의해서 이를 메모리에 로딩한 후에 함수와 생성자를 참조할 때는 참조연산자 다음에 함수명과 클래스명으로 참조한다.

fun add2(x: Int, y: Int): Int = x + y

val addF = ::add2 // 함수 참조와 변수 할당
println(addF(10, 20))

class Foo(val bar: String)

val con = (::Foo)("생성자 참조") // 생성자 참조와 객체 생성
println(con.bar) // 생성자 참조

 

object 정의 참조

싱글턴 객체를 만드는 object 정의도 메모리에 있는 것을 바로 참조할 수 있다.

object A1 {
    const val CONST = 1000
    val a = 100
    fun getFull(): Int = a
}

val oa1 = A1::a
val oaCon = A1::CONST
val oa2 = A1::getFull

println(oa1.get()) // 30
println(oaCon.get()) // 100
println(oa2) // 1000

 

4. 애노테이션 알아보기

프로그램 코드에 특정 주석을 부여해서 개발 툴이나 JVM 등에 정보를 추가하는 것을 애노테이션(annotation)이라고 한다.

@+애노테이션 이름을 붙여서 사용한다.

 

사용 경고 애노테이션 

특정 함수나 클래스 등이 앞으로 중단되어 사용하지 못할 경우 deprecated 애노테이션을 사용

@Deprecated("USe removeAt(index) insted")// 경고 처리
class ABC {
    var field1 = ""
    var field2 = 0
    fun function1() {}
    fun function2() {}
}

val a = ABC()