{% hint style="info" %}
배열은 공변
이고, 제네릭은 불공변이다.
공변
은
자기 자신과 자식 객체로 타입 변환을 허용해주는 것이다.
{% endhint %}
- 공변(convariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이면 T는 공변이라고 한다. 타입 A가 B의 하위 타입(subtype)이라면,
A[]
는B[]
의 하위 타입이 될 수 있는 성질이다. - 불공변(invariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이 아니면, T는 불공변이라고 한다. 제네릭 타입
List<A>
와List<B>
가 있을 때,A
가B
의 하위 타입이더라도List<A>
는List<B>
의 하위 타입이 아니다. 즉, 제네릭 타입은 불공변성을 가진다.
대표적으로 배열은 공변, 제네릭은 불공변인데 예를 들어 배열의 요소들을 출력하는 메소드가 있다고 하자.
[배열의 공변성 예제]
배열은 공변성이 있으므로, 하위 타입의 배열을 상위 타입의 배열로 사용할 수 있다.
@Test
void arrayCovarianceTest() {
Integer[] integers = new Integer[]{1, 2, 3}; // Integer 배열 생성
printArray(integers); // Integer[]를 Object[]로 전달 가능
}
void printArray(Object[] arr) {
for (Object e : arr) {
System.out.println(e); // 배열 요소 출력
}
}
위의 코드에서 Integer[]
는 Object[]
의 하위 타입으로 간주된다. 따라서 printArray(Object[] arr)
메서드에 Integer[]
를 인자로 전달해도 문제가 발생하지 않는다.
[제네릭의 불공변성 예제]
제네릭 타입은 불공변성을 가지므로, List<Integer>
를 List<Object>
로 사용할 수 없다.
@Test
void genericInvarianceTest() {
List<Integer> list = Arrays.asList(1, 2, 3); // List<Integer> 생성
printCollection(list); // 컴파일 에러 발생
}
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e); // 컬렉션 요소 출력
}
}
위의 코드에서 List<Integer>
는 List<Object>
의 하위 타입이 아니기 때문에, printCollection(Collection<Object> c)
메서드에 List<Integer>
를 전달하면 컴파일 오류가 발생한다.
{% hint style="danger" %}
제네릭 타입은 불공변성이기 때문에, List<Integer>
와 List<Object>
는 아무런 관계가 없다.
{% endhint %}
이러한 제네릭의 불공변 때문에 와일드카드1(제네릭의 ?타입)가 등장할 수 밖에 없었다.
제네릭이 도입되기 전, 컬렉션의 요소를 다루는 메서드(= 로 타입)는 타입 안전성을 보장하지 못했다. 컬렉션의 타입 매개변수를 명시할 수 없기 때문에, 모든 요소는
Object
타입으로 처리되었고, 타입 캐스팅이 필요한 상황에서 문제가 발생할 수 있었다.
예시 1: 컬렉션 요소 출력
void printCollection(Collection c) {
Iterator i = c.iterator(); // 타입 지정 없이 Iterator 사용
while (i.hasNext()) {
System.out.println(i.next()); // 모든 요소는 Object 타입으로 간주
}
}
위 코드에서 컬렉션의 요소들은 Object
타입으로 취급되기 때문에, 특정 타입으로 다루려면 타입 캐스팅이 필요하다. 이는 런타임에 타입 에러를 발생시킬 수 있는 잠재적인 위험을 내포하고 있다다.
예시 2: 컬렉션 요소 합 구하기
int sum(Collection c) {
int sum = 0;
Iterator i = c.iterator();
while (i.hasNext()) {
// 문제: 컬렉션의 요소가 Integer가 아닐 수도 있음
sum += Integer.parseInt(i.next().toString()); // 런타임 오류 가능성
}
return sum;
}
위의 메서드는 Collection
에 있는 요소들이 Integer
타입이라고 가정하고 작성된 것이다. 하지만 만약 String
과 같은 다른 타입의 요소를 가진 컬렉션을 전달하면, 컴파일 시에는 문제가 없지만 런타임에 ClassCastException
이 발생할 수 있다.
위와 같은 문제를 해결하기 위해 Java 개발자들은 타입을 지정하여 컴파일 시점에 타입 안전성을 보장할 수 있는 방법을 고안하였고, 그 결과 제네릭이 등장하게 되었다.
제네릭을 사용하면, 컬렉션이나 메서드에 타입 매개변수를 지정할 수 있어, 컴파일 시점에 타입을 검사할 수 있다. 이렇게 하면 런타임 오류의 가능성을 줄이고, 코드의 안정성과 가독성을 높일 수 있다.
제네릭은 런타임 형변환 오류를 방지하기 위해, 자바 5(JDK 1.5)부터 도입되었다. 컴파일러가 안전하게 자동으로 형변환을 추가해줄 수 있게 되었다.
제네릭 클래스 혹은 인터페이스란, 클래스와 인터페이스 선언에 타입 매개변수(T
)가 쓰인 것을 말한다. 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
제네릭 형식 : 클래스/인터페이스 이름<실제 타입 매개변수>
제네릭 타입을 하나 정의하면, 그에 딸린 로 타입(Raw Type)도 함께 정의된다.
제네릭 도입 후 코드 수정
제네릭을 사용하면 컬렉션에 타입을 지정할 수 있어, 컴파일 시점에 타입 안전성을 보장할 수 있다. 예를 들어, Collection<Integer>
타입을 사용하여 숫자들의 합을 구하는 메서드를 작성할 수 있다.
수정된 코드 예제
int sum(Collection<Integer> c) {
int sum = 0;
for (Integer e : c) { // Collection의 요소 타입을 Integer로 제한
sum += e;
}
return sum;
}
- 위 코드에서는
Collection<Integer>
타입을 사용하여, 컬렉션이Integer
타입의 요소만 포함하도록 제한했다. - 컴파일 시점에 타입 검사가 이루어져, 다른 타입의 컬렉션이 전달되면 컴파일 오류가 발생합니다. 이로써 타입 안전성을 보장할 수 있다.
{% hint style="danger" %}
제네릭 타입은 불공변성(Invariance) 을 가진다. 즉, Collection<Integer>
와 Collection<Object>
는 아무런 관계가 없다. 제네릭이 도입되기 전에는 가능했던 작업이 이제는 불가능해진 경우가 발생할 수 있다.
{% endhint %}
아래와 같이 printCollection
메서드를 작성하고 List<Integer>
를 전달하려고 하면, 컴파일 오류가 발생한다.
@Test
void genericTest() {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list); // 컴파일 오류: Collection<Object>는 Collection<Integer>와 호환되지 않음
}
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
Collection<Object>
는Collection<Integer>
의 상위 타입이 아니기 때문에, 제네릭 타입에서는 서로 호환되지 않는다. 이로 인해printCollection
메서드에List<Integer>
를 전달하려고 하면 컴파일 오류가 발생한다.- 이는 제네릭의 불공변성으로 인한 문제이다.
와일드카드의 도입
위와 같은 문제를 해결하기 위해 와일드카드(?)
가 도입되었다. 와일드카드를 사용하면 제네릭 타입을 보다 유연하게 사용할 수 있으며, 모든 타입의 컬렉션에서 공통으로 사용할 수 있는 메서드를 작성할 수 있다.
와일드카드 타입 사용 예제
void printCollection(Collection<?> c) {
for (Object e : c) { // 와일드카드 타입으로 컬렉션의 요소를 다룸
System.out.println(e);
}
}
@Test
void genericTest() {
List<Integer> list = Arrays.asList(1, 2, 3);
printCollection(list); // 이제 컴파일 오류 없이 호출 가능
}
Collection<?>
는 비한정적 와일드카드 타입2으로, 어떤 타입의 컬렉션이라도 인자로 받을 수 있다.List<Integer>
,List<String>
,List<Object>
등 다양한 타입의 컬렉션을 모두 전달할 수 있어, 보다 유연한 메서드를 작성할 수 있다.- 단, 와일드카드 타입에서는 컬렉션에 새로운 요소를 추가할 수 없고,
null
만 허용됩니다. 이는 타입 안전성을 유지하기 위함이다.
와일드 카드에 대한 설명 블로그 : https://mangkyu.tistory.com/241
{% hint style="info" %}
로 타입은 제네릭(Generic) 타입에서 타입 매개변수를 전혀 사용하지 않은 때를 말한다. (ex)List
). 또한 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다.
{% endhint %}
로 타입의 단점을 나타내주는 몇 가지 예시를 들어보자.
// Stamp 인스턴스만 취급한다.
private final Collection stamps = ...;
stamps.add(new Coin(...));
실수로 도장 대신 동전을 넣어도 오류 없이 컴파일 되고 실행되는 문제가 발생한다.
for(Iterator i = stamps.iterator(); i.hasNext(); ){
Stamp stamp = (Stamp) i.next(); //ClassCastException
stamp.cancle();
}
오류는 이상적으로 컴파일할때 발견하는 것이 좋지만, 로 타입을 사용한다면 런타임에나 오류를 발견할 수 있다.
제네릭을 지원한 이후에는 매개변수화된 컬렉션 타입으로 타입 안전성을 확보한다. 제네릭을 사용하면 타입 선언 자체에 Stamp 인스턴스만 취급한다라는 것이 녹아든다.
private final Collection<Stamp> stamps = ...;
stamps.add(new Coin()); // 컴파일 오류 발생
컴파일 오류가 바로 발생한다. 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환
을 추가 하여 절대 실패하지 않음을 보장한다.
제네릭을 사용하여 컬렉션의 타입을 지정함으로써, 컴파일러가 타입 안전성을 보장할 수 있다.
로 타입을 사용한 코드
import java.util.ArrayList;
import java.util.List;
public class RawTypeExample {
public static void main(String[] args) {
// 로 타입 사용
List list = new ArrayList(); // 타입 매개변수를 지정하지 않음
list.add("Hello");
list.add(123); // 문자열과 숫자를 모두 추가할 수 있음
// 컬렉션의 요소를 가져올 때마다 타입 캐스팅이 필요함
String str = (String) list.get(0);
Integer num = (Integer) list.get(1);
System.out.println(str); // 출력: Hello
System.out.println(num); // 출력: 123
}
}
위 예제에서 List
는 로 타입으로 사용되었다. 이 경우, 리스트에 어떤 타입의 객체든 추가할 수 있기 때문에, 각 요소를 꺼낼 때 타입 캐스팅이 필요하다. 만약 잘못된 타입으로 캐스팅하려고 하면 ClassCastException
이 발생할 수 있다.
제네릭을 사용한 코드
import java.util.ArrayList;
import java.util.List;
public class GenericTypeExample {
public static void main(String[] args) {
// 제네릭을 사용하여 List<String>으로 선언
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 컴파일 오류: 정수는 추가할 수 없음
// 타입 캐스팅이 필요 없음
String str = list.get(0);
System.out.println(str); // 출력: Hello
}
}
위 코드에서 List<String>
은 제네릭 타입을 사용하여 선언되었다.
이제 이 리스트에는
String
타입만 저장할 수 있으며, 컴파일 시점에 타입 오류가 발생할 가능성을 줄일 수 있다. 요소를 가져올 때도 타입 캐스팅이 필요하지 않다.
- 타입 안전성이 확보된다.
private final Collection<Stamp> stamps = ...;
위 예제처럼 컴파일러가 stamps
에는 Stamp
의 인스턴스만 넣어야 함을 인지하기 때문에, 다른 엉뚱한 타입의 인스턴스는 컴파일 에러를 내뱉게 된다.
올바른 인스턴스라면 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하기 때문에 그 이후부터는 정상적으로 작동할 것이다.
- 로 타입은 제네릭이 안겨주는 안전성과 표현력이 없다.
하지만 그럼에도 로 타입이 존재하고 있는 것은, 로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야 했기 때문이다.
따라서 이러한 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거방식을 도입하게 된다. 소거 방식이란, 런타임에 타입 정보가 사라지는 것을 의미한다.
로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다. 제네릭이 등장하기 이전의 코드와의 호환성을 위해서 로 타입이 남겨져 있다.
List
와 같은 로 타입은 권장하지 않지만 List<Object>
는 괜찮다. 모든 타입을 허용한다는 의사를 컴파일러에게 명확하게 전달한 것이기 때문이다.
그렇다면
List와 List<Object>의 차이
는 무엇일까?
List
는 제네릭 타입과 무관한 것이고, List<Object>
는 모든 타입을 허용한다는 것입니다.
다시 말해서 매개변수로 List
를 받는 메서드에 List<String>
을 넘길 수 있지만, 제네릭의 하위 규칙 때문에 List<Object>
를 받는 메서드에는 매개변수로 넘길 수 없다.
List<String>
은 로 타입인 List
의 하위 타입이지만 List<Object>
의 하위 타입은 아니기 때문이다. 위의 공변 설명에서 함 그래서 List<Object>
와 같은 매개변수화 타입을 사용할 때와 달리 List
같은 로 타입을 사용하면 타입 안전성을 잃게 된다.
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
// 로 타입
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
위의 코드는 컴파일은 성공하지만 로 타입인 List
를 사용하여 unchecked call to add(E) as a member of raw type List...
라는 경고 메시지가 발생된다. 그런데 실행을 하게 되면 strings.get(0)
의 결과를 형변환하려 할 때 ClassCastException
이 발생한다. Integer를 String으로 변환하려고 시도했기 때문이다.
위 코드를
List<Object>
로 변경하면?
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0);
}
// List<Object>
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
컴파일 오류가 발생하며 incompatible types: List<String> cannot be converted to List<Object>...
라는 메시지가 출력된다. 실행 시점이 아닌 컴파일 시점에 오류를 확인할 수 있어 보다 안전하다.
타입 매개변수를 사용해 실제 타입으로 지정된 제네릭 타입
List<String> list = new ArrayList<>(); // List의 타입 매개변수로 String을 지정
list.add("Hello"); // String 타입의 요소를 추가
매개변수화 타입에서 구체적으로 지정된 타입
List<E>
// 매개변수화 타입에서 String이 실제 타입 매개변수
List<String> list = new ArrayList<>(); // 여기서 String이 실제 타입 매개변수로 사용됨
타입 매개변수를 가지는 클래스나 인터페이스
E
// 제네릭 클래스를 정의할 때 타입 매개변수 T를 사용
public class Box<T> {
private T content; // T 타입의 변수를 선언
public void setContent(T content) {
this.content = content; // T 타입의 값을 설정
}
public T getContent() {
return content; // T 타입의 값을 반환
}
}
// Box 클래스를 사용하는 예제
Box<Integer> intBox = new Box<>(); // Integer 타입의 Box 생성
intBox.setContent(123); // Integer 값 설정
System.out.println(intBox.getContent()); // 출력: 123
Box<String> strBox = new Box<>(); // String 타입의 Box 생성
strBox.setContent("Hello, Generics"); // String 값 설정
System.out.println(strBox.getContent()); // 출력: Hello, Generics
제네릭 타입 또는 제네릭 메서드에서 사용되는 타입 매개변수
// 제네릭 클래스 Box의 정의에서 E가 정규 타입 매개변수
public class Box<E> {
private E content; // E 타입의 변수를 선언
public void setContent(E content) {
this.content = content; // E 타입의 값을 설정
}
public E getContent() {
return content; // E 타입의 값을 반환
}
}
타입 매개변수를
?
로 지정하여 어떤 타입이든 허용함List<?>
// 와일드카드 타입을 사용한 메서드 정의
public void printList(List<?> list) {
// List<?>는 어떤 타입의 리스트든 받을 수 있음
for (Object elem : list) {
// 와일드카드 타입이므로 요소를 Object로 취급
System.out.println(elem);
}
}
// 사용 예제
List<String> stringList = Arrays.asList("Apple", "Banana", "Orange");
List<Integer> intList = Arrays.asList(1, 2, 3);
printList(stringList); // 출력: Apple, Banana, Orange
printList(intList); // 출력: 1, 2, 3
제네릭 타입에서 타입 매개변수를 사용하지 않은 형태
List
// 제네릭 타입의 타입 매개변수를 사용하지 않은 경우
List rawList = new ArrayList(); // 로 타입으로 정의
rawList.add("Hello"); // String 타입의 값 추가
rawList.add(123); // Integer 타입의 값 추가
// 로 타입 사용 시 컴파일러가 타입 안전성을 보장하지 않음
for (Object obj : rawList) {
System.out.println(obj); // 출력: Hello, 123
}
특정 타입 또는 그 하위 타입으로 제한된 타입 매개변수
<E extends Number>
// 타입 매개변수 E가 Number 또는 그 하위 타입이어야 함
public <E extends Number> void printNumber(E number) {
System.out.println(number); // Number 타입의 값을 출력
}
// 사용 예제
printNumber(123); // 출력: 123 (Integer)
printNumber(45.67); // 출력: 45.67 (Double)
// printNumber("Hello"); // 컴파일 에러: String은 Number의 하위 타입이 아님
자기 자신을 타입 매개변수로 참조하는 타입 한정
<T extends Comparable<T>>
// T가 Comparable<T> 인터페이스를 구현해야 함
public class Node<T extends Comparable<T>> {
private T value; // T 타입의 값을 저장
private Node<T> next; // 다음 노드를 가리키는 포인터
public Node(T value) {
this.value = value; // 노드의 값을 설정
}
public T getValue() {
return value; // 노드의 값을 반환
}
}
// 사용 예제
Node<Integer> node = new Node<>(10); // Integer 타입의 노드 생성
System.out.println(node.getValue()); // 출력: 10
와일드카드 타입이 특정 타입 또는 그 하위/상위 타입으로 제한됨
List<? extends Number>
// Number 또는 그 하위 타입을 요소로 갖는 리스트를 인자로 받음
public void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num); // Number 타입의 요소를 출력
}
}
// 사용 예제
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printNumbers(intList); // 출력: 1, 2, 3
printNumbers(doubleList); // 출력: 1.1, 2.2, 3.3
타입 매개변수를 사용하는 메서드
static <E> List<E> asList(E[] a)
// 제네릭 타입 매개변수를 사용하는 메서드 정의
public static <E> List<E> asList(E[] array) {
return Arrays.asList(array); // 배열을 리스트로 변환하여 반환
}
// 사용 예제
String[] stringArray = {"Hello", "World"};
List<String> stringList = asList(stringArray); // 제네릭 메서드를 사용하여 리스트 생성
System.out.println(stringList); // 출력: [Hello, World]
런타임에 제네릭 타입 정보를 제공하기 위해 사용하는 클래스 리터럴
String.class
// 런타임에 타입 정보를 얻기 위해 사용하는 타입 토큰
public <T> T createInstance(Class<T> clazz) throws Exception {
// 클래스 타입 T를 기반으로 새로운 인스턴스를 생성
return clazz.getDeclaredConstructor().newInstance();
}
// 사용 예제
String str = createInstance(String.class); // String 클래스의 인스턴스 생성
System.out.println(str); // 출력: 빈 문자열 (String의 기본 생성자 사용)
위의 예제들은 제네릭 프로그래밍의 다양한 개념을 설명하며, 각각의 코드에 대한 주석을 통해 해당 개념이 어떻게 적용되는지 쉽게 이해할 수 있도록 돕습니다.
제네릭을 사용하는 이유는 타입 안전성을 보장하고, 코드의 가독성과 유지보수성을 높이기 위함이다.
하지만 모든 상황에서 특정 타입을 명시할 수 없는 경우가 있기 때문에 비한정적 와일드카드(
?
)와 로 타입(Raw Type)이 존재한다.
- 비한정적 와일드카드 타입은 제네릭 타입 매개변수가 무엇이든 상관없이 사용할 수 있도록 한다.
- 제네릭 타입의 안전성을 유지하면서도, 실제 타입 매개변수에 의존하지 않는 메서드를 작성할 수 있다.
Set<?>
와 같은 비한정적 와일드카드 타입은Set<String>
,Set<Integer>
등 어떤 타입의Set
이라도 사용할 수 있다.- 하지만 와일드카드 타입에서는
null
외에는 어떤 원소도 추가할 수 없다.
public class TypeTest {
private static void addToWildList(final List<?> list, final Object o) {
// 컴파일 오류: 제네릭 타입에 의존성이 있음
// list.add(o);
// null은 허용됨
list.add(null);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String s = "kimtaeng";
addToWildList(list, s); // Okay! 메서드 호출 자체는 문제없음
}
}
- 로 타입은 제네릭 도입 이전의 컬렉션 타입으로, 타입 안전성을 보장하지 않는다.
- 로 타입을 사용할 경우, 어떤 타입의 객체든 추가할 수 있으며, 컴파일 시점에 타입 검사가 이루어지지 않아 런타임 오류의 가능성이 높다.
- 제네릭 타입의 타입 정보가 런타임에 지워지기 때문에
Set<String>
과Set
은 동일하게 취급된된다.
public class TypeTest2 {
public static void main(String[] args) {
List raw = new ArrayList<String>(); // Okay! 로 타입은 타입 안전성을 제공하지 않음
List<?> wildcard = new ArrayList<String>(); // Okay! 비한정적 와일드카드
raw.add("Hello"); // Okay! 로 타입은 어떤 타입의 원소도 추가 가능
raw.add(1); // 컴파일러가 타입 검사를 하지 않기 때문에 가능
// wildcard.add("Hello"); // 컴파일 오류: 비한정적 와일드카드 타입은 null 외에 추가할 수 없음
List<String> list = new ArrayList<>(); // 제네릭 타입 사용
list.add("Hello"); // String 타입의 원소만 추가 가능
// list.add(1); // 컴파일 오류: 정수는 추가할 수 없음
// 메서드 호출은 가능
wildcard.size(); // Okay!
wildcard.clear(); // Okay!
}
}
특성 | 로 타입 (Set ) |
비한정적 와일드카드 (Set<?> ) |
---|---|---|
타입 안전성 | 보장되지 않음 | 보장됨 |
타입 불변식 유지 | 위반하기 쉬움 | 타입 불변식 유지 |
원소 추가 | 어떤 타입의 원소도 추가 가능 | null 외에는 추가할 수 없음 |
메서드 호출 | 타입에 관계없이 사용 가능 | 제네릭 타입에 의존하지 않는 메서드만 사용 가능 |
사용 가능 상황 | 하위 버전과의 호환성 필요 시, 클래스 리터럴, instanceof |
제네릭 타입에 의존하지 않는 메서드 작성 시 |
제네릭 타입은 클래스 리터럴3에서 사용할 수 없다. List.class
와 같은 로 타입만 사용할 수 있으며, List<String>.class
나 List<?>.class
는 허용되지 않는다.
Class<List> listClass = List.class; // Okay!
제네릭 타입 정보는 런타임에 제거되므로, instanceof
연산자는 로 타입이나 비한정적 와일드카드 타입에서만 사용할 수 있다. Set<?>
을 사용해 타입 캐스팅을 할 수 있다.
if (o instanceof Set) {
Set<?> s = (Set<?>) o; // 로 타입 대신 비한정적 와일드카드 타입으로 형변환
}
- 로 타입은 타입 안전성이 보장되지 않기 때문에, 사용을 피해야 합니다. 로 타입을 사용하는 주요 이유는 하위 버전과의 호환성 때문입니다.
- **비한정적 와일드카드 타입 (
<?>
)**은 제네릭 타입의 타입 매개변수를 알 수 없거나 신경 쓰지 않아도 될 때 사용합니다. 타입 안전성을 유지하면서 제네릭을 사용할 수 있습니다. - 로 타입과 비한정적 와일드카드 타입은 클래스 리터럴과
instanceof
연산자와 같은 특정 상황에서 로 타입이 필요할 때만 사용해야 하며, 그 외에는 제네릭 타입을 사용하는 것이 좋습니다.
{% hint style="danger" %} 로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. 오직 하위 버전과 호환하기 위해서 남아있다. {% endhint %}
- 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
Set<Object>
는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고,Set<?>
는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.- 이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다. Set<Object>와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
참고 글 :
[이펙티브 자바 3판] 아이템 26. 로 타입은 사용하지 말라
[JAVA] 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기(공변과 불공변, 상한 타입과 하한 타입)
[ java] generic type erasure란 무엇이띾
Footnotes
-
모든 타입을 대신할 수 있는 와일드카드 타 ↩
-
Java 제네릭에서 사용하는 와일드카드의 한 종류로, 어떤 타입이든 수용할 수 있는 제네릭 타입을 나타냅니다. 비한정적 와일드카드는 물음표 기호
?
로 표현되며, 구체적인 타입을 알 수 없거나 타입에 상관없이 작업을 수행할 때 사용됩니다.비한정적 와일드카드의 사용법
- 표기법:
?
기호를 사용하여 나타냅니다. 예를 들어,Collection<?>
는 "아무 타입의Collection
"을 의미합니다. - 의미: 어떤 타입이든 수용할 수 있다는 의미입니다. 즉,
Collection<Integer>
,Collection<String>
,Collection<Object>
와 같은 다양한 타입의 제네릭 컬렉션을 인자로 받을 수 있습니다.
- 표기법:
-
Java에서 클래스나 인터페이스의 타입 정보를 나타내기 위한 특별한 문법입니다. 클래스 리터럴을 사용하면 클래스의 타입 정보를 런타임에 참조할 수 있습니다. 이는 클래스 파일의 메타데이터를 사용하여 객체의 인스턴스를 생성하거나, 리플렉션(Reflection) 작업 등을 수행할 때 유용합니다.
기본 문법:타입이름.class
↩