제네릭이란?

  • JDK1.5부터 도입되어 다양한 데이터 타입을 다룰 수 있도록 클래스, 메소드를 일반화하는 기법
    image.png
  • 클래스 내부에서 사용할 데이터 타입을 외부에서 지정함
  • 제네릭은 원시 값이 아닌 참조 타입으로만 사용 가능함
    • 제네릭은 컴파일 시 Object Type으로 변환되고, 컴파일러가 타입을 캐스팅하기 때문
      //source
      class Container<E> {
          private E element;
          public E getElement() {
              return element;
          }
      }
      public static void main(String[] args) {
          Container<Integer> container = new Container<Integer>();
          Integer integer = container.getElement();
      }
      //runtime
      class Container {
          private Object element;
          public Object getElement() {
              return element;
          }
      }
      public static void main(String[] args) {
          Container container = new Container();
          Integer integer = (Integer) container.getElement();
      }

장점

JDK1.5 이전까지 자바에서 자료구조를 저장할 때 Object Type으로 저장했고,
그렇기 때문에 사용 시 각 타입으로 캐스팅하여 사용해야 했다.

List list = new ArrayList();
list.add(new MyObject());
MyObject myObject = (MyObject) list.get(0);
myObject.printName(); //name 출력

이런 런타임 시 사용자가 지정한 캐스팅의 단점은 컴파일 시 오류를 알 수 없다는 것이다.
다음 코드는 문제 없이 컴파일되지만 런타임에 예외가 발생한다.

List list = new ArrayList();
list.add(new MyObject());
list.add(new Integer(1));
MyObject myObject = (MyObject) list.get(0); 
MyObject intObject = (MyObject) list.get(1); // ClassCastException 발생

하지만 제네릭을 이용한다면 클래스에서 사용할 데이터 타입을 지정할 수 있다.
지정한 타입과 다른 타입을 사용하면 컴파일 시 오류가 발생해 좀 더 빠르게 대응이 가능하다.

제네릭 타입 파라미터

제네릭 클래스, 인터페이스에서 사용되는 파라미터

public class MyGenericClass<T>{...} // T가 제네릭 타입
public class MyGenericClassWithMyTypeName<MyTypeName123>{...}

특수문자를 제외한 어떤 문자가 들어가도 되지만 대표적으로 사용되는 몇 가지 이름이 있다.

<E> : Element. (Collection의 Element를 표현할 때 사용됨)
<T> : Type
<K> : Key
<V> : Value
<N> : Number
<S>, <U>, <V> : more Types

제네릭 타입 파라미터 범위 설정

제네릭 타입 파라미터의 범위는 기본적으로 Unbounded(모든 참조 타입 사용 가능)이다.
다음 방법으로 제네릭 타입의 범위를 지정할 수 있다.

Upper Bounded Type Parameter
제네릭 타입의 범위를 클래스의 서브 타입으로 제한
Class<[TypeParameter] extends [SuperClass]>
(인터페이스 및 추상클래스도 extends로 사용)

public class UpperBoundedList<T extends Number>{...}

이와 같이 사용하면 제네릭 타입이 상위 타입을 상속하거나 구현한 타입임을 명시할 수 있으며 Number의 제네릭 타입에서 상속한 타입의 메서드를 사용할 수 있다.

public class MyObject<T extends List>{
    private T myObject;
    public T get(){
        return myObject.stream()
                       .collect(Collectors.toList());
    }
}

Wildcard

제네릭 타입을 매개변수나 리턴타입, 필드 등으로 지정할 때 타입 파라미터를 제한할 목적으로 사용된다.
제네릭 클래스의 타입 파라미터 이외의 타입을 인자로 받고, 반환할 때 사용할 수 있다.
와일드카드는 제네릭 클래스의 형식, 제네릭 클래스 인스턴스 생성, 슈퍼타입으로 사용될 수 없다.

public boolean containsAll(Collection<?> collection){}

와일드카드도 기본적으로 Unbounded이다.
(List<?> -> List<? extends Object>)
제네릭 타입 파라미터처럼 UpperBound를 제한할 수 있고,
추가적으로 LowerBound 설정도 가능하다.

Lower Bounded Type Parameter
제네릭 타입의 범위를 지정한 클래스의 상위 타입으로 제한
Class<[TypeParameter] super [SubClass]>

public void removeAll(List<? super Integer>){...}

와일드카드는 정확한 데이터 타입을 알고, 조작하기보다는 행위에 초점을 두고 사용한다.
정확한 타입이 필요한 곳에서 와일드카드를 사용한다면 캡처 컴파일 에러가 발생한다.

public class MyObject<T>{
    private List<T> myList = new ArrayList<>();

    public void addAll(List<?> wildCardList){
        this.myList.add(wildCardList.get(0));// 에러 발생
    }
    ...
}