JDK 1.5에 등장한 제네릭은, 여러 타입의 파라미터를 삽입해 객체를 생성할 수 있어 코드를 간결하게, 재사용성을 높여주었다. 또한 컴파일 단계에서 검출되지 않은 타입 문제가 런타임 단계에서 발생할 가능성을 방지해준다.
1️⃣ 제네릭 타입
Generic Types
A generic type is a generic class or interface that is parameterized over types.
- 유형에 대해 매개변수화되는 제네릭 클래스 또는 인터페이스.
- 타입을 구체적으로 지정하는 것이 아닌, 추후에 지정할 수 있도록 일반화해두는 것.
- 내부에서 타입을 지정하는 것이 아닌, 외부에서 지정하게끔 일반화
- 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 한다.
생김새
ArrayList<String> list = new ArrayList<>();
//제네릭 타입 매개변수에 정수 타입을 할당
FruitBox<Integer> intBox = new FruitBox<>();
- <> 해당 꺽쇠 괄호가 "제네릭" 이다.
- <T> 부분의 실행부에서 타입을 받아와, 내부에서 T 타입으로 지정한 멤버들에게 전파하여 타입이 구체적으로 설정된다.
타입 파라미터 기호 네이밍
제네릭 기호는 문법적으로 정해진 것이 없고, 통상적인 네이밍 규칙(convention)이 있을 뿐이다.
- <T> : 타입(Type)
- <E> : 요소(Element) - List
- <K> : 키(Key) - Map<k,v>
제네릭 클래스 고려사항
- 제네릭 타입 간에는 상하위의 관계가 없다.
- 제네릭 타입의 배열 생성 불가.
- 컴파일 시점에서 메모리를 확보해야 하는데, 타입 변수가 무엇인지 알수 없는 제네릭 특성상 불가능하다.
- Object 배열 이후, T[]로 형변환은 가능하다.
class Box<T>{
T[] tArr1; //선언 가능
T[] tArr2 = new T[3]; //생성은 불가
}
- static 변수/메소드 선언 불가
- 제네릭 타입은 인스턴스가 생성되어야 정해지지만, static 멤버는 인스턴스 생성 전에 메모리에 올라가 있다.
제네릭 메서드 고려사항
- 제네릭 타입 간 상하위 관계가 없다.
- 제네릭 클래스 내부에서도 정의 가능
- 제네릭 클래스의 타입이름 <T>와 제네릭 메서드 타입이름<T>는 이름만 같을 뿐, 별개이다.
- 만약 제너릭("<>") 없이 T를 사용했다면, 구체화된 T 타입을 가진다.
- static 메서드가 가능하다
- 제네릭 메서드의 제네릭 타입은 지역변수처럼 사용되고, 프로그램 실행시 static 메서드가 메모리에 올라갈 때 타입 지정 없이 메서드의 틀만 공유될 수 있다.
- 메서드 호출 시 타입을 지정하면 된다.
2️⃣ 공변(covariant) 과 불공변(invariant)
공변이란, A가 B의 하위 타입일 때 T<A> 가 T<B>의 하위 타입이면 T는 공변이다.
불공변이란, A가 B의 하위 타입일 때 T<A>가 T<B>의 하위 타입이 아니면 T는 불공변이다.
- 대표적으로 배열은 공변, 제네릭은 불공변
- Integer이 Object의 하위 타입이고, Integer[]은 Object[]의 하위 타입이다.
- Integer은 Object의 하위 타입이고, List<Integer>은 List<Object>의 하위 타입이 아니다.
- 다음은 공변, 불공변의 예시이다.
void foo(){
List<Integer> list1 = Arrays.asList(1,2,3);
Integer[] list2 = new Integer[]{1,2,3};
//컴파일 에러 발생
printCollection(list1);
//실행가능
printArray(list2);
}
void printCollection(Collection<Object> c){
for(Object e : c){
sout(e);
}
}
void printArray(Object[] arr){
for(Object e : arr) {
sout(e);
}
}
여기서 제네릭의 ? 타입인, 와일드카드 타입이 등장한다.
3️⃣ 와일드카드
- 제네릭이 등장했지만 오히려 실용성이 떨어지는 상황들이 생긴다.
- 모든 타입을 대신할 수 있는 타입 - 와일드카드 타입
- 임의의 타입을 나타내며, 정해지지 않은 unknown type으로 타입에 제한이 없어 비제한 와일드카드 타입이라고 한다.
- 위에서 제네릭의 불공변 특성으로 인해, 컬렉션을 제대로 활용하지 못하는 상황을 해결해준다.
- 자바 모든 객체의 최상위 부모는 Object
- 그러나 요소를 추가하는 것은 타입 안전성을 보장할 수 없어, 허용하지 않는다.
- 와일드카드의 타입이 any가 아닌, unknown이다.
void printCollection(Collection<?> c){
for (Object e : c){
sout(e);
}
void addCollection(){
Collection<?> c = new ArrayList<String>();
c.add(new Object()); //컴파일 에러
}
4️⃣ 한정적 와일드 카드
위와 같은 문제를 해결하기 위해, 한정적 와일드카드(Bounded Wildcard)를 제공한다.
특정 타입을 기준으로, 상한 범위와 하한 범위를 지정함으로써 호출 범위를 확장 또는 제한할 수 있다.
상한 경계 와일드카드(Upper Bounded Wildcard)
- 상한 경계 와일드카드는 와일드카드 타입에 extends를 사용해 와일드카드 타입의 최상위 상위 타입을 정의함으로써 상한 경계를 설정한다.
- 결론부터 말하자면, 상한 경계 와일드카드는 컬렉션에서 원소를 꺼내 활용하는 것이 가능하다.
다음과 같은 3가지 클래스가 존재한다고 가정하자.
class MyGrandParent {}
class MyParent extends MyGrandParent {}
class MyChild extends MyParent {}
이후 실행되는 코드를 보자.
void printCollection(Collection<? extends MyParent> c) {
// 컴파일 에러
for (MyChild e : c) {
System.out.println(e);
}
for (MyParent e : c) {
System.out.println(e);
}
for (MyGrandParent e : c) {
System.out.println(e);
}
for (Object e : c) {
System.out.println(e);
}
}
- MyParent를 extend했기 때문에, MyParent와 MyGrandParent, Object 타입으로 get하는 연산에는 문제가 없다.
- 그러나 범위 바깥의 MyChild는 접근할 수 없어 컴파일 에러가 발생한다.
이후, Collection<? extend MyParent> c 의 c의 원소를 사용 또는 소모하여 컬렉션에 추가하는 경우는 상황이 달라진다.
아래 코드를 보자
void addElement(Collection<? extends MyParent> c) {
c.add(new MyChild()); // 불가능(컴파일 에러)
c.add(new MyParent()); // 불가능(컴파일 에러)
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
- 모든 타입에 대해 컴파일 에러가 발생한다.
- 컬렉션 타입인 <? extend MyParent> 로 가능한 타입은 MyParent와 미지의 모든 MyParent 자식 클래스들이므로, c가 MyParent 하위 타입중 어떤 타입인지 모르기 때문이다.
- 상한 경계가 지정된 경우에는 하위 타입을 특정할 수 없어 새로운 원소를 추가하는 것이 불가능하다.
- 상한 경계 와일드카드는 컬렉션에서 원소를 꺼내 활용할 수 있다.
하한 경계 와일드카드 (Lower Bounded Wildcard)
- super를 사용해, 와일드카드의 최하위 타입을 정의해 하한 경계를 설정할 수 있다.
- 결론부터 말하자면, 컬렉션에서 값을 꺼내 원소를 만드는 경우가 일부 가능하다.
void addElement(Collection<? super MyParent> c) {
c.add(new MyChild());
c.add(new MyParent());
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
- 컬렉션 c가 갖는 타입은 적어도 MyParent의 부모 타입들이다.
- 그러므로 MyParent의 자식 타입이라면 안전하게 컬렉션에 추가할 수 있고, 부모 타입인 경우에만 컴파일 에러가 ㅏㄹ생한다.
컬렉션에서 원소를 꺼내 활용하는 경우를 보자
void printCollection(Collection<? super MyParent> c) {
// 불가능(컴파일 에러)
for (MyChild e : c) {
System.out.println(e);
}
// 불가능(컴파일 에러)
for (MyParent e : c) {
System.out.println(e);
}
// 불가능(컴파일 에러)
for (MyGrandParent e : c) {
System.out.println(e);
}
for (Object e : c) {
System.out.println(e);
}
}
- 부모타입을 특정할 수 없어, 모든 부모 타입들에 컴파일 에러가 발생한다.
- Object의 경우에는 모든 객체의 부모임이 명확하기에 컴파일 에러가 발생하지 않는다.
- 하위 타입인 경우에도, MyParent와 미지의 MyParent 부모 타입이기에 MyChild와 같이 경계 아래 하위타입은 추가될 수 없다.
5️⃣ 맺으며
PECS(Producer-Extends, Consumer-Super) 공식
제네릭 타입과 와일드카드는 상당히 많이 헷갈리는 주제이고, 언제 사용해야 할지 모호해 보인다. 이펙티브 자바에서 등장한 PECS 공식은 이러하다.
- 컬렉션으로부터 와일드카드 타입의 객체를 꺼내 생성(product)하면 extend를
- 갖고있는 객체를 컬렉션에 사용(consume)해 추가하면 super를
이러한 공식을 염두에 두고, 추후에 사용하게 된다면 사용해보자.
'Java&Kotlin' 카테고리의 다른 글
[Java] Stream & Lambda - Stream (0) | 2024.06.26 |
---|---|
[Java] Stream & Lambda - Lambda (0) | 2024.06.25 |
[JAVA] Record: 불변 데이터 클래스의 활용 (1) | 2024.06.12 |
[JAVA] Optional : 안전한 null 처리 방법 (0) | 2024.06.12 |
[Java & Intellij IDEA] Java 22 & Gradle 8.7 (0) | 2024.05.28 |