Java Generics의 기초

1. 소개

Java Generics는 버그를 줄이고 유형에 대한 추상화 계층을 추가하기 위해 JDK 5.0에 도입되었습니다.

이 기사는 Generics in Java에 대한 간략한 소개, 그이면의 목표 및 코드 품질을 개선하는 데 사용할 수있는 방법입니다.

2. 제네릭의 필요성

Integer 를 저장하기 위해 Java로 목록을 생성하려는 시나리오를 상상해 봅시다 . 우리는 다음과 같이 쓰고 싶은 유혹을받을 수 있습니다.

List list = new LinkedList(); list.add(new Integer(1)); Integer i = list.iterator().next(); 

놀랍게도 컴파일러는 마지막 줄에 대해 불평 할 것입니다. 반환되는 데이터 유형을 알 수 없습니다. 컴파일러에는 명시 적 캐스팅이 필요합니다.

Integer i = (Integer) list.iterator.next();

목록의 반환 유형이 Integer 임을 보장 할 수있는 계약은 없습니다 . 정의 된 목록에는 모든 개체가 포함될 수 있습니다. 컨텍스트를 검사하여 목록을 검색하고 있다는 것만 알고 있습니다. 유형을 볼 때 Object 임을 보장 할 수만 있으므로 유형이 안전한지 확인하려면 명시 적 캐스트가 필요합니다.

이 캐스트는 성 가실 수 있습니다 . 이 목록의 데이터 유형이 Integer 라는 것을 알고 있습니다. 캐스트도 코드를 복잡하게 만듭니다. 프로그래머가 명시 적 캐스팅에 실수를하면 유형 관련 런타임 오류가 발생할 수 있습니다.

프로그래머가 특정 유형을 사용하려는 의도를 표현할 수 있고 컴파일러가 그러한 유형의 정확성을 보장 할 수 있다면 훨씬 쉬울 것입니다. 이것이 제네릭의 핵심 아이디어입니다.

이전 코드 조각의 첫 번째 줄을 다음과 같이 수정 해 보겠습니다.

List list = new LinkedList();

유형을 포함하는 다이아몬드 연산자를 추가하여이 목록의 전문화를 Integer 유형으로 만 좁 힙니다. 즉, 목록 내에 보관 될 유형을 지정합니다. 컴파일러는 컴파일 타임에 유형을 적용 할 수 있습니다.

작은 프로그램에서는 사소한 추가처럼 보일 수 있지만 큰 프로그램에서는 상당한 견고성을 추가하고 프로그램을 더 쉽게 읽을 수 있습니다.

3. 일반적인 방법

일반 메서드는 단일 메서드 선언으로 작성되고 다른 유형의 인수로 호출 할 수있는 메서드입니다. 컴파일러는 사용되는 유형의 정확성을 보장합니다. 다음은 일반 메서드의 몇 가지 속성입니다.

  • 일반 메서드에는 메서드 선언의 반환 형식 앞에 형식 매개 변수 (형식을 둘러싸는 다이아몬드 연산자)가 있습니다.
  • 유형 매개 변수는 제한 될 수 있습니다 (경계는 문서 뒷부분에서 설명 됨).
  • 일반 메소드는 메소드 서명에서 쉼표로 구분 된 다른 유형 매개 변수를 가질 수 있습니다.
  • 제네릭 메서드의 메서드 본문은 일반 메서드와 같습니다.

배열을 목록으로 변환하는 제네릭 메서드를 정의하는 예 :

public  List fromArrayToList(T[] a) { return Arrays.stream(a).collect(Collectors.toList()); }

이전 예에서 메서드 시그니처에서 메서드가 제네릭 유형 T를 처리한다는 것을 의미합니다 . 메서드가 void를 반환하는 경우에도 필요합니다.

위에서 언급했듯이 메서드는 하나 이상의 제네릭 형식을 처리 할 수 ​​있습니다.이 경우 모든 제네릭 형식은 메서드 서명에 추가되어야합니다. 예를 들어 위 메서드를 수정하여 형식 T 및 형식 을 처리하려는 경우 G , 다음과 같이 작성해야합니다.

public static  List fromArrayToList(T[] a, Function mapperFunction) { return Arrays.stream(a) .map(mapperFunction) .collect(Collectors.toList()); }

T 유형의 요소가있는 배열을 G 유형의 요소가있는 목록으로 변환하는 함수를 전달합니다 . 예제는 Integer 를 해당 문자열 표현 으로 변환 하는 것입니다.

@Test public void givenArrayOfIntegers_thanListOfStringReturnedOK() { Integer[] intArray = {1, 2, 3, 4, 5}; List stringList = Generics.fromArrayToList(intArray, Object::toString); assertThat(stringList, hasItems("1", "2", "3", "4", "5")); }

Oracle 권장 사항은 일반 유형을 나타 내기 위해 대문자를 사용하고 형식 유형을 나타 내기 위해 더 설명적인 문자를 선택하는 것입니다. 예를 들어 Java Collections에서 T 는 유형, K 는 키, V 는 값입니다.

3.1. 제한된 제네릭

앞서 언급했듯이 유형 매개 변수는 제한 될 수 있습니다. Bounded는 " 제한됨 "을 의미 하며 메서드에서 허용 할 수있는 유형을 제한 할 수 있습니다.

예를 들어, 메소드가 유형 및 모든 하위 클래스 (상한) 또는 유형의 모든 수퍼 클래스 (하한)를 허용하도록 지정할 수 있습니다.

상한 유형을 선언하기 위해 우리는 사용할 상한이 뒤에 오는 유형 뒤에 키워드 extends 를 사용합니다. 예를 들면 :

public  List fromArrayToList(T[] a) { ... } 

여기서 extends 키워드 는 유형 T 가 클래스의 경우 상한을 확장하거나 인터페이스의 경우 상한을 구현 한다는 의미로 사용됩니다 .

3.2. 다중 경계

유형은 다음과 같이 여러 상한을 가질 수도 있습니다.

T 에 의해 확장 된 유형 중 하나가 클래스 (예 : Number ) 인 경우 경계 목록에서 첫 번째에 넣어야합니다. 그렇지 않으면 컴파일 타임 오류가 발생합니다.

4. 제네릭과 함께 와일드 카드 사용

와일드 카드는 Java " ? "에서 물음표로 표시됩니다 . ”이며 알 수없는 유형을 참조하는 데 사용됩니다. 와일드 카드는 제네릭을 사용할 때 특히 유용하며 매개 변수 유형으로 사용할 수 있지만 먼저 고려해야 할 중요한 사항이 있습니다.

그것은 것으로 알려져 개체 의 모든 자바 클래스의 상위 유형은, 그러나, 모음 개체 모든 모음의 슈퍼 타입이 아니다.

예를 들어, 목록 의 슈퍼 아닙니다 목록 및 형의 변수에 할당 목록을 유형의 변수 목록 컴파일러 오류가 발생합니다. 이는 동일한 컬렉션에 이기종 유형을 추가 할 때 발생할 수있는 충돌을 방지하기위한 것입니다.

동일한 규칙은 유형 및 하위 유형의 모든 모음에 적용됩니다. 이 예를 고려하십시오.

public static void paintAllBuildings(List buildings) { buildings.forEach(Building::paint); }

우리의 하위 유형 상상 경우 건물을 , 예를 들어, 하우스 , 우리의 목록이 방법을 사용할 수 없습니다 하우스 에도 불구하고, 하우스 의 하위 유형입니다 건물 . 이 메서드를 Building 유형 및 모든 하위 유형과 함께 사용해야하는 경우 경계 와일드 카드가 마법을 수행 할 수 있습니다.

public static void paintAllBuildings(List buildings) { ... } 

이제이 메서드는 Building 유형 과 모든 하위 유형에서 작동합니다. 이를 건물 유형이 상한 인 상한 와일드 카드라고합니다 .

알 수없는 유형은 지정된 유형의 상위 유형이어야하는 하한으로 와일드 카드를 지정할 수도 있습니다. 하한은 super 키워드 다음에 특정 유형을 사용하여 지정할 수 있습니다. 예를 들면 다음과 같습니다.T (= T 및 모든 부모) 의 수퍼 클래스 인 알 수없는 유형을 의미 합니다.

5. 유형 삭제

제네릭은 유형 안전성을 보장하고 제네릭이 런타임에 오버 헤드를 일으키지 않도록하기 위해 Java에 추가되었으며 컴파일러는 컴파일 시간에 제네릭에 유형 삭제 라는 프로세스를 적용합니다 .

유형 삭제는 모든 유형 매개 변수를 제거하고 해당 경계 또는 유형 매개 변수가 제한되지 않은 경우 Object로 대체합니다 . 따라서 컴파일 후 바이트 코드에는 일반 클래스, 인터페이스 및 메서드 만 포함되므로 새로운 유형이 생성되지 않습니다. 컴파일 타임에 적절한 캐스팅이 Object 유형 에도 적용됩니다 .

다음은 유형 삭제의 예입니다.

public  List genericMethod(List list) { return list.stream().collect(Collectors.toList()); } 

With type erasure, the unbounded type T is replaced with Object as follows:

// for illustration public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } // which in practice results in public List withErasure(List list) { return list.stream().collect(Collectors.toList()); } 

If the type is bounded, then the type will be replaced by the bound at compile time:

public  void genericMethod(T t) { ... } 

would change after compilation:

public void genericMethod(Building t) { ... }

6. Generics and Primitive Data Types

A restriction of generics in Java is that the type parameter cannot be a primitive type.

For example, the following doesn't compile:

List list = new ArrayList(); list.add(17);

To understand why primitive data types don't work, let's remember that generics are a compile-time feature, meaning the type parameter is erased and all generic types are implemented as type Object.

As an example, let's look at the add method of a list:

List list = new ArrayList(); list.add(17);

The signature of the add method is:

boolean add(E e);

And will be compiled to:

boolean add(Object e);

Therefore, type parameters must be convertible to Object. Since primitive types don't extend Object, we can't use them as type parameters.

However, Java provides boxed types for primitives, along with autoboxing and unboxing to unwrap them:

Integer a = 17; int b = a; 

So, if we want to create a list which can hold integers, we can use the wrapper:

List list = new ArrayList(); list.add(17); int first = list.get(0); 

The compiled code will be the equivalent of:

List list = new ArrayList(); list.add(Integer.valueOf(17)); int first = ((Integer) list.get(0)).intValue(); 

Future versions of Java might allow primitive data types for generics. Project Valhalla aims at improving the way generics are handled. The idea is to implement generics specialization as described in JEP 218.

7. Conclusion

Java Generics는 프로그래머의 작업을 더 쉽게 만들고 오류 발생 가능성이 적기 때문에 Java 언어에 강력한 추가 기능입니다. 제네릭은 컴파일 타임에 유형 정확성을 강화하고, 가장 중요한 것은 애플리케이션에 추가 오버 헤드를 일으키지 않고 제네릭 알고리즘을 구현할 수 있도록합니다.

기사와 함께 제공되는 소스 코드는 GitHub에서 사용할 수 있습니다.