개발 언어/코틀린
코틀린(Kotlin) | 제네릭 (Generic)
jjiiiinn
2019. 6. 23. 21:57
728x90
제네릭
- 클래스나 메서드 내부에서 사용할 타입을 외부에서 지정하는 방법이다.
- 원하는 자료형이 입력되지 않았을때 컴파일 타임에 에러를 발생시켜 안정성을 높일수 있다.
자바에서의 제네릭
코틀린의 제네릭을 알아보기 전에 자바에서의 제네릭 사용법부터 알아보자.
제네릭 선언
// 제네릭 클래스
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]
}
}
코틀린에서의 제네릭
제네릭 선언
- 선언 방식은 자바와 거의 동일하다.
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))
}
제너릭은 공부할수록 더 어려운거 같다.
계속 공부해야지 하고 미루던것을 이번 블로그 정리하면서 다시 공부하였다.
정리하고도 의미파악을 잘 못한 부분도 있어서 내가 잘못 알고 적은부분도 있을것이다.
혹시라도 내가 잘못 알고 있는 부분이 있다면 지적 부탁드립니다.
출처
728x90