Generics
안들어도 되는분
- 이펙티브자바 제네릭편을 이해한분
- 조슈아 블로크가 발표한 (역시 이펙티브자바에서도 나오는)PECS 규칙을 이해한분
- Jackson 의 ObjectMapper 에서 List<Map<String, String> 과 같은 중첩 제네릭 타입을 보존하는 방법을 알고, 이해하는 분
Generics
- JavaSE 1.5 에 추가
- Scala 창시자로 유명한 마틴오더스키가 참여한 프로젝트
- 컴파일 타임에 타입 안정성을 보장하기 위해 사용
- 대표적으로 Collection Library 에서 사용
- 제네릭 클래스, 제네릭 메서드를 직접 정의 가능
1.5 이전
List list = new ArrayList();
list.add("hello");
// get() 메서드가 Object 를 반환하므로 컴파일 에러 발생
// String str = list.get(0);- 컴파일 에러를 없애기위해 강제 형변환
List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);1.5 이전
- 강제 형변환을 했더니 타입안정성을 보장하지 않음
List list = new ArrayList();
list.add(30);
// 런타임 예외 발생!
String str = (String) list.get(0);1.5 이전
- 타입 안정성까지 보장하기 위한 코드
List list = new ArrayList();
list.add(30);
Object element = list.get(0);
if(element instanceof String){
String str = (String) element;
}1.5 Generics
List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0);- 타입 아규먼트를 이용해 컴파일 타임에 타입을 제한함으로서 타입 안정성 확보
제네릭 클래스
class MyList<T>{
private T[] ts;
private int cursor;
@SuppressWarnings("unchecked")
MyList(){
this.ts = (T[]) new Object[10];
}
void add(T t){
this.ts[cursor++] = t;
}
T get(int idx){
return this.ts[idx];
}
}제네릭 클래스
- 클래스명 뒤에 타입 파라미터를 의미하는 <T>를 붙여 제네릭 클래스를 정의
- T는 타입 파라미터로써 말 그대로 변수이기때문에 어떠한 명칭을 지어도 상관없지만 자바 관례상 대문자 알파뱃 사용
- T(Type), E(Element), K(Key), V(Value) ...
- T는 타입 정의시 전달한 타입 아규먼트에 의해 정의됨
제네릭 메서드
class MyList2{
private Object[] ts;
private int cursor;
MyList2(){
this.ts = new Object[10];
}
<T> void add(T t){
this.ts[cursor++] = t;
}
@SuppressWarnings("unchecked")
<T> T get(int idx){
return (T) this.ts[idx];
}
}제네릭 메서드
- 타입 파라미터를 클래스가 아닌 메서드에 지정
- 제네릭 클래스와 마찬가지로 사용할 수 있으며 좀 더 유연한 API를 제공할 수 있음
- List와 같이 하나의 타입을 다루는 자료구조성 라이브러리에는 어울리지않음
MyList2 myList2 = new MyList2();
myList2.add(123);
String str2 = myList2.get(0);제네릭 클래스 VS 제네릭 메서드
- 모든 인터페이스에서 한가지 타입을 다뤄야하는 자료구조성 클래스에는 제네릭 클래스가 어울림
- 유틸성 메서드에는 제네릭 메서드가 어울림
- 조슈아 블로크는 이펙티브자바에서 가능하면 제네릭 메서드로 만들것을 권장
- Effective Java 3 Item 30. 가능하면 제네릭 메서드로 만들 것
- 제네릭 클래스는 사용하는 쪽에서 타입 아규먼트를 지정하면서 제네릭 클래스를 사용하고있음을 알리지만 제네릭 메서드는 좀 더 유연히 작동하기때문에 제네릭 메서드로 만드는것을 권장함
제네릭 클래스 VS 제네릭 메서드
// 사용하는 쪽에서 명시적으로 <String> 이라는 타입 아규먼트를 지정해야함
MyList<String> myList = new MyList<String>();
myList.add("hello");
String str = myList.get(0);MyList2 myList2 = new MyList2();
// 제네릭 메서드로 구현되어있지만 사용하는 쪽에서는 제네릭이 적용되어있음을 모름
myList2.add(123);
String str2 = myList2.get(0); Java SE 1.7 Diamond Operator
- JavaSE 1.7에 추가
- 기존에는 타입 선언과 객체 생성 부분 모두 타입 아규먼트를 명시해야 했으나 1.7에 추가된 다이아몬드 연산자를 이용해 타입 선언에만 명시해주면 객체 생성부분은 컴파일러가 추론함
MyList<String> myList = new MyList<>();타입 소멸(Type Erasure)
- 제네릭 정보는 컴파일 시에만 유효하며 바이트 코드에 제네릭 정보는 남지않음.
- 컴파일러가 자동으로 형변환 코드를 넣어주는 Syntax Sugar 정도의 역할만 할뿐
- C#은 컴파일 이후에도 제네릭 정보가 남아있게 구현했지만 자바는 그렇지 못함
- 하위 호환성을 유지하기위한 선택
- 자바의 제네릭을 사용할때 겪는 많은 문제들은 대부분 타입소멸로 인한것들이 많음
- 가끔 분명히 제네릭으로 타입을 보장했는데 안에 이상한 타입이 들어있는 경우가 발생(MyBatis)
타입 소멸(Type Erasure)
Test.MyList var1 = new Test.MyList();
var1.add("hello");
String var2 = (String)var1.get(0);class MyList<T> {
private T[] ts = (Object[])(new Object[10]);
private int cursor;
MyList() {
}
void add(T var1) {
this.ts[this.cursor++] = var1;
}
T get(int var1) {
return this.ts[var1];
}
} 타입 소멸(Type Erasure)
public static <T> void method(){
T t1 = new T();
T t2 = T.newInstance();
T[] ts = new T[10];
} - 그래서 자바 제네릭은 이런게 안됨
public static <T> void method(Class<T> tClass) throws IllegalAccessException, InstantiationException {
T t = tClass.newInstance();
}- 타입정보를 명시적으로 전달해줘야함
공변/불공변/반공변
- 공변(Covariant), 불공변(Invariant), 반공변(ContraVariant)
- Invariant 는 이펙티브자바2판에서는 불변으로 번역되었으나, (아마도) Immutable과 용어가 겹쳐 3판에서는 불공변으로 변역. 동일한 주제를 일부 다루는 코틀린 인 액션은 무공변으로 변역했으나 이 발표자료는 이펙티브자바 3판을 따르기로 함
- Immutable이 직접적인 값/상태의 불변을 표현한다면 Invariant는 성질/타입의 불변을 표현
공변(Covariant)
- 배열은 대표적인 공변 타입
Object[] objects = new String[10];- String은 Object의 하위 타입이므로 String[]이 Object[]의 하위 타입으로 함께 변함(공변)
- 배열의 공변성으로 인해 발생하는 문제
Object[] objects = new String[10];
objects[0] = 10;
불공변(Invariant)/반공변(ContraVariant)
- 제네릭은 기본적으로 불공변, 제네릭에 들어오는 타입에 따라 리스트의 타입이 변하지않음.
// Compile Error
ArrayList<Object> objects = new ArrayList<String>();- ArrayList<Object>와 ArrayList<String>은 상하위 개념이 아니라 그냥 다른 타입
모든 타입을 받는 제네릭 클래스
- List를 파라미터로 받고싶다. 타입 파라미터는 어떤것이든 상관없다. 이 요구사항을 충족하는 메서드 시그니처는 어떻게 작성하면 될까?
public static void method(Object[] objects){
// 배열은 이런식으로 받아주면 된다.
}public static void method(List<Object> list){
// 리스트도 이런식으로 받아주면 될까?
}
// 무변의 특성을 가진 제네릭 클래스이기때문에 컴파일 에러 발생
method(new ArrayList<String>());
method(new ArrayList<Integer>());모든 타입을 받는 제네릭 클래스
- Raw 타입을 사용하면 가능
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
public static void method(List list){
} 모든 타입을 받는 제네릭 클래스
- 1.5에 제네릭이 추가되면서 자바의 많은 인터페이스에 변경이 발생함(Collection, Class 등)
- 인터페이스 그 자체가 변경됐음에도 Raw 타입을 지원하는 이유는 하위호환을 위한 것
- 1.5 이상이라면 Raw 타입을 사용하는것은 권장하지않음
- 1.5 이상에서 Raw 타입 사용시 컴파일러는 warnning 발생
- 그럼 이런 요구사항은 어떻게...?
- Effective Java 3 Item 26. 로 타입은 사용하지 말라
와일드카드(Wild Card)
- 이럴때 사용할 수 있게 와일드 카드 지원
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
public static void method(List<?> list){
} - 타입 파라미터가 뭐가 지정됐든 상관없을때 사용 가능
One more thing
- 사실 제네릭 메서드를 이용해 해결 가능
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
public static <T> void method(List<T> list){
} 타입 파라미터 VS 와일드카드
- 대부분의 경우 와일드 카드를 사용해야 하는 경우 제네릭 메서드를 이용해서도 해결이 가능
- 와일드 카드와 타입 파라미터의 가장 큰 차이는 타입 캡쳐
- 와일드 카드는 타입을 캡쳐하지않음
- 메서드 내에서 해당 제네릭 타입을 사용하거나 반환타입을 지정해야한다면 타입 파라미터, 사용하지 않는다면 와일드 카드 사용 권장
타입 파라미터 VS 와일드카드
public static <T> T get(List<T> list, int idx){
return list.get(idx);
}- 와일드카드는 타입을 캡쳐하지 않으므로 반환값으로 사용할 수가 없음
타입 파라미터 VS 와일드카드
public static boolean contains(List<?> list, Object object){
return list.contains(object);
}- 이 메서드의 경우 메서드 내에서 제네릭 정보를 사용하는 곳이 전혀 없고, 반환값도 제네릭과 상관없으므로 와일드카드를 이용해 구현 가능
- 다만 마찬가지로 타입 파라미터를 이용해서 구현해도 동일하게 작동함
- 어디까지나 관례(Convention)이며 메서드 시그니처만으로 이 메서드의 구현부를 추론할때 사용
- '타입파라미터로 되어있으니 이 메서드는 타입정보를 사용하는 구나', '와일드카드로 되어있으니 타입 정보를 사용하지 않는구나'
타입 파라미터 VS 와일드카드
public static void reverse(List<?> list){
int last = list.size() - 1;
for(int i = 0, length = list.size() / 2; i <= length; i++){
Object element = list.get(i);
list.set(i, list.get(last - i));
list.set(last - i, element);
}
} - List의 순서를 바꾸는 메서드
- 안에 들어있는 요소의 타입은 중요하지않기때문에 와일드카드 사용
- 코드를 돌려보면 알겠지만 컴파일에러 발생
- 와일드카드는 타입을 캡쳐하지않기때문에 이런코드에 대응하지못함
타입 파라미터 VS 와일드카드
public static void reverse(List<?> list){
helper(list);
}
private static <T> void helper(List<T> list){
int last = list.size() - 1;
for(int i = 0, length = list.size() / 2; i <= length; i++){
T element = list.get(i);
list.set(i, list.get(last - i));
list.set(last - i, element);
}
} - 마찬가지로 이런 경우 제네릭 메서드로 만들면 해결이 가능하지만 내부 헬퍼 메서드를 이용해서 해결하기를 권장함
타입 파라미터 VS 와일드카드
- 애초에 제네릭 메서드로 만들면 헬퍼 메서드없이 해결이 가능하지만 저렇게 하는 이유는 메서드 시그니처만으로 메서드를 알리기위함임. 공개된 메서드의 시그니처가 제네릭 메서드라면 메서드내부에서 타입을 필요로한다는 의미가 됨
- 조슈아 블로크는 이펙티브자바에서 가능하면 제네릭 클래스로 만들고, 더 가능하면 제네릭 메서드로 만들고, 더 가능하면 와일드카드를 이용하기를 권장함
- Item 29. 이왕이면 제네릭 타입으로 만들라
- Item 30. 이왕이면 제네릭 메서드로 만들라
- Item 31. 한정적 와일드 카드를 사용해 API 유연성을 높이라
한정적 타입 파라미터
class ToyList<T>{
private T[] ts;
private int cursor;
@SuppressWarnings("unchecked")
ToyList(){
this.ts = (T[]) new Object[10];
}
void add(T t){
this.ts[cursor++] = t;
}
T get(int idx){
return this.ts[idx];
}
}
interface Toy{}
class Robot implements Toy{}
class Drone implements Toy{} 한정적 타입 파라미터
- ToyList에는 Toy 인터페이스의 구현체들만 담고싶음
ToyList<Robot> robots = new ToyList<>();
ToyList<Drone> robots = new ToyList<>();- 만족스럽지만 이 코드를 막을 순 없음
ToyList<String> robots = new ToyList<>();한정적 타입 파라미터
- 한정적 타입 파라미터(Bouned Type Parameter)를 사용하여 해결 가능
class ToyList<T extends Toy>{
// ...생략
}
// String은 Toy의 하위 타입이 아니므로 컴파일 에러 발생
ToyList<String> robots = new ToyList<>();공변/반공변
- 제네릭 메서드나 와일드 카드를 이용해 공변을 구현할 수 있으나 특정 타입이 아닌 Object에 대한 내용만 구현가능
- extends, super 키워드를 사용하여 좀 더 구체적인 타입에 대한 공변/반공변을 구현할 수 있음
- extends를 사용할 경우 상위 한정(Upper Bounded), super를 사용할 경우 하위 한정(Lower Bounded) 라고 표현
public static <T extends Number> void method(List<T> numbers) {}
method(new ArrayList<Integer>());
method(new ArrayList<Long>());
method(new ArrayList<String>()); // 컴파일 에러PECS
- 제네릭 사용시 'super를 사용하느냐, extends를 사용하느냐' 는 자바개발자들에게 너무나도 어려운 개념
public void sort(Comparator<? super E> c) {}
public boolean addAll(Collection<? extends E> c) {}- 실제 ArrayList.java 에 정의되어있는 두 메서드
- 어떤 기준으로 super를 쓰고, extends를 썼는지 알 수 있는가?
PECS
- 제네릭을 좀 더 쉽게 사용하기위해 조슈아 블로크는 PECS(Producer-Extends, Consumer-Super)라는 규칙을 제안
- 생산자는 extends를 쓰고, 소비자는 super를 쓰라는 규칙
- 간단히 생각해서 get() 을 호출할땐 extends, add() 를 호출할땐 super
- addAll 같은경우 인자로 넘어온 Collection 의 내부 데이터들을 모두 꺼냄(생산의 개념). 그렇기 때문에 extends 사용
- sort의 경우 인자로 넘어온 Comparator 의 compare() 메서드에 인자로 사용(소비의 개념). 그렇기 때문에 super 사용
PECS
- 어려우면 너무 고민하지말고 그냥 T, ? 쓰면 보통의 상황에선 다 해결된다. (by 토비)
- 제네릭을 사용할 때 와일드 카드는 반드시 필요한 경우가 아니면 사용하지 않아도 무방하다(자바5 이전에 우리는 제네릭이 없는 상태에서도 충분히 행복하게 자바 프로그래밍을 수행했다는 점을 기억하기 바란다) (폴리글랏 프로그래밍 p.89, 임백준)
PECS
List<Number> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
list1.addAll(list2); - 숫자 리스트에 정수 리스트를 더하는건 우리의 상식상 아무런 문제가 없다.
- 실제 저 코드는 정상적으로 컴파일되며, 정상적으로 실행된다.
- addAll의 시그니처가 Collection<? extends E>가 아니라 Collection<E>라면?
- 앞에서 알아본 불공변(invariant)의 특성으로인해 컴파일에러가 발생할 것이다.
Type Token
- 자바의 제네릭은 타입 소멸 방식으로 구현됐기때문에 리플렉션 API로 제네릭 정보를 구할수 없다.(런타임시엔 제네릭 정보가 사라짐)
- 다만 몇가지의 경우 타입 소멸이 되지않아 제네릭 정보를 구할 수 있다.
class Super<T>{}
class Sub extends Super<String>{}
Sub sub = new Sub();
// 상위 타입의 정보를 구함
ParameterizedType type = (ParameterizedType) sub.getClass().getGenericSuperclass();
// 제네릭 정보를 배열로 얻음
Type[] types = type.getActualTypeArguments();
// 첫번째 타입 파라미터의 이름을 구함
String typeName = types[0].getTypeName(); Type Token
- 하위 클래스에서 상위 클래스를 구현할때 전달한 타입 아규먼트 정보는 컴파일 후에도 사라지지않는데 이를 이용해서 제네릭 정보만 전달하기위한 용도로 사용 가능
- Super 클래스는 제네릭 셔틀 용도로 상위 클래스로만 사용되기때문에 추상 클래스로 만들어놓고, 하위 클래스는 익명 클래스 방식으로 구현하여 제네릭 셔틀로 사용
- 이를 Super Type Token 이라 부르며 특히 자바 객체로 타입을 변환해야하는 역직렬화시 많이 사용
- Jackson, Spring 등은 이미 TypeReference<T> 라는 클래스를 제공하고있음
Type Token
- Jackson 에서 제공하는 TypeReference
public abstract class TypeReference<T> implements Comparable<TypeReference<T>> {
final Type _type;
protected TypeReference() {
Type superClass = this.getClass().getGenericSuperclass();
if (superClass instanceof Class) {
throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
} else {
this._type = ((ParameterizedType)superClass).getActualTypeArguments()[0];
}
}
public Type getType() {
return this._type;
}
public int compareTo(TypeReference<T> o) {
return 0;
}
}참고자료
- 이펙티브자바(조슈아 블로크)/서적
- 폴리글랏 프로그래밍(임백준)/서적
- 토비의 봄(이일민)/유튜브
Copy of Generics
By changyong
Copy of Generics
generics 발표 자료
- 176