안드로이드

[Android] 제네릭(Generic)에 대해 알아 보자

코딩하는후운 2022. 8. 24. 19:27
반응형

[Android] 제네릭(Generic)에 대해 알아 보자

제네릭 이란?

데이터의 타입을 일반화한다는 것을 의미합니다.

제네릭을 통해 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 타임에 미리 지정하는 방법입니다.

 

제네릭 장점

  1. 객체의 타입 안정성 증가
  2. 반환값에 대한 타입 변환 및 타입 검사 x
  3. 모든 객체에 대한 확장성
  4. 런타임 환경에 아무런 영향이 없는 컴파일 시점의 전처리 기술이다

 

기존의 방식

JDK 1.5 이전에서는 여러 타입을 사용하는 클래스나 메소드에서 인수나 반환값으로 최상위 객체인 Object 타입 을 사용했습니다.

: 원하는 타입으로 변환해야 하며, 오류가 발생할 가능성도 존재

 

제네릭 도입

JDK 1.5부터 도입된 제네릭

컴파일 시에 미리 타입이 정해지므로, 안정성이 증가하고 타입 변환과 검사를 할 필요가 없어집니다.

 

 

Type parameter의 관례

Type Parameter로 사용하는 변수들은 보통 아래와 같이 많이 사용한다.

그러나 꼭 한 글자가 아니어도 되고, 소문자가 섞여도 무방하다.

또한 타입 파라미터의 개수도 하나 이상이 될 수 있다.

 

[JAVA] 제네릭

List<Interger> list1 = new ArrayList<>();
List list2 = new ArrayList<>();
Map<String, String> map = new ArrayList<>();

위와같이 꺽쇠안에 클래스 타입이 명시된 패턴을 자주 발견할 수 있다.

이걸 제네릭(Generic) 이라고 부르며, 제네릭 파라미터는 꺽쇠안에 포함하여 전달한다.

 

 

제네릭 파라미터를 전달받는 클래스 정의

 

Sample클래스를 초기화시키면서 임의의 클래스를 제네릭 파라미터로 전달하여 Sample클래스의 T에 대한 타입을 지정

 

이처럼 제네릭 타입으로 어떤 클래스를 전달했냐에 따라서 메소드의 파라미터, 혹은 리턴타입이 제네릭 파라미터로 전달받은 클래스 타입으로 유연하게 바뀌며 동시에 강제성을 갖게 해주는 부분을 확인할 수 있다.

제네릭 파라미터에 의해 타입이 고정되기 때문에 안정성이 확보되는 것을 확인 할 수 있다.

 

제네릭의 특징 및 사용법

  • 클래스 혹은 메소드에 선언할 수 있다.
  • 동시에 여러 타입을 선언할 수 있다.
  • 와일드 카드를 이용하여 타입에 대하여 유연한 처리를 가능케 한다.
  • 제네릭 선언 및 정의시에 타입의 상속관계를 지정할 수 있다.

클래스 혹은 메소드에 선언할 수 있다.

와일드 카드 : 와일드카드(wildcard)는 여러 문자열을 상징할 수 있는 ‘*’, ‘?’ 등의 단일 문자를 가리킨다.

 

 

제네릭 2가지 선언 방법

  1. 클래스에 제네릭 파라미터를 선언하는 방법
    - 컬렉션에서 자주볼수 있는 유형

    2. 메소드에 제네릭 파라미터 선언하는 방법
        - 제네릭 타입이 메소드 호출시점에 결정되야 할 경우 사용 (다이나믹한 처리를 가능)

 

동시에 여러 타입을 선언

제네릭 파라미터를 정의하는곳에 콤마를 기준으로 여러 타입을 선언하여 사용이 가능

 

 

와일드 카드(Wild Card)는 대입연산 수행시 유연한 처리를 돕는다.

JAVA 컴파일러는 대입연산을 수행할 때 left-value의 제네릭타입과 right-value의 제네릭타입이 정확하게 일치하지 않을 경우 컴파일 에러를 발생시킨다. (불변성)

하지만, 와일드 카드를 사용한다면 컴파일러가 유연하게 대처하도록 할 수 있다.

 

method2처럼 와일드 카드로 모든 타입에 대하여 허용하게 될 경우 최상위 클래스인 Object로 정의 된다.

내부에서 특정 타입으로 캐스팅하여야 된다는 단점이 존재

그래서 제네릭 파라미터 대입 연산시 left-value와 right-value간의 캐스팅이 가능하도록 super와 extends라는 키워드로 지원하고 있다. (공변/반공변)

제네릭 선언 및 정의시에 타입의 상속관계를 지정할 수 있다.

 

1. 제네릭 타입 정의시 상속관계를 명시하는 방법 (와일드카드를 사용한다)

 

2. 제네릭 타입 선언시 상속관계를 명시하는 방법

 

 

[Kotlin] 제네릭(Generic)

제네릭 함수 정의

Generic 함수를 정의할 때, 타입이 정해지지 않은 변수는 함수 이름 앞에 <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
}

 

제네릭 클래스 정의

Generic 클래스를 정의할 때 타입이 정해지지 않은 변수는 클래스 이름 다음에와 같이 정의합니다.

class Rectangle<T>(val width: T, val height: T) {
}
fun main(args: Array<String>) {
    val rec = Rectangle<Double>(10, 20)
    val rec1 = Rectangle<String>("aa", "bb")
}

코틀린은 전달된 인자로 부터 T의 타입을 추론하기 때문에 아래와 같이 Rectangle만 써줘도 됩니다.

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(제한, 제약)

숫자가 아닌 인자도 허용이 되기 때문에, <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
    }
}

 

2개 이상의 Constraints

T는 Number를 상속받고, Comparable을 구현한 객체로 제한하였습니다. 두가지 이상의 제약을 걸려면 아래 처럼 where를 사용해야 합니다.

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입니다.

하지만 Rectangle<Dobule>의 Super class는 Rectangle<Number>가 아닙니다.

두개의 타입이 서로 상속 관계이지만, Generic 클래스의 상속 관계는 아니라는 것을 Invariance(불변성)라고 합니다.

코틀린에서는 Generic의 모든 타입은 Invariance입니다.

 

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가 되도록 합니다.

에러

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에서는 반대 방향의 상위/하위 클래스 구조를 갖는다는 것을 정의합니다.

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
}

 

자바와 코틀린의 공변/반공변 차이

ArrayList에 정의된 addAll() 메서드와 sort() 메서드이다.

public boolean addAll(Collection<? extends E> c)
public void sort(Comparator<? super E> c)

 

PECS(Producer-Extends Consumer-Super) 규칙에 의해

producer 인 Collection 에는 extends 가(공변),

consumer 인 Comparator 에는 super 가(반공변) 적용되어있다.

 

이렇게 사용하는 곳에서 공변/반공변을 지정하는걸 use-site variance 라고 표현한다.

자바는 언어 문법차원에서 use-site variance 만 지원하고있다.

 

이에 반해 코틀린은 declaration-site variance 문법을 지원하고있다.

이는 선언할때 공변/반공변을 지정한다는 내용이다.

 

 

 

차이점이 있다면,

코틀린에서는 T에 대한 반변 선언을 클래스 선언시에 했다는 점과,

자바는 T를 사용하려고 할 때 반변 선언을 한다는 것이다.

 

코틀린에서는 이 방식을 Declaration-Site Variance (선언 위치 변환) 이라 부르고,

자바는 이 방식을 Use-Site Variance (사용 위치 변환) 이라 부른다.

 

더 자세한 설명은 Declaration-site and use-site variance explained 글을 참고하면 된다.

 

이 두 개는 어떻게 보면 큰 차이점을 보이고 있는데,

코틀린에서는 한 번만 선언하면 그 클래스 안에서는 두번 다시 선언할 필요가 없다.

하지만 자바는 한 번에 선언하지 못하고 사용할 때 마다 계속 선언해야 한다.

그럼 코틀린에서는 자바와 같은 방식을 사용하지 못하는가? 대답은 No라고함.

 

 

참조 :

https://jehuipark.github.io/java/java-generic

https://readystory.tistory.com/201

https://meoru-tech.tistory.com/35

https://codechacha.com/ko/generics-class-function-in-kotlin/

https://multifrontgarden.tistory.com/263

https://medium.com/depayse/kotlin-제네릭-generic-1-정의-타입-파라미터-제약-3bc8cf759cb3

https://velog.io/@windsekirun/Kotlin-Generics-3-Declaration-Site-Use-Site-variance

 

반응형