원문
1. 개요
JDK 5.0에서 Java Generic이 등장합니다. 제너릭은 버그를 줄이고 타입을 넘어선 추상적인 계층을 추가하는 용도로 추가되었습니다.
2. Generics의 필요성
예를 들어봅시다. 자바에서 Integer가 저장되는 list를 만든다고 할 때
아마 다음과 같이 코드를 작성할 것입니다.
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
이 경우 컴파일러는 코드의 마지막 줄에 대해서 에러를 발생 시킬 것입니다. 어떤 데이터 타입이 반환 될지 모르기 때문입니다.
따라서 컴파일러는 명시적인 형변환을 요구할 것입니다.
Integer i = (Integer) list.iterator.next();
리스트에서 꺼낸 값이 Integer라는 보장이 없기 때문에 이러한 상황이 발생합니다. 리스트는 아무 Object나 될 수 때문에 타입의 안전을 위해 이러한 형변환이 필요하게 됩니다.
하지만 명시적 형변환은 코드를 지저분하게 만듭니다. 개발자 입장에서는 이 리스트에 담긴 값이 Integer라는 것을 알고 있기 때문입니다.
만약 개발자가 특정한 타입을 사용할 것이라는 의도를 표기 할 수 있고, 컴파일러가 그러한 타입들의 명확함을 보장 받을 수 있다면 어떨까요? 이러한 생각이 generic의 핵심 사상입니다.
위에서 작성한 코드의 첫 줄을 수정해 봅시다.
List<Integer> list = new LinkedList<>();
diamond operator <>안에 타입을 넣어서 추가하여, Integer 타입 전용 리스트를 만들었습니다.
다른 관점에서 보자면, 리스트 안에서 특정 타입을 유지하도록 하여, 컴파일러가 컴파일 할 때 특정 타입을 강요하도록 만든 것입니다.
3. Generic Methods
제너릭을 단일 매서드 선언에 쓸 수 있을 뿐아니라, 여러 타입의 매개변수에 사용 할 수 있습니다. 컴파일러가 우리가 사용하는 어떤 타입이던 명확성을 보장해줍니다.
- 제너릭 매소드는 매소드 선언의 반환 타입 앞에 타입 매개 변수(타입을 둘러싼 다이아몬드 연산자)를 가지고 있습니다.
- 타입은 제한될 수 있습니다.
- 제너릭 매서드는 메서드 시그니처에서 쉼표로 구분된 다른 유형의 매개 변수를 가질 수 있습니다.
- 제너릭 매서드의 매소드 바디는 일반 매소드와 동일합니다.
이제 array를 list로 바꾸는 예제를 봅시다.
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
매서드 시그니처 안의 <T>는 매서드가 제너릭 타입 T를 다루겠다는 것을 의미합니다. 이것은 심지어 void가 리턴되는 경우에도 필요합니다.
앞서 언급 했듯이 제너릭 타입을 한개 이상 사용하는 것이 가능합니다. 이 경우 우리는 모든 제너릭 타입을 매소드 시그니처에 추가해야만 합니다.
다음은 타입 T와 타입 G를 처리하기 위해 위의 코드를 변경한 것입니다.
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
T 타입의 요소가 있는 배열을 G 타입의 요소가 있는 목록으로 변환하는 함수를 전달합니다. 예시는 Interger를 String으로 변환한 것입니다.
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}
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());
}
타입 삭제에 의하여 unbounded type 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 언어에 강력한 추가 기능입니다. 제네릭은 컴파일 시간에 타입 정확성을 적용해주는 장점이 있으며, 가장 중요한 점은 애플리케이션에 추가 오버헤드를 발생시키지 않으면서 제네릭 알고리즘을 구현할 수 있다는 것입니다.
Reference