Oracle의 권장 사항은 대문자를 사용하여 일반 타입을 나타내고 보다 설명적인 문자를 선택하여 공식적인 타입을 나타내는 것입니다. JAVA 컬렉션에서는 유형에 T, 키에 K, 값에 V를 사용합니다.
3.1. Bounded Generics
타입 매개변수는Bounded 될 수 있음을 기억하십시오, 제너릭은 매서드가 허용하는 타입을 제한 할 수 있습니다.
예를 들어 메소드가 타입과 모든 하위 클래스(upper bound) 또는 타입과 모든 상위클래스(lower bound)를 허용하도록 지정할 수 있습니다.
upper-bounded type을 선언하려면 타입 뒤에 extends 키워드를 사용하고 그 뒤에 사용하려는 upper bound를 사용합니다.
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
여기서 extends 키워드를 사용하는 것은 타입 T가 클래스인 경우 upper bound를 확장(extends) 하거나 인터페이스의 경우 구현(implements)한다는 의미입니다.
3.2. Multiple Bounds
타입은 여러 개의 upper bounds를 가질 수 있습니다:
<T extends Number & Comparable>
T에 의해 확장된 타입 중 하나가 class(예: Number)인 경우 bounds 목록의 첫 번째 항목에 넣어야 합니다. 그렇지 않으면 컴파일 타임 오류가 발생합니다.
4. Wildcards 사용하기
와일드카드는 물음표 ?로 표시됩니다. Java에서는 알 수 없는 유형을 참조하는 데 사용합니다. 와일드카드는 제네릭과 함께 특히 유용하며 매개변수 유형으로 사용할 수 있습니다.
그러나 먼저 고려해야 할 중요한 사항이 있습니다. 우리는 Object가 모든 자바 클래스의 상위 유형이라는 것을 알고 있습니다. 그러나 Object 컬렉션은 컬렉션의 상위 유형이 아닙니다.
예를 들어 List<Object>는 List<String>의 상위 유형이 아니며 List<Object> 유형의 변수를 List<String> 유형의 변수에 할당하면 컴파일러 오류가 발생합니다. 이는 동일한 컬렉션에 이질적이 유형을 추가할 경우 발생할 수 있는 충돌을 방지하기 위한 것입니다.
유형 및 해당 하위 유형의 모든 컬렉션에 동일한 규칙이 적용됩니다.
다음 예시입니다:
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
House가 Building의 하위 유형이더라도 House List에 이 방법을 사용할 수 없습니다.
Building 타입 및 모든 하위 타입에 이 메소드를 사용해야 하는 경우,bounded wildcard가 이 문제를 해결 할 수 있습니다:
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
이제 이 방법은Building 타입 및 모든 하위 타입에서 작동합니다. 이를 상한 와일드카드(upper-bounded wildcard)라고 하며, 여기서Building 유형은upper bound입니다.
알 수 없는 타입이 지정된 타입의 상위 유형이어야 하는 와일드카드를 지정할 수도 있습니다. 특정 타입이 뒤에 오는 super 키워드를 사용하여 하한을 지정할 수 있습니다. 예를 들어 <? super T>는 T의 슈퍼클래스(= T와 모든 부모)인 알 수 없는 유형을 의미합니다.
5. Type Erasure
유형 안전성(type safety)을 보장하기 위해 제네릭이 Java에 추가되었습니다. 그리고 제네릭이 런타임에 오버헤드를 일으키지 않도록 하기 위해 컴파일러는 컴파일 타임에 제네릭에 타입 삭제(type erasure) 라는 프로세스를 적용합니다.
유형 삭제는 모든 유형 매개변수를 제거하고 해당 범위로 대체하거나 유형 매개변수가 제한되지 않은 경우 Object로 바꿉니다. 이런 식으로 컴파일 후 바이트 코드에는 일반 클래스, 인터페이스 및 메서드만 포함되어 새로운 유형이 생성되지 않습니다. 컴파일 시 Object 유형에도 적절한 캐스팅이 적용됩니다.
type 삭제의 예시 :
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
타입 삭제에 의하여 unboundedtype T가 Object로 교체됩니다.
// for illustration
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
// which in practice results in
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
만약 타입이 bounded 되었다면, 타입은 컴파일 시점에 bound로 교체됩니다.
public <T extends Building> void genericMethod(T t) {
...
}
public void genericMethod(Building t) {
...
}
6. Generics and Primitive Data Types
Java에서 제네릭의 한 가지 제한 사항은 유형 매개변수가 기본 유형이 될 수 없다는 것입니다.
다음 예시는 컴파일 되지 않습니다:
List<int> list = new ArrayList<>();
list.add(17);
기본 데이터 타입이 작동하지 않는 이유를 이해하려면 제네릭이 컴파일 타임 기능이라는 점을 기억하세요. 즉, 타입 매개변수가 지워지고 모든 제네릭 타입이 Object 타입으로 구현됩니다.
list의 add 매소드를 봅시다:
List<Integer> list = new ArrayList<>();
list.add(17);
add 매소드의 서명은 다음과 같습니다:
boolean add(E e);
그리고 다음과 같이 컴파일 됩니다.
boolean add(Object e);
따라서 타입 매개변수는 Object로 변환할 수 있어야 합니다. 기본 타입은 Object를 확장하지 않기 때문에 타입 매개변수로 사용할 수 없습니다.
그러나 Java는 기본 타입에 대한 boxed 유형을 제공하며, 이를 풀어서 사용하기 위한 autoboxing 및 unboxing 기능을 제공합니다.
Integer a = 17;
int b = a;
따라서 integer를 담을 수 있는 목록을 만들고 싶다면 다음 wrapper를 사용할 수 있습니다.
List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);
컴파일 된 코드는 다음과 같습니다:
List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();
Java의 향후 버전에서는 제네릭에 대한 기본 데이터 유형을 허용할 수 있습니다. Project Valhalla는 제네릭 처리 방식을 개선하는 것을 목표로 합니다. 아이디어는 JEP 218에 설명된 대로 제네릭 전문화를 구현하는 것입니다.
7. 결론
Java Generics는 프로그래머의 작업을 더 쉽고 오류가 덜 발생하도록 해주는 Java 언어에 강력한 추가 기능입니다. 제네릭은 컴파일 시간에 타입 정확성을 적용해주는 장점이 있으며, 가장 중요한 점은 애플리케이션에 추가 오버헤드를 발생시키지 않으면서 제네릭 알고리즘을 구현할 수 있다는 것입니다.