1. 개요
JDK 5.0은 버그를 줄이고 유형에 대한 추상화 계층을 추가하기 위해 Java Generics를 도입했습니다.
이 예제은 Generics in Java, 그 이면의 목표 및 코드 품질을 향상시키는 방법에 대한 간략한 소개입니다.
2. 제네릭의 필요성
Integer 를 저장할 Java List을 생성하려는 시나리오를 상상해 봅시다 .
다음을 작성하려고 할 수 있습니다.
List list = new LinkedList();
list.add(new Integer(1));
Integer i = list.iterator().next();
놀랍게도 컴파일러는 마지막 줄에 대해 불평합니다. 어떤 데이터 유형이 반환되는지 모릅니다.
컴파일러는 명시적 캐스팅이 필요합니다.
Integer i = (Integer) list.iterator.next();
List의 반환 유형이 Integer 임을 보장할 수 있는 계약은 없습니다 . 정의된 List에는 모든 개체가 포함될 수 있습니다. 우리는 컨텍스트를 검사하여 List을 검색한다는 것만 알고 있습니다. 형식을 볼 때 개체 라는 것만 보장할 수 있으므로 형식이 안전한지 확인하기 위해 명시적 캐스트가 필요합니다.
이 캐스트는 성가실 수 있습니다. 이 List의 데이터 유형이 Integer 라는 것을 알고 있습니다 . 캐스트는 또한 코드를 복잡하게 만듭니다. 프로그래머가 명시적 캐스팅에 실수를 하면 유형 관련 런타임 오류가 발생할 수 있습니다.
프로그래머가 특정 유형을 사용하려는 의도를 표현할 수 있고 컴파일러가 그러한 유형의 정확성을 보장한다면 훨씬 쉬울 것입니다. 이것이 제네릭의 핵심 아이디어입니다.
이전 코드 조각의 첫 번째 줄을 수정해 보겠습니다.
List<Integer> list = new LinkedList<>();
유형을 포함하는 다이아몬드 연산자 <>를 추가하여 이 List의 전문화 범위를 정수 유형 으로만 좁힙니다 . 즉, List 내부에 보관된 유형을 지정합니다. 컴파일러는 컴파일 타임에 형식을 적용할 수 있습니다.
작은 프로그램에서 이것은 사소한 추가처럼 보일 수 있습니다. 그러나 더 큰 프로그램에서 이것은 상당한 견고성을 추가하고 프로그램을 읽기 쉽게 만듭니다.
3. 일반적인 방법
단일 메서드 선언으로 일반 메서드를 작성하고 다른 유형의 인수를 사용하여 호출할 수 있습니다. 컴파일러는 우리가 사용하는 유형의 정확성을 보장합니다.
다음은 제네릭 메서드의 일부 속성입니다.
- 제네릭 메서드에는 메서드 선언의 반환 형식 앞에 형식 매개 변수(형식을 묶는 다이아몬드 연산자)가 있습니다.
- 유형 매개변수는 제한될 수 있습니다(이 기사의 뒷부분에서 경계에 대해 설명합니다).
- 제네릭 메서드는 메서드 서명에서 쉼표로 구분된 다른 형식 매개 변수를 가질 수 있습니다.
- 제네릭 메서드의 메서드 본문은 일반 메서드와 같습니다.
다음은 배열을 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 유형의 요소가 있는 List으로 변환하는 함수를 전달합니다 .
예는 Integer 를 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는 "제한된"을 의미하며 메서드가 허용하는 유형을 제한할 수 있습니다.
예를 들어 메소드가 유형과 모든 하위 클래스(상한) 또는 유형과 모든 상위 클래스(하한)를 허용하도록 지정할 수 있습니다.
상한 유형을 선언하려면 유형 뒤에 extends 키워드를 사용하고 그 뒤에 사용하려는 상한을 사용합니다.
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
여기서 extends 키워드를 사용하는 것은 T 유형 이 클래스의 경우 상한을 확장하거나 인터페이스의 경우 상한을 구현한다는 의미입니다.
3.2. 다중 경계
유형은 또한 여러 상한을 가질 수 있습니다.
<T extends Number & Comparable>
T 에 의해 확장된 유형 중 하나가 클래스(예: Number )인 경우 경계 List의 첫 번째 항목에 넣어야 합니다. 그렇지 않으면 컴파일 타임 오류가 발생합니다.
4. 제네릭과 함께 와일드카드 사용
와일드카드는 물음표 ? 로 표시됩니다. Java에서는 알 수 없는 유형을 참조하는 데 사용합니다. 와일드카드는 제네릭과 함께 특히 유용하며 매개변수 유형으로 사용할 수 있습니다.
그러나 먼저 고려해야 할 중요한 사항이 있습니다. 우리는 Object 가 모든 Java 클래스의 상위 유형이라는 것을 알고 있습니다. 그러나 Object 컬렉션은 컬렉션 의 상위 유형이 아닙니다.
예를 들어 List<Object> 는 List<String> 의 상위 유형이 아니며 List< Object > 유형 의 변수를 List<String> 유형의 변수에 할당 하면 컴파일러 오류가 발생합니다. 이는 동일한 컬렉션에 이기종 유형을 추가할 경우 발생할 수 있는 충돌을 방지하기 위한 것입니다.
유형 및 해당 하위 유형의 모든 컬렉션에 동일한 규칙이 적용됩니다.
다음 예를 고려하십시오.
public static void paintAllBuildings(List<Building> buildings) {
buildings.forEach(Building::paint);
}
House 와 같은 Building 의 하위 유형을 상상하면 House 가 Building 의 하위 유형 이더라도 House List과 함께 이 방법을 사용할 수 없습니다 .
건물 유형 및 모든 하위 유형과 함께 이 방법을 사용해야 하는 경우 경계 와일드카드가 마법을 수행할 수 있습니다.
public static void paintAllBuildings(List<? extends Building> buildings) {
...
}
이제 이 방법은 건물 유형 및 모든 하위 유형에서 작동합니다. 이를 상한 와일드카드라고 하며, 여기서 건물 유형 은 상한입니다.
알 수 없는 유형이 지정된 유형의 상위 유형이어야 하는 하한이 있는 와일드카드를 지정할 수도 있습니다. 특정 유형이 뒤에 오는 super 키워드 를 사용하여 하한을 지정할 수 있습니다 . 예를 들어 <? super T> 는 T 의 슈퍼클래스 (= T와 모든 부모) 인 알 수 없는 유형을 의미합니다 .
5. 유형 삭제
유형 안전성을 보장하기 위해 제네릭이 Java에 추가되었습니다. 그리고 제네릭이 런타임에 오버헤드를 일으키지 않도록 하기 위해 컴파일러는 컴파일 타임에 제네릭에 유형 삭제 라는 프로세스를 적용합니다.
유형 삭제는 모든 유형 매개변수를 제거하고 해당 범위로 대체하거나 유형 매개변수가 제한되지 않은 경우 Object 로 바꿉니다. 이런 식으로 컴파일 후 바이트 코드에는 일반 클래스, 인터페이스 및 메서드만 포함되어 새로운 유형이 생성되지 않습니다. 컴파일 시 Object 유형 에도 적절한 캐스팅이 적용됩니다 .
다음은 삭제 유형의 예입니다.
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
유형 삭제를 사용하면 무제한 유형 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());
}
유형이 경계가 있는 경우 유형은 컴파일 시간에 경계로 대체됩니다.
public <T extends Building> void genericMethod(T t) {
...
}
컴파일 후 변경됩니다.
public void genericMethod(Building t) {
...
}
6. 제네릭 및 기본 데이터 유형
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 유형을 제공하며, 이를 unwrap하기 위한 autoboxing 및 unboxing도 제공합니다 .
Integer a = 17;
int b = a;
따라서 정수를 담을 수 있는 List을 만들고 싶다면 다음 래퍼를 사용할 수 있습니다.
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 언어에 강력한 추가 기능입니다. 제네릭은 컴파일 시간에 유형 정확성을 적용하고 가장 중요한 것은 애플리케이션에 추가 오버헤드를 발생시키지 않으면서 제네릭 알고리즘을 구현할 수 있다는 것입니다.
기사와 함께 제공되는 소스 코드는 GitHub에서 사용할 수 있습니다 .