Generics

이창용 / Naver Labs

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[]의 하위 타입으로 함께 변함(공변)
  • 배열의 공변성으로 인해 발생하는 문제

불공변(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;
    }
}

참고자료

  • 이펙티브자바(조슈아 블로크)/서적
  • 폴리글랏 프로그래밍(임백준)/서적
  • 토비의 봄(이일민)/유튜브

Generics

By changyong

Generics

generics 발표 자료

  • 380