이펙티브 자바 스터디에서 제가 발표한 아이템 31의 내용에 대해 스터디원께서 좋은 질문을 해주셨습니다.
바로 제네릭 메서드에서 type parameter section이 존재하는 이유가 무엇일까? 라는 질문이었습니다.
공식 문서
제네릭 메서드는 메서드 자체의 타입 파라미터를 가진 메서드입니다
제네릭 타입을 선언하는 것과 유사하지만, 타입 파라미터의 scope은 메서드 안에서만 유효합니다
static 과 non-static 제네릭 메서드, 제네릭 생성자 모두 허용됩니다
제네릭 메서드는 꺽쇄 <> 내부에 타입 파라미터의 목록을 포함하고 있으며, 이 목록은 메서드의 반환 타입보다 앞에 적혀있어야 합니다
특히 static 제네릭 메서드에서는 반드시 메서드의 반환 타입 이전에 명시되어야 합니다
질문에 대한 답변
궁금한 점은 어차피 파라미터로 타입이 주어질텐데 왜 type parameter section을 반드시 명시해야 하는가? 입니다.
사실 제네릭 메서드라고 불리기 위해서는 명시를 해야하도록 정의가 되었기 때문에 명시를 해야하는 것이죠
클래스 전체의 타입 파라미터와는 별개로 해당 메서드의 scope 안에서만 적용되는 타입 파라미터를 설정하기 위해서 말입니다
public class GenericEx<E> {
// 제네릭 타입 변수
private E element;
// 제네릭 파라미터 메소드
void set(E element) {
this.element = element;
}
// 제네릭 타입 반환 메소드
E get() {
return element;
}
// 아래 제네릭 메소드들의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
static <E> E genericMethod1(E o) {
return o;
}
static <T> T genericMethod2(T o) {
return o;
}
}
static 메서드의 경우 애초에 클래스의 타입 파라미터인 E를 받아올 수가 없습니다
클래스의 타입 파라미터인 E는 객체가 생성되고 나서야 받아올 수 있기 때문에 generic method의 타입 파라미터는 클래스의 타입 파라미터와는 완전 별개라는 것을 알아야 합니다
끝내기 전에 제네릭 메서드에 대해 조금 더 알아보도록 하겠습니다
Generic Methods
public class Generics {
public <T> List<T> fromArrayToList(T[] a) {
return Arrays.stream(a).collect(Collectors.toList());
}
public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
return Arrays.stream(a)
.map(mapperFunction)
.collect(Collectors.toList());
}
@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
Integer[] intArray = {1, 2, 3, 4, 5};
List<String> stringList
= Generics.fromArrayToList(intArray, Object::toString);
assertThat(stringList).containsExactlyElementsOf(List.of("1", "2", "3", "4", "5"));
}
}
상단의 fromArrayToList
메서드는 non-static generic method의 예시입니다
상단의 fromArrayToList
메서드에서 <T>
는 메서드가 제네릭 타입 T를 사용한다는 것을 암시해주므로 설령 메서드가 void 를 반환한다 하더라도 필요합니다
하단의 fromArrayToList
메서드는 static generic method의 예시이며, 메서드가 한가지 이상의 제네릭 타입을 가질 수 있다는 것을 보여줍니다
제네릭 메서드라면 메서드 내부에서 사용되는 모든 제네릭 타입을 반드시 method signature에 작성해야 합니다
하단의 fromArrayToList
메서드는 T 타입의 배열을 G 타입의 원소를 가진 리스트로 바꿔주는 함수입니다
아래의 테스트 코드에서 정확히 반환되는 지를 확인할 수 있습니다.
Bounded Generics
public <T extends Number> List<T> fromArrayToList(T[] a) {
...
}
위의 코드처럼 extends 키워드를 사용해서 upper-bounded type을 선언할 수 있습니다
Multiple Bounds
<T extends Number & Comparable>
과 같이 Number 클래스와 Comparable 인터페이스를 모두 상속받은 타입만 선언 할 수 있도록 multiple upper bounds를 가질 수 있습니다
클래스와 인터페이스 중에서는 클래스를 제일 선두에 작성해야 합니다
Type Erasure
제네릭은 자바 5에서 추가된 특징으로 이전 버전까지는 로 타입의 컬렉션을 사용했기 때문에 호환성을 위해서 Type Erasure이라는 작업을 컴파일 시점에 실행합니다
Type erasure은 모든 타입 파라미터를 제거하고 해당 타입 파라미터를 bounds나 Object로 치환합니다
1. bound가 없는 예시 코드
public <T> List<T> genericMethod(List<T> list) {
return list.stream().collect(Collectors.toList());
}
상단의 코드는 컴파일 시점에 아래와 같이 변경됩니다
public List<Object> withErasure(List<Object> list) {
return list.stream().collect(Collectors.toList());
}
public List withErasure(List list) {
return list.stream().collect(Collectors.toList());
}
우선 타입 파라미터 T의 bounds가 존재하지 않으므로 Object로 모조리 변합니다
그리고 결국에는 아래와 같이 로타입으로 변경됩니다
2. bound가 있는 예시 코드
public <T extends Building> void genericMethod(T t) {
...
}
위의 코드와 같이 타입 파라미터가 bounded 되었다면, 컴파일 시점에 아래와 같이 bound로 치환됩니다
public void genericMethod(Building t) {
...
}
Generics and Primitive Data Types
Type erasure 작업으로 인해 타입 파라미터는 primitive type이 될 수 없습니다
List<int> list = new ArrayList<>(); // 불가능
list.add(17);
List<Integer> list = new ArrayList<>(); // 가능
list.add(17);
원시 타입을 제네릭 타입으로 사용할 수 없는 이유를 알기 위해서는 add 메서드를 살펴봐야 합니다
boolean add(E e);
이윽고 컴파일 시점에 add 메서드는 아래와 같이 변경됩니다
boolean add(Object e);
결과적으로 타입 파라미터는 Object로 형변환 가능해야 하는데, 원시 타입은 Object를 확장하지 않으므로 원시 타입은 타입 파라미터로 사용할 수 없습니다.
써야 한다면 Object를 상속 받은 wrapper 클래스를 사용해야 합니다
PS
https://www.baeldung.com/java-generics
'개발' 카테고리의 다른 글
[제네릭] 타입 안전 이종 컨테이너.. 이거 어디에 사용할까? (0) | 2022.02.21 |
---|---|
[제네릭] 컬렉션이나 단일 원소 컨테이너에서 매개변수화 되는 대상은 무엇일까? (0) | 2022.02.20 |
[제네릭] Java에서 배열을 공변(covariant)으로 만든 이유는 무엇인가? (0) | 2022.02.14 |
[제네릭] Unbounded Wildcard Type인 컬렉션에는 왜 null 값만 들어가는가? (0) | 2022.02.07 |
[Java] 공급자 Supplier<T> (0) | 2022.01.25 |