티스토리 뷰

제네릭

  • 클래스나 메서드 내부에서 사용할 타입을 외부에서 지정하는 방법이다.
  • 원하는 자료형이 입력되지 않았을때 컴파일 타임에 에러를 발생시켜 안정성을 높일수 있다.

자바에서의 제네릭

코틀린의 제네릭을 알아보기 전에 자바에서의 제네릭 사용법부터 알아보자.

제네릭 선언

// 제네릭 클래스
public class GenericJava<T> {
    private T value;

    // 제네릭 메서드
    public static <T> void genericMethod(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        // String으로 지정
        GenericJava<String> genericJava = new GenericJava<>();
        genericJava.value = "string";
        // genericJava.value = 31; // error

        GenericJava.genericMethod("string");
    }
}

가변성

종류 의미
공변성 T’가 T의 서브타입이면, C<T’>는 C<T>의 서브타입이다.
구체적인 방향으로 타입 변환을 허용하는 것
반공변성 T’가 T의 서브타입이면, C<T>는 C<T’>의 서브타입이다.
추상적인 방향으로의 타입 변환을 허용하는 것
무변셩 C<T>와 C<T’>는 아무 관계가 없다.

 

자바의 제네릭의 경우 기본적으로 무변성을 가진다.

만약 자바의 제네릭이 무변성이 아닐경우 아래와 같은 상황이 나타날수 있다.

public static void main(String[] args) {
  List<String> strs = new ArrayList<String>();
  List<Object> objs = strs; // String은 Object의 하위타입
  objs.add(31); // List<Object>이므로 모든 객체를 저장할 수 있다.
  String s = strs.get(0); // !!! ClassCastException: Integer를 String으로 변환할 수 없다
}

무변성이 아니면 위와 같이 사용하게 될것이고 제네릭 사용(안정성 측면)의 의미가 없어진다.

하지만 무변성 때문에 아래와 같은 문제점도 발생한다.

interface GenericCollection<T> {
	void addAll(GenericCollection<T> items);
}

public static void copyAll(GenericCollection<Object> to, GenericCollection<String> from) {
	// GenericCollection<String>은 GenericCollection<Object>의 하위타입이 아니다.
	to.addAll(from); // error
}

개념적으로는 String은 Object의 하위 타입이므로 GenericCollection<Object>에 GenericCollection<String>을 넣는것은 당연하다 생각되지만 위의 무변성때문에 하위타입으로 간주되지 않아 위의 코드는 컴파일시에 에러가 발생한다.

java: incompatible types: GenericJava.GenericCollection<java.lang.String> cannot be converted to GenericJava.GenericCollection<java.lang.Object>

와일드 카드

  • ?를 사용하며 제한자를 사용하지않고 와일드 카드를 사용하면 어떤 객체든 사용 할 수있다.
  • 제너릭으로 넘어오는 타입에 상관없이 사용하고 싶을때 쓴다.
  • extends와 super를 사용하여 제한을 걸수있다.

서브타입 와일드 카드

공변성: T’가 T의 서브타입이면, C<T’>는 C<T>의 서브타입이다.
  • 서브타입 와일드 카드를 사용하면 공변성을 나타낸다.
  • extends키워드로 나타내며 특정 클래스의 하위 클래스로 제한한다.
  • 최소한 특정 클래스를 부모로 가지므로 데이터를 가져올 수 있다.
  • 하지만 하위 클래스 중 어떤 클래스인지 알수는 없기 때문에 데이터를 입력 할수는 없다.
import java.util.ArrayList;
import java.util.List;

public class GenericJava {

    public static String join(List<? extends CharSequence> charList, String joiner) {
        //charList.add("empty string"); // error!!

        StringBuilder sb = new StringBuilder();
        for (CharSequence charSequence : charList) {
            sb.append(charSequence).append(joiner);
        }

        return sb.toString();
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("park");
        stringList.add("jin");

        System.out.println(join(stringList, "~~")); // park~~jin~~

        // 좀 억지이긴 하지만....
        List<StringBuffer> stringBuffers = new ArrayList<>();
        stringBuffers.add(new StringBuffer("park"));
        stringBuffers.add(new StringBuffer("jin"));

        System.out.println(join(stringBuffers, "!!")); // park!!jin!!
    }
}

슈퍼타입 와일드 카드

반공변성: T’가 T의 서브타입이면, C<T>는 C<T’>의 서브타입이다.
  • 슈퍼타입 와일드 카드로는 반공변성을 나타낸다.
  • super키워드로 나타내며 특정 클래스의 상위 클래스로 제한한다.
  • 특정 클래스의 상위 클래스이므로 데이터를 입력할 수는 있다.
  • 하지만 상위 클래스 중 어떤 클래스인지 특정 지을 수 없기때문에 데이터를 가져올수는 없다. (Object로 가져올 수는 있음.)
import java.util.ArrayList;
import java.util.List;

public class GenericJava {
    public static <T> void addAll(List<? super T> to, List<? extends T> from) {
        for (T value : from) {
            to.add(value);
        }
    }

    public static void main(String[] args) {
        List<String> to = new ArrayList<>();

        List<String> from = new ArrayList<>();
        from.add("park");
        from.add("jin");

        addAll(to, from);

        System.out.println(to); // [park, jin]
    }
}

출처: https://blog.naver.com/qkrghdud0/220697126377

 


코틀린에서의 제네릭

제네릭 선언

  • 선언 방식은 자바와 거의 동일하다.
class GenericClass<T>(val t: T) {
    
}


fun <T> genericFunction(t: T) {
    
}

공변(out) - ? extends T

fun <T: CharSequence> join(list: List<out T>, joiner: String): String {
    val sb = StringBuilder()
    for (c: CharSequence in list) {
        sb.append(c).append(joiner)
    }

    return sb.toString()
}

fun main() {
    val strings = listOf("park", "jin")
    val result1 = join(strings, "~~")
    println(result1) // park~~jin~~

    val buffers = listOf(StringBuffer("park"), StringBuffer("jin"))
    val result2 = join(buffers, "!!")
    println(result2) // park!!jin!!
}

반공변(in) - ? super T

fun <T: Any> addAll(from: MutableList<out T>, to: MutableList<in T>) {
    for (e in from) {
        to.add(e)
    }
}

fun main() {
    val from = mutableListOf("park", "jin")
    val to = mutableListOf<String>()

    addAll(from, to)

    println(to) // [park, jin]
}

 

클래스 제네릭에 in/out을 주어 해당 제너릭의 소비/생성 위치를 제한할 수 있다.

interface Generic<out T> {
    fun consume(t: T) // error
    fun produce(): T
}
interface Generic<in T> {
    fun consume(t: T)
    fun produce(): T // error
}

* - 스타 프로젝션(star projection)

  • 제너릭에 대한 정보가 필요 없을때 사용한다.
  • in으로 되어있는 제너릭을 *로 받으면 in Nothing으로 간주한다.
  • out으로 되어있는 제너릭을 *로 받으면 out Any?로 간주한다.
fun printAll(list: List<*>) {
    for (e in list) {
        println(e)
    }
}

fun main() {
    printAll(listOf("a", "b", "c"))
    printAll(listOf('a', 'b', 'c'))
    printAll(listOf(1, 2, 3, 4))
}

제너릭은 공부할수록 더 어려운거 같다.

계속 공부해야지 하고 미루던것을 이번 블로그 정리하면서 다시 공부하였다.

정리하고도 의미파악을 잘 못한 부분도 있어서 내가 잘못 알고 적은부분도 있을것이다.

혹시라도 내가 잘못 알고 있는 부분이 있다면 지적 부탁드립니다.


출처

댓글