제네릭은 익숙하듯 하다가도, 시간이 지나면 내가 제대로 알고있나? 라는 생각이 들었던 부분이었습니다. 자바 뿐만 아니라 여러 언어에서도 타입에 대한 안정성을 보장하기 위해 제네릭을 차용해 사용하고 있습니다.
무백스 스터디를 진행하면서 공식문서와 레퍼런스를 통해 공부한 제네릭의 모든 내용을 정리해보려고 합니다!!
✅ 제네릭은 왜 사용할까?
- 컴파일 타임에 강력한 유형 검사를 할 수 있습니다.
- 자바 컴파일러는 제네릭을 사용한 코드가 type safety를 위반한다면 오류를 발생시킵니다.
- 형 변환을 제거할 수 있습니다.
// 제네릭 사용 x
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
// 제네릭 사용 O
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
✅ Generic Types
제네릭 타입은 클래스, 인터페이스
에 매개변수화된 타입을 말합니다. 제네릭 타입은 다음과 같이 정의하여 사용합니다.
class name<T1, T2, …, Tn> {…}
타입변수는 아무 이름을 지정해도되며, 위와같이 여러개의 타입 변수를 나열할 수 있습니다. 일반적으로 타입변수의 명명 규칙은 다음과 같습니다.
타입 변수 이름 | 설명 |
---|---|
E | Element(Java 컬렉션 프레임워크에서 광범위하게 사용됩니다.) |
K | Key |
V | Value |
N | 숫자 |
T | 타입(클래스, 인터페이스 |
S, U, V | 여러개의 타입 변수에서 2, 3, 4번째 타입을 지칭할때 사용됩니다. |
단일 제네릭 타입 사용 예시
제네릭 타입을 사용하여 초기에 설명했던 제네릭을 사용하면서 얻을 수 있는 이점을 알아보겠습니다!
public class Box {
private Object object;
public Object getObject() {
return object;
}
public void setObject(final Object object) {
this.object = object;
}
}
class BoxGeneric<T> {
private T t;
public T getT() {
return t;
}
public void setT(final T t) {
this.t = t;
}
}
class BoxApplication {
public static void main(String[] args) {
// use non-generic Box class
Box box = new Box();
box.setObject("안녕하세요!"); // String을 제외한 모든 타입이 set될 수 있다.
Object object = box.getObject(); // Object를 반환하므로 type safety하지 않다.
String greeting = (String) object; // 별도의 캐스팅이 필요
System.out.println(greeting);
// use generic Box class
BoxGeneric<String> boxGeneric = new BoxGeneric<>(); // 애초에 String이라는 타입으로 지정했으므로 type-safety함
boxGeneric.setT("안녕하세요!");
String greetingGeneric = boxGeneric.getT(); // 응답값이 String
System.out.println(greetingGeneric);
}
}
// [실행결과]
// 안녕하세요!
// 안녕하세요!
Box 클래스의 Object를 사용하므로 경우 원시 타입이 아니라면 원하는 타입을 set할 수 있습니다. 하지만, get을 할때도 Object를 반환하게 됩니다. 따라서 컴파일 타임에서 어떤 클래스가 반환됐는지 확인할 수 없고, 별도의 캐스팅이 필요하며 캐스팅하는 타입이 올바른지에 대한 검증을 런타임에 맡길 수 밖에 없습니다.
반면에 BoxGeneric 클래스는 Box<String>
으로 참조 변수의 타입에서 어떤 타입을 T로 보유할지 선언합니다. 따라서 set, get 동작에서 String이 아닌 타입을 전달하거나 반환받으려고 하면 컴파일 타임에서 해당 오류를 발견할 수 있습니다!
new BoxGeneric<>()
에서<>
만 사용하고 String을 생략할 수 있는 이유는 Java7에 도입된타입 추론
에 의해 가능합니다!<>
는 비공식 적으로 다이아몬드라고 부르기도 합니다!
또한Box<T>
와 같이 사용되는 T를타입 매개변수(Type Parameter)
라고 지칭하고,Box<Integer>
와 같이 T의 타입을 선언한 Integer는타입 인자(Type Argument)
라고 부릅니다!
복수의 제네릭 타입 사용 예시
초기에 설명했듯이 제네릭 타입은 복수개도 선언할 수 있습니다.
interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
class OrderedPairApplication {
public static void main(String[] args) {
OrderedPair<Integer, String> numberNamePair = new OrderedPair<>(1, "one");
Integer key = numberNamePair.getKey();
String value = numberNamePair.getValue();
System.out.println(key + " " + value);
}
}
// [실행결과]
// 1 one
K, V는 타입 변수 명명 규칙에서 Key, Value를 의미합니다. 따라서 명시적으로 한쌍의 키와 값을 저장하는 Pair 클래스를 생성합니다.
매개변수화된 타입
OrderedPair<K, V>
에서 K또는 V를 매개변수화된 타입으로 둘 수 있습니다.
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
Raw Types
Raw Types는 제네릭 클래스 혹은 인터페이스를 타입 인자없이
선언한 경우를 말합니다. 예를들어 위에서 생성한 BoxGeneric 클래스의 경우 <T>
타입 변수를 지정하지 않고 참조 변수를 선언하면 raw types가 됩니다.
BoxGeneric<String> boxGeneric = new BoxGeneric<>();
BoxGeneric rawTypes = new BoxGeneric();
rawTypes = boxGeneric; // 타입 인자가 String인 boxGeneric을 Raw Types 변수에 할당
rawTypes.setT(89);
Object t = rawTypes.getT(); // Raw Types는 Object로 반환한다.
System.out.println(t);
// 실행결과
/// 89
rawTypes.getT()를 했을때 Object가 반환되는 이유는 Type Erasure가 동작하여 타입 변수가 정해지지 않았다면 Object로 반환됩니다.
✅ Generic Methods
제네릭 메소드는 자체적으로 타입 변수를 도입하는 메서드입니다. 제네릭 타입과 유사하지만, 지정한 타입 변수의 범위가 해당 제네릭 메소드로 제한됩니다. 정적, 비정적 메서드와 더불어 제네릭 클래스의 생성자도 적용할 수 있습니다.
static, non-static method에서 제네릭 메소드 정의하기
정적, 일반 메서드에서 제네릭 메소드를 정의하려면
반환 유형 앞에 <T>
와 같이 타입 변수를 지정해야 합니다.
// Pair가 동일한지 비교하는 유틸 클래스
class PairUtil {
public static <K, V> boolean isSame(Pair<K, V> pair1, Pair<K, V> pair2) {
return pair1.getKey().equals(pair2.getKey())
&& pair1.getValue().equals(pair2.getValue());
}
}
class OrderedPairApplication {
public static void main(String[] args) {
OrderedPair<Integer, String> numberNamePair1 = new OrderedPair<>(1, "one");
OrderedPair<Integer, String> numberNamePair2 = new OrderedPair<>(1, "one");
boolean same1 = PairUtil.<Integer, String>isSame(numberNamePair1, numberNamePair2);
boolean same2 = PairUtil.isSame(numberNamePair1, numberNamePair2);
System.out.println(same1);
System.out.println(same2);
}
}
// 실행 결과
// true
// true
<Integer, String>isSame(…)
과 같이 제네릭 메서드의 타입 인자를 명시적으로 지정할 수 있지만, 타입 추론이 가능하므로 이를 생략할 수 있습니다.
생성자에 제네릭 적용하기
클래스가 제네릭 클래스인지 아닌지 여부에 상관없이 생성자에도 제네릭을 지정할 수 있습니다.
public class TypeInference {
static class MyClass<X>{
public <T> MyClass(T t) {
System.out.println("hello" + t);
}
}
public static void main(String[] args) {
MyClass<String> myClass = new MyClass<>(32);
}
}
// 실행 결과
// hello32
✅ Bounded Type Parameters
제한된 타입 변수는 제네릭 타입을 지정하는 인수에 대해서 특정 범위까지 제한할 수 있는 기능을 제공합니다.
선언하는 방법으로는 타입 매개변수의 이름
, extends 키워드
, 상한값
을 차례대로 나타냅니다.
<T extends Number>
Generic Method에서 사용
class BoxGeneric<T> {
private T t;
public T getT() {
return t;
}
public void setT(final T t) {
this.t = t;
}
// Bounded Type Parameters를 사용한 generic method
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
BoxGeneric<Integer> boxGeneric = new BoxGeneric<>();
boxGeneric.setT(30);
boxGeneric.inspect(50);
// boxGeneric.inspect("text");
// method inspect in class BoxGeneric<T> cannot be applied to given types;
}
}
// 실행 결과
T: java.lang.Integer
U: java.lang.Integer
inspect 메서드는 제네릭 메서드입니다. 제네릭의 타입 변수를 보면 임의의 U 타입은 Number 클래스이거나, 하위 클래스여야 합니다.
Number 클래스는 정수형 Wrapper 클래스들이 상속받아 구현하고 있으므로 다음 클래스들 + Number 클래스의 객체들만이 해당 인자 u로 전달될 수 있습니다.
따라서 boxGeneric.inspect(”text”);
가 불가능한 이유는 String 클래스가 Number 클래스의 하위 클래스가 아니기 때문입니다.
Bounded Type Parameters에서 사용되는 extends 키워드는 단순히 상속관계 뿐만 아니라 인터페이스의 구현 관계에서도 적용될 수 있습니다.
만약 Runnable 인터페이스를 구현한 하위 객체들만 제한된 타입 변수로 두고 싶다면 다음과 같이 사용할 수 있습니다.
<T extends Runnable>
Generic Types에서 사용
class BoxGeneric<T extends Number> {
private T t;
public T getT() {
return t;
}
public void setT(final T t) {
this.t = t;
}
// Bounded Type Parameters를 사용한 generic method
public <U extends Number> void inspect(U u){
System.out.println(t.intValue());
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
BoxGeneric<Integer> boxGeneric = new BoxGeneric<>();
boxGeneric.setT(30);
boxGeneric.inspect(50);
}
}
// 실행 결과
30
T: java.lang.Integer
U: java.lang.Integer
inspect 메서드에서 첫줄에서 Number 클래스에 정의되어 있는 intValue() 메서드를 호출합니다.
생각해보면 이미 제한된 타입 변수를 통해 타입 인수로 들어올 수 있는 클래스의 상한선이 정해졌으므로(extends) 상한 클래스의 메서드는 당연히 사용할 수 있어야 하고, 실제로도 사용할 수 있습니다.
여러 타입을 바운드하기(Multiple Bounds)
제한된 타입 변수에는 여러 타입을 제한하도록 둘 수 있습니다.
extends 키워드 뒤에 타입을 &
를 이용해 나열하여 여러 타입을 제한할 수 있습니다.
단 몇가지 조건이 필요합니다.
- 타입 인수로 지정할때는 반드시 지정된 타입들을 모두 구현한 클래스여야 합니다.
- 클래스는 최대 1개만 지정할 수 있고, 인터페이스는 2개이상 지정할 수 있습니다.
- 클래스와 인터페이스를 나열한다면 반드시 클래스를 먼저 지정해야 합니다.
interface A {
}
interface B {
}
class Box implements A, B {
private Object object;
public Object getObject() {
return object;
}
public void setObject(final Object object) {
this.object = object;
}
}
class BoxGeneric<T extends Box & A & B> { // A & Box & B 는 컴파일 오류 발생
private T t;
public T getT() {
return t;
}
public void setT(final T t) {
this.t = t;
}
public static void main(String[] args) {
BoxGeneric<Box> boxGeneric = new BoxGeneric<>();
boxGeneric.setT(new Box());
System.out.println(boxGeneric.getT());
}
}
// 실행 결과
chapter21.Box@129a8472
재귀적 타입 제한(Recursive Type Bounded)
재귀적 타입 제한은 타입 매개변수가 자기 자신을 포함하는 수식에 의해 한정됩니다.
특히 Comparable 인터페이스에서 많이 사용됩니다.
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
위의 제네릭 메서드는 if (e > elem)
구문에서 컴파일 에러가 발생합니다. 대소 비교연산자는 boolean을 제외한 기본 자료형 타입에서만 사용가능합니다. 하지만, T는 타입을 나타내므로 비교연산자를 사용할 수 없습니다.
(<T extends Integer>
와 같이 Wrapper를 정의해도 타입이므로 연산자를 사용할 수 없습니다.)
이를 해결하기 위해서 Comparable 인터페이스를 구현합니다.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
<T extends Comparable<T>>
는 T라는 타입은 반드시 Comparable 인터페이스를 구현한 클래스임이 보장되므로, compareTo 메서드를 통해 비교연산을 할 수 있습니다.
✅ Generic’s subtyping
일반적으로 클래스를 확장하게 되면 부모 클래스 참조 변수를 통해 자식 클래스의 인스턴스를 참조할 수 있습니다. 이는 클래스간의 관계와 인터페이스를 구현한 관게에서 모두 적용되며, 이런 원리를 통해 Dependency Injection을 하기도 합니다.
제네릭에서도 타입 인수로 지정한 타입과 호환되는 다른 타입이 있다면 해당 타입을 이용할 수 있습니다.
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
하지만 다음과 같은 경우에는 subtyping을 지원하지 않습니다.
public void boxTest(Box<Number> n) { /* ... */ }
boxTest(…) 메서드의 매개인자로 Box<Integer>
혹은 Box<Double>
을 넘겨줄 수 있을것 같지만 Box<Integer>
, Box<Double>
은 Box<Number>
의 하위 유형이 아니기 때문에 불가능합니다. 즉 3개의 타입은 모두 다른 존재 입니다.
만약 제네릭 클래스, 인터페이스를 상속관계로 정의하고 싶다면 인터페이스 및 클래스를 확장시키면 됩니다.여기서 확장시킨다는것의 의미는 interface extends를 말하며, 클래스의 경우 abstract class를 통해 확장시킬 수 있습니다.
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
abstract class PayloadList<E,P> implements List<E> {
abstract void setPayload(int index, P val);
...
}
위와 같이 정의하면 PayloadList는 List의 서브타입이 됩니다.
✅ Wildcards
와일드카드는 ?
로 사용되며 unknown 타입을 의미합니다. 일반적으로 다양한 상황에서 사용되지만 제네릭 메서드 호출, 제네릭 클래스의 인스턴스 생성, 슈퍼타입에서 타입인자로 와일드카드를 사용하지 않기를 권고하고 있습니다.
Unbounded Wildcards
?
하나만 사용하게 되면 바인딩되지 않은 와일드카드가 됩니다. unbounded wildcards가 유용하게 사용되는 시나리오는 두 개 정도 존재합니다.
- Object 클래스에서 제공하는 기능을 사용하여 구현하는 메서드
- 제네릭 클래스의 타입 인자에 의존하지 않는 메서드의 경우(List.size()나 List.clear()와 같은 메서드)
특정 List타입의 모든 요소를 출력해야 하는 메서드의 경우 다음과 같이 unbounded wildcards를 이용할 수 있습니다.
public static void printList(List<?> elements) {
for (Object element : elements) {
System.out.println(element);
}
}
위의 elements 인자의 타입이 만약 List<Object>
라면 List<Integer>
, List<Double>
과 같이 Object가 아닌 타입들은 인자로 전달될 수 없습니다.
<?>
는 마치<Object>
처럼 보이기도 합니다. 하지만,List<Object>
로 예시를 둘 경우 해당 객체 및 모든 객체들을 리스트에 삽입할 수 있지만,List<?>
에는 오직 null만을 삽입할 수 있습니다.이는 ?라는 의미가 Unknown이라는 의미로 타입이 정의되지 않았고, 어떤 타입도 확실하지 않기 때문입니다.
Uppder Bounded Wildcards
와일드 카드를 통해 상한 경계를 지정할 수 있습니다.
만약 Number, Integer, Double 타입에서 모두 이용할 수 있는 메서드를 만들고 싶다면 다음과 같이 메서드를 만들 수 있습니다.
class Test {
void printList(List<? extends Number> values) {
for (Number value : values) {
System.out.println(value);
}
}
public static void main(String[] args) {
Test test = new Test();
List<Integer> integers = new ArrayList<>(List.of(1, 2, 3));
test.printList(integers);
}
}
? extends Number
는 해당 unknown 타입이 최대 Number 클래스이거나 Number 클래스의 자식이어야 함을 나타내는 타입의 상한을 지정합니다.
다음과 같이 자식관계(혹은 부모관계)를 갖는 클래스들이 있을때
class MyGrandParent {
}
class MyParent extends MyGrandParent {
}
class MyChild extends MyParent {
}
아래 메서드를 실행시키게 되면 컴파일 에러가 발생합니다.
void printCollection(Collection<? extends MyParent> c) {
// 컴파일 에러: java: incompatible types: capture#1 of ? extends chapter21.MyParent cannot be converted to chapter21.MyChild
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);
}
}
사실 의미상 ? extends MyParent
라는것이 MyParent의 하위 클래스들은 모두 허용이 되어야 하지 않을까? 생각될 수 있습니다. 의미적으로 클래스의 상한선에 제한을 두는것이기도 합니다.
하지만 MyParent의 자식 클래스인 MyChild를 이용해 출력하려고 하면 컴파일 에러가 발생합니다.
우선 ? extends MyParent
가 가능한 타입은 MyParent와 unknown한 모든 MyParent의 자식 클래스입니다.
이때 unknown child class라는 것의 의미는 다시말해 자식의 타입중에서 어떤 타입인지 확실하지 않다는 의미가 됩니다. 즉, 해당 타입이 MyChild 일수도 있지만 아닐수도 있다는 의미입니다.
또한 적어도 MyParent타입이 된다는것은 확실하므로 해당 타입 혹은 해당 부모 타입으로 꺼내는 것은 문제가 없습니다.
(인터페이스 타입으로 인터페이스의 구현체를 참조하는 경우를 생각해보면 동일합니다!)
하지만 가지고 있는 원소를 사용 혹은 소모(consume)하여 컬렉션에 추가하는 경우에는 이야기가 조금 달라집니다.
void addElement(Collection<? extends MyParent> c) {
c.add(new MyChild()); // 불가능(컴파일 에러)
c.add(new MyParent()); // 불가능(컴파일 에러)
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
? extends MyParen
타입인 c는 MyParent이거나 모든 MyParent의 자식 클래스 중 하나입니다. 결국 MyParent 혹은 MyParent의 하위 타입 중에서 어떤 타입인지 결정할 수 없기 때문에 위의 모든 add가 컴파일에러가 발생합니다.
MyGrandParent가 안되는 이유도 MyParent보다 더 상위 타입이 되므로 더 구체적인 클래스(MyParent)가 덜 구체적인 클래스(MyGrandParent)를 참조할 수 없기에 불가능합니다.
따라서 위의 코드와 같이 원소를 소모하는 경우에는 상한 경계가 아닌 하한 경계를 지정해 최소한 MyParent 타입임을 보장시키면 문제를 해결할 수 있습니다.
Lower Bounded Wildcards
하한 경계 와일드카드는 unknown type을 특정 타입 또는 해당 타입의 상위 타입으로 제한 하는데 ? super A
와 같이 작성합니다. 상한 경계와 하한 경계모두 와일드카드를 통해 지정할 수 있지만, 둘 다 지정할 수는 없습니다.
상한 경계와 마찬가지로 원소를 사용(consume)하여 컬렉션에 추가하는 코드를 하한 경계로 지정해보겠습니다.
void addElement(Collection<? super MyParent> c) {
c.add(new MyChild());
c.add(new MyParent());
c.add(new MyGrandParent()); // 불가능(컴파일 에러)
c.add(new Object()); // 불가능(컴파일 에러)
}
? super MyParent
를 통해 컬렉션 c가 갖는 타입은 적어도 MyParent의 부모 타입들이 됩니다. 따라서 MyParent이거나 MyParent의 자식 타입들은 안전하게 컬렉션에 추가할 수 있습니다.(덜 구체적인것이 구체적인것을 참조)
하지만, 부모 타입인 경우에는 부모 타입중에서 어떤 타입인지 확실하지 않으므로 불가능합니다.
이번에는 컬렉션에서 값을 꺼내서 원소를 만드는 produce하는 코드를 살펴보겠습니다.
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);
}
}
c안에 존재하는 원소는 MyParent 타입이거나 부모 타입이지만, 해당 타입들 중에서 어떤 타입인지 특정지을 수 없습니다. 따라서 모든 부모 타입인 경우에서 컴파일 에러가 발생합니다.
또한 구체적인 자식 타입이 덜 구체적인 MyParent 타입을 참조할 수 없으므로 자식 타입또한 컴파일 에러가 발생합니다.
반면에 모든 타입은 Object의 하위 타입이므로 예외적으로 Object는 c의 원소들을 참조할 수 있습니다.
PECS(Producer-Extends, Consumer-Super) 공식
이펙티브 자바에서는 PECS 공식을 사용하기를 권장합니다.
즉, 와일드카드 타입의 객체를 생성 및 만들게되면 extends를, 갖고 있는 객체를 컬렉션에 사용 또는 소비(consume)하게 되면 super를 사용하라는 공식입니다.
void printCollection(Collection<? extends MyParent> c) {
for (MyParent e : c) {
System.out.println(e);
}
}
void addElement(Collection<? super MyParent> c) {
c.add(new MyParent());
}
printCollection
메서드의 경우 컬렉션에서 원소를 꺼내면서 와일드카드 타입의 객체들을 생성하고 있습니다.(produce)addElement
메서드의 경우 컬렉션에 원소를 추가함으로써 객체를 사용(consume)하고 있습니다.
따라서 전자의 경우에는 extends가 후자의 경우에는 super를 사용하는것이 적절합니다.
와일드카드와 서브타입
제네릭 타입은 단순히 타입 간의 부모, 자식 관계가 있다고 해서 서로 연관되지 않습니다.
하지만, 와일드카드를 사용하면 제네릭 타입 간의 관계를 만들 수 있습니다.
아래와 같이 부모, 자식 간의 클래스 관계가 존재할때
class MyParent {
}
class MyChild extends MyParent {
}
다음과 같이 부모 타입으로 자식 타입을 참조할 수 있습니다.
MyChild child = new MyChild();
MyParent parent = child; // 가능
하지만 다음과 같이 제네릭을 사용하는 코드는 불가능합니다.
List<MyChild> childs = new ArrayList<>();
List<MyParent> parents = childs; // 컴파일 에러
List<Child>
와 List<Parent>
사이의 공통적인 상위 타입은 List<?>
와 같이 Unknown 타입으로 정의할 수 있습니다.
List<? extends MyChild> childs = new ArrayList<>();
List<? extends MyParent> parents = childs; // 가능 <? extends MyChild>는 <? extends MyParent>의 서브타입 입니다.
와일드카드 캡처와 헬퍼 메서드
컴파일러는 와일드카드의 유형을 유추하기도 합니다. List<?>
와 같이 정의했어도 컴파일러는 특정 유형을 유추하게 됩니다. 이를 와일드카드 캡처라고 합니다.
void foo(List<?> i) {
i.set(0, i.get(0)); // java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?
}
컴파일러는 i 를 Object 타입인것으로 간주하고 처리
합니다. List.set메서드에서는 컴파일러가 해당 리스트에 삽입하는 객체의 유형을 확인할 수 없기때문에 컴파일 에러가 발생합니다.
따라서 이를 와일드카드 헬퍼 메서드를 통해 해결할 수 있습니다.
void foo(List<?> i) {
fooHelper(i);
}
// 와일드카드를 캡쳐할 수 있습니다.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
fooHelper 제네릭 메서드는 와일드카드를 캡처하는 메서드입니다. 이를 통해 와일드카드의 타입을 사용자가 임의로 지정하여 문제를 해결할 수 있습니다.
공식문서에서 제공하는 와일드카드 가이드라인
언제 상한 경계 와일드카드를 사용할지 혹은 하한 경계 와일드카드를 사용해야할지 구분하기 어렵고 혼란스럽습니다. 오라클 공식문서에서는 이런 부분에서 간단하게 가이드라인을 제공해주고 있습니다.
- In variable
in
변수는 코드에 데이터를 제공합니다. 만약copy(src, dest)
와 같은 복사메서드에 2개의 인수가 있다면src
는 복사할 데이터를 제공하므로in 변수
가 됩니다.- Out variable
out
변수는 다른 곳에서 사용할 데이터를 보유합니다.dest 인수
가 데이터를 받아들이게 되므로 out 변수가 됩니다.
위와 같이 변수가 존재할때 다음과 같이 가이드라인을 제공합니다.
- In 변수는 extends 키워드를 사용하여 상한 와일드카드로 정의합니다.
- out 변수는 super 키워드를 사용해 하한 와일드카드로 정의합니다.
- Object 클래스에 정의된 메서드를 사용해 In 변수에 액세스할 수 있는 경우, 제한없는 와일드카드(?)를 사용합니다.
- 코드가 In, Out 변수를 모두 액세스 해야 하는 경우 와일드카드를 사용하지 않습니다.
위 가이드라인은 메서드 반환 유형에는 적용하지 않습니다. (와일드카드를 반환 유형으로 사용하면 코드를 사용하는 사용자가 와일드카드를 처리해주어야 합니다.)