제네릭을 사용하면 담을 수 있는 타입을 컴파일러에 알려주어 클래스나 인터페이스가 사용할 데이터 타입을 나중에 지정할 수 있다.
이러면 컴파일러가 알아서 형변환 코드를 추가해주어 코드의 재사용성을 높이고 엉뚱한 타입 객체를 넣으려는 시도를 차단해 안정성 높은 프로그램을 만들어준다.
제네릭 지원 전의 컬렉션
import java.util.ArrayList;
public class NonGenericExample {
public static void main(String[] args) {
// 비제네릭 컬렉션 생성
ArrayList list = new ArrayList();
// 컬렉션에 요소 추가
list.add("Hello");
list.add("World");
list.add(123); // Integer도 추가 가능 (모든 객체가 가능)
// 컬렉션에서 요소 꺼내기 (캐스팅 필요)
String firstElement = (String) list.get(0);
String secondElement = (String) list.get(1);
Integer thirdElement = (Integer) list.get(2);
// 출력
System.out.println(firstElement); // Hello
System.out.println(secondElement); // World
System.out.println(thirdElement); // 123
}
}
제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때 마다 형변환이 필요했다.
만약 엉뚱한 타입의 객체를 넣는다면 런타임에 형변환 오류가 뜬다.
제네릭을 활용하면 타입 안정성을 확보할 수 있다.
import java.util.ArrayList;
public class GenericExample {
public static void main(String[] args) {
// 제네릭 컬렉션 생성
ArrayList<String> list = new ArrayList<>();
// 컬렉션에 요소 추가
list.add("Hello");
list.add("World");
// list.add(123); // 컴파일 오류: String 타입만 허용
// 컬렉션에서 요소 꺼내기 (캐스팅 불필요)
String firstElement = list.get(0);
String secondElement = list.get(1);
// 출력
System.out.println(firstElement); // Hello
System.out.println(secondElement); // World
}
}
엉뚱한 타입의 인스턴스를 넣으려 하면 컴파일 오류가 발생한다.
컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가해준다.
제네릭 타입(Generic type)
제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입이라 한다.
제네릭 타입은 클래스(혹은 인터페이스) 이름이 나오고 꺽쇠괄호 안에 실제 타입 매개변수를 나열하여 일련의 매개변수화 타입을 정의한다.
📌 매개변수화 타입 / 실제 타입 매개변수
List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입이다.
- String이 정규(formal)타입 매개변수 E에 해당하는 실제 타입 매개변수다.
📌 제네릭 타입 / 정규 타입 매개변수
List<E>은 제네릭 타입이다.
E는 List<E>에서 정규 타입 매개변수다.
📌 raw type
제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 말한다.
- 제네릭 타입을 하나 정의하면 그에 딸린 raw type도 함께 정의된다.
- List<E>의 raw type은 List다
- 타입선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다.
- 제네릭이 나오기 전 코드와 호환성을 위해 있는 개념이라 할 수 있다.
- raw type을 쓰면 제네릭이 안겨주는 안정성과 표현력을 잃게 되어 사용하지 않도록 한다.
🐣 비검사 경고
javac 명령줄 인수에 -Xlint:uncheck 옵션을 추가하면 비검사 경고를 알려준다.
Set<Lark> exaltation = new HashSet();
- @SuppressWarnings("unchecked"): 경고를 제거할 순 없지만 해당 타입은 안전하다고 표시하는 것
제네릭 클래스
// 제네릭 클래스 선언
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public static void main(String[] args) {
// Integer 타입의 Box 인스턴스 생성
Box<Integer> integerBox = new Box<>();
integerBox.setContent(123);
System.out.println("Integer Box Content: " + integerBox.getContent());
// String 타입의 Box 인스턴스 생성
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello");
System.out.println("String Box Content: " + stringBox.getContent());
}
}
제네릭 인터페이스
// 제네릭 인터페이스 선언
public 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;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
public static void main(String[] args) {
// String, Integer 타입의 OrderedPair 인스턴스 생성
Pair<String, Integer> p1 = new OrderedPair<>("One", 1);
System.out.println("Key: " + p1.getKey() + ", Value: " + p1.getValue());
// String, String 타입의 OrderedPair 인스턴스 생성
Pair<String, String> p2 = new OrderedPair<>("Hello", "World");
System.out.println("Key: " + p2.getKey() + ", Value: " + p2.getValue());
}
}
비한정적 와일드 카드와 타입 파라미터
<?>: 비한정적 와일드카드 (Unbounded Wildcard)
<T>: 타입 파라미터 (Type Parameter)
<?>는 컬렉션의 타입을 명시하지 않아 모든 타입을 수용하지만, 내부 요소의 타입을 알 수 없기 때문에 요소 추가 작업은 제한된다. 주로 읽기 전용 작업에 사용된다.
<T>는 특정 타입을 지정하며, 호출 시 구체적인 타입으로 대체된다. 타입 안전성을 유지하면서 읽기 및 쓰기 작업 모두에 사용할 수 있다.
<?>와 <T>는 각각의 목적과 사용 방식이 다르며, 제네릭 프로그래밍에서 서로 다른 상황에 유용하게 사용된다.
아래에서 코드와 함께 보자.
비한정적 와일드카드 (<?>)
특정 타입에 구애받지 않고 모든 타입을 수용할 수 있는 와일드카드
- 용도: 불특정 타입의 객체를 처리하는 컬렉션을 나타내며, 주로 읽기 전용 작업에 사용된다.
public void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
// list.add("Hello"); // 컴파일 오류: 비한정적 와일드카드는 추가를 허용하지 않음
}
타입 파라미터 (<T>)
메서드, 클래스 또는 인터페이스가 사용할 수 있는 특정 타입을 나타내는 기호
- 여기서 T는 임의의 타입 이름 일반적으로 T는 Type의 약자다.
- 상황에 따라 E(Element), K(Key), V(Value), N(Number) 등의 이름도 사용된다.
- 용도: 특정 타입으로 제한된 객체를 처리하는 제네릭 메서드나 클래스 정의에 사용된다. 호출 시 구체적인 타입으로 대체된다.
public <T> void printList(List<T> list) {
for (T element : list) {
System.out.println(element);
}
// list.add("Hello"); // 컴파일 오류: T가 String인지 알 수 없음
}
public <T> void addToList(List<T> list, T element) {
list.add(element);
}
제네릭에서는 배열보다는 리스트
공변(covariant) vs 불공변(invariant)
배열은 변할수 있고 리스트는 변할 수 없다.
Object[] objectArray = new Long[1]; // 공변
objectArray[0] ="타입 달라서 못 넣음"; // 런타임 에러 java.lang.ArrayStoreException: java.lang.String
List<Object> ol = new ArrayList<Long>(); // 불공변 컴파일 에러
컴파일 시에 바로 알 수 있는 리스트를 사용하자.
📌 불공변(invariant)
Type1과 Type2가 있을 때 List<Type1>은 List<Type2>의 하위 타입도 상위 타입도 아니다.
- List<Object>에는 어떤 객체든 넣을 수 있지만, List<String>에는 문자열만 넣을 수 있다.
📌 실체화(reify)
- 배열: 런타임에도 자신이 담기로 한 원소 타입을 인지하고 확인한다.
- 리스트: 원소 타입을 컴파일 타임에만 검사하고 런타임에는 알수 없다.
또한 배열과 제네릭은 잘 어우러지지 못한다.(new List<E>[] 컴파일 시 제네릭 배열 생성 오류)
- 타입 안전하지 않기 때문
public class Chooser{
private final Object[] choiceArray;
public Chooser(Collection choices){
choiceArray = choices.toArray(); // 다른 타입의 원소가 있으면 형변환 오류
}
public Object choose(){
Random rnd = ThreadLocalRandom.current();
return choiceArray[rnd.nextInt(choiceArray.length)];
}
}
public class Chooser<T>{
private final T[] choiceArray;
public Chooser(Collection<T> choices){
choiceArray = (T[]) choices.toArray();
}
}
아래와 같이 리스트를 사용하여 더 안정성 있게 바꿀 수 있다.
public class Chooser<T> {
private final List<T> choiceArray;
public Chooser(Collection choices){
choiceArray = new ArrayList<>(choices); // 다른 타입의 원소가 있으면 형변환 오류
}
public T choose(){
Random rnd = ThreadLocalRandom.current();
return choiceList.get(rnd.nextInt(choiceList.size()));
}
}
제네릭 클래스 만들기
아래는 제네릭을 사용하지 않은 Stack이다.
public class Stack {
private Object[] elements;
private int size = 0 ;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
제네릭을 사용해서 바꾸어 보자.
클래스 선언에 타입 매개변수를 추가하고 배열의 타입을 맞추어 주면 한 군데서 컴파일 오류가 날 것이다.
new E[DEFAULT_INITIAL_CAPACITY]; // Type parameter 'E' cannot be instantiated directly
E와 같은 실체화 불가 타입은 배열을 만들 수 없다.
배열을 사용하는 코드를 제네릭으로 만들려 할 때는 이 문제가 항상 발목을 잡는다.
해결방법으로 2가지를 살펴보자.
1. Object 배열을 생성으로 우회한 후 경고 숨키기
Object 배열을 생성한 후 캐스팅 하면 경고가 뜬다.
(E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // Unchecked cast: 'java.lang.Object[]' to 'E[]'
@SuppressWarnings("unchecked")를 사용해서 경고를 숨길 수 있다.
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안정성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다.
@SuppressWarnings("unchecked")
public Stack(){
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public class Stack<E> {
private E[] elements;
private int size = 0 ;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안정성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다.
@SuppressWarnings("unchecked")
public Stack(){
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e){
ensureCapacity();
elements[size++] = e;
}
public E pop(){
if(size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
- 장점
- 가독성: E[]로 선언하여 오직 E타입 인스턴스만 받음을 확실히 어필
- 코드가 더 짧다.
- 배열 생성 시 단 한번만 해주면 된다.
- 단점
- 힙 오염(heap pollution)을 일으킬 수 있다.
2. Object[]로 사용하기
두 번째 방법은 배열을 Object로 만드는 것이다. Object 배열로 구현했을 때는 아래와 같은 경고가 뜬다.
public E pop(){
if(size == 0)
throw new EmptyStackException();
E result = (E) elements[--size]; // Unchecked cast: 'java.lang.Object' to 'E'
elements[size] = null;
return result;
}
public class Stack<E> {
private Object[] elements;
private int size = 0 ;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e){
ensureCapacity();
elements[size++] = e;
}
public E pop(){
if(size == 0)
throw new EmptyStackException();
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unckecked") E result = (E) elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
- 단점
- 배열에서 원소를 읽을 때 마다 추가 작업이 필요하다.
제네릭 메서드 만들기
메서드도 제네릭으로 만들 수 있다.
매개변수화 타입을 받는 정적 유틸리티 메서드는 보통 제네릭이다.
public static Set union(Set s1, Set s2){ // Raw use of parameterized class 'Set'
Set result = new HashSet(s1); // Unchecked call to 'HashSet(Collection<? extends E>)' as a member of raw type 'java.util.HashSet'
result.addAll(s2); // Unchecked call to 'addAll(Collection<? extends E>)' as a member of raw type 'java.util.Set'
return result;
}
위의 메서드는 몇몇의 경고가 뜬다.
메서드 선언 부에 타입 매개변수 목록을 명시해주어 경고를 없애보도록 하자.
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
이렇게 제네릭을 이용해서 메서드를 만들면 경고없이 컴파일되며, 타입이 안전하고, 쓰기도 쉽다.
타입 매개변수 목록 위치는 메서드의 제한자와 반환 타입 사이에 온다.
제네릭 싱글턴 팩터리 패턴
identity function(항등함수)를 담은 클래스를 만들어보자.
항등함수 객체는 상태가 없어 요청할 때 마다 새로 생성하는 것은 낭비다.
제네릭을 사용하지 않으면 타입별로 하나씩 만들어야 했겠지만,
소거 방식을 사용한 덕에 제네릭 싱글턴 하나면 충분하다.
📌 항등함수: 입력 값을 수정 없이 그대로 반환하는 특별한 함수
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction(){
return (UnaryOperator<T>) IDENTITY_FN;
}
한정적 와일드 카드로 유연하게
push 형태: 생성자 ➡️ <? extends T>
public class Stack<E>{
public void pushAll(Iterable<E> src){
for(E e : src)
push(e);
}
}
위와 같이 타입 파라미터(<E>) 를 사용했을 때는 타입이 한정되어 진다.
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers); // 컴파일 오류
push할 때를 생각해보면 Number는 Integer의 상위 타입이니 논리적으로 문제 없을 것이라고 생각했지만 타입 파라미터의 불공변 특징으로 오류 메시지가 뜨게 된다. 이런 상황에서 한정적 와일드카드 타입이란 특별한 매개변수 타입을 사용하여 해결할 수 있다.
public void pushAll(Iterable<? extends E> src){
for(E e : src)
push(e);
}
Pop 형태: 소비자 ➡️ <? super T>
이번엔 pop의 형태를 생각해보자.
public void popAll(Collection<E> dst){
while (!isEmpty())
dst.add(pop());
}
똑같이 <? extends E>를 사용하면 될까? 적용하기전에 활용할 때를 생각해보자.
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
Stack<Number> 의 원소를 Object용 컬렉션으로 옮기려 한다고 생각할 수 있다. 이번에는 반대로 E의 상위 타입의 Collection을 사용해야하는 것이다.
public void popAll(Collection<? super E> dst){
while (!isEmpty())
dst.add(pop());
}
이처럼 유연성을 극대화하려면 소비자용 입력 매개변수에 와일드카드 타입을 사용하면 된다.
🐣 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드카드 타입을 써도 좋을 게 없다.
매개변수화 타입 T가
생산자라면 <? extends T>를 사용하고
소비자라면 <? super T>를 사용한다.
pushAll의 src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 src의 적절한 타입은 Iterable<? extends E>
popAll의 dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 dst의 적잘한 타입은 Collection<? super E>
예시 살펴보기
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
여기서 만약 extends Comparable<? super E>를 하지 않았다면 사용하려는 타입의 Comparable<사용하려는 타입>을 매번 구현해주어야 하게 된다.
public interface Comparable<E>
public interface Delayed extends Comparable<Delayed>
public interface ScheduledFuture<V> extends Delayed, Future<V>
위와 같은 상황에서는 아래와 같이 사용할 수 있게 된다.
List<ScheduledFuture<?>> scheduledFutures = ...;
...max(scheduledFutures);
와일드 카드와 타입 매개변수 둘 다 가능할 때
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
메서드 선언에 타입 매개변수가 한번만 나오면 와일드 카드로 대체하기
다만 이렇게 했을 때 컴파일이 되지 않을 수 있다.
public static void swap(List<?> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
와일드카드가 쓰기작업이 불가능하기 때문이다.
이럴때는 private 도우미 메서드를 따로 작성해서 활용해주면 된다.
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.set(j, list.get(i)));
}
제네릭과 가변인수 조심하기
제네릭과 가변인수(varargs)를 혼용하면 타입 안정성이 깨진다.
가변인수를 사용하면 클라이언트가 메서드에 넘기는 인수의 개수를 클라이언트가 조절할 수 있게 된다.
다만, 이 가변인수 메서드를 호출하면 가변인수를 담기 뒤한 배열이 자동으로 하나 만들어진다.
때문에 가변인수 매개변수에 제네릭이나 매개변수화 타입이 포함되면 알기 컴파일 경고가 생긴다.
실체화 불가 타입을 사용하면서 내부적으로 배열이 만들어지기 때문에 컴파일 경고가 발생하는 것 같다.
static void danerous(List<String>... stringLists){
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
형변환하는 것이 없는데 ClassCastException이 던져진다. 마지막 줄에 컴파일러가 생성한 보이지 않는 형변환이 숨어 있기 때문이다.
이처럼 제네릭 가변인수 배열 매개변수에 값을 저장하는 것은 타입 안정성이 깨지게 된다.
List<String>[] lists = new List<String>[]; // 컴파일 에러
위 코드처럼 직접 생성은 컴파일 에러를 뜨게하지만
아래코드는 왜 경고만 띄우고 제네릭 가변인수 매개변수를 받는 메서드를 선언할 수 있게 했을까?
static void danerous(List<String>... stringLists){ ...
이유는 제네릭이나 매개변수화 타입의 가변인수 매개변수를 받는 메서드가 실무에서 유용하기 때문이다.
때문에 모순을 수용하게 되었다.
예시
Arrays.asList(T... a)
Collections.addAll(Collection<? super T> c, T... elements)
EnumSet.of(E first, E... rest)
다행히 위의 메서드들은 타입이 안전하다고 한다.
메서드가 안전한가?
메서드가 안전한지 확인하려면 두 가지를 생각해보자.
가변인수 메서드를 호출할 때 가변인수 매개변수를 담는 제네릭 배열이 만들어지는데 이 배열에 대해 두 가지를 체크해보면 된다.
1. 메서드가 해당 배열에 아무것도 저장하지 않는가?
2. 해당 배열의 참조가 밖으로 노출되지 않는가?
📌 @SafeVarargs
메서드 작성자가 그 메서드가 타입 안전함을 보장한다고 알리는 장치
사용 시 메서드가 안전하지 않을 수 있다는 경고에서 벗어날 수 있다.
- 재정의할 수 없는 메서드에만 달아야한다.
➡️ java8에서는 정적 메서드와 final 인스턴스 메서드에만, java9 부터는private 인스턴스 메서드도 허용된다.
배열의 참조가 외부로 노출되는 상황에 대해 살펴보자
static <T> T[] toArray(T... args){
return args;
}
static <T> t[] pickTwo(T a, T c){
switch(ThreadLocalRandom.current().nextInt(3)){
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError();
}
public static void main(String[] args){
String[] attributes = pickTwo("가나", "다라", "마바"); // 형변환 실패
}
pickTwo는 Object[] 타입을 반환하기 때문에 형변환 실패가 된다.
타입 안전 이종 컨테이너
📌 타입안전 이종 컨테이너(type safe heterogeneous container pattern)
한 타입의 객체만 담을 수 있는 컨테이너가 아니라 여러 다른 타입 (이종)을 담을 수 있는 타입 안전한 컨테이너
타입 안전 이종 컨테이너 구현 방법
컨테이너가 아니라 키를 매개변수화 하기
📌 타입 토큰
컴파일타임 타입 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고 받는 class 리터럴
String.class ➡️ Class<String>
class의 리터럴 타입은 Class가 아닌 Class<T>다.
컨테이너에서 매개변수화할 수 있는 타입의 수가 제한된다.
- Set에는 원소의 타입을 뜻하는 하나의 타입 매개변수
- Map에는 키와 값을 뜻하는 2개의 매개변수
타입 안전 이종 컨테이너 패턴
키를 매개변수화한 다음, 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하면된다.
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
// Class 객체, 인스턴스 추가해서 관계 생성
// 키와 값 사이의 type linkage 정보는 사라지지만 getFavorite메서드로 관계를 살릴 수 있다.
public <T> void putFavorite(Class<T> type, T instance){ // class의 클래스가 제네릭
favorites.put(Object.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
}
public static void main(Stringp[] args){
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class);
String favoriteString = f.getFavorite(String.class);
int favoriteInteger = f.getFavorite(Integer.class);
System.out.printf("%s %x %s\n", favoritesString, favoriteInteger, favoriteClass.getName());
}
위의 코드 Favorites은 타입별로 즐겨찾는 인스턴스를 저장하고 검색할 수 있는 클래스다.
Favorites 인스턴스는 타입 안전하고 일반적은 Map과 달리 여러 가지 타입의 원소를 담을 수 있다.
따라서 Favorites는 타입 안전 이종 컨테이너라 할만하다.
코드 살펴보기
Map<Class<?>, Object>
에서 볼 수 있듯이 비한정적 와일드카드를 사용했다.
- 중첩된 와일드카드 사용
- 비한정적 와일드카드 타입이어서 해당 Map안에 입력이 되지 않을 것 이라고 생각들 수 있지만 와일드카드 타입이 중첩 되었음을 알아야한다. 맵이 아닌 키가 와일드 카드 타입인 것이다.
- 즉 모든 키가 서로 다른 매개변수화 타입일 수 있다는 뜻으로 다양한 타입을 지원할 수 있어지게 된다.
- favorites 맵의 값 타입
- favorites Map의 값 타입은 단순 Object다.
- 키와 값 사이의 타입 관계를 보증하지 않음을 뜻한다.
- 즉, 모든 값이 키로 명시한 타입임을 보증하지 않는다.
- 자바 타입 시스템에는 이 관계를 명시할 방법이 없지만 여기선 성립이 됨을 알 수 있다.
- favorites Map의 값 타입은 단순 Object다.
public <T> void putFavorite(Class<T> type, T instance){ // class의 클래스가 제네릭
favorites.put(Object.requireNonNull(type), instance);
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
- putFavorite
- Class 객체, 인스턴스 추가해서 관계 생성
- getFavorite
- 키와 값 사이의 type linkage 정보는 사라지지만 getFavorite메서드로 관계를 살릴 수 있다.
- 값의 타입을 Object에서 T로 반환해주어야한다.
- 클래스의 cast메서드를 사용해서 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
- 다만 코드 컴파일이 잘 된다면 ClassCastException의 문제가 없을 것이다.
- = favorites 맵 안의 값은 해당 키의 타입과 항상 일치함
- 클래스의 cast메서드를 사용해서 객체 참조를 Class 객체가 가리키는 타입으로 동적 형변환한다.
- T로 비검사 형변환할필요 없이 타입을 안전하게 만들 수 있는 방법이다.
🐣 cast 메서드
형변환 연산자의 동적 버전
단순 주어진 인수가 Class 객체가 알려주는 타입의 인스턴스인지를 검사한 다음,
맞다면 그 인수를 그대로 변환하고 아니면 ClassCastException을 던진다.
❓ 그대로 반환하기만 할꺼면 왜 cast 메서드를 사용하는가?
cast 메서드의 시그니처기ㅏ Class 클래스가 제네릭이라는 이점을 완벽히 활용하기 때문이다.
- cast의 반환타입은 Class 객체의 타입 매개변수와 같다.
public class Class<T>{ T cast(Object obj); }
- 제약
- 악의적인 클라이언트가 Class 객체를 raw 타입으로 넘기면 Favorites 인스턴스의 타입 안정성이 쉽게 깨진다.
- (컴파일할 때 경고뜸)
- putFavorite 메서드에서 instance의 타입이 type으로 명시한 타입과 같은지 확인하면 타입 안정성을 보장할 수 있다.
public <T> void putFavorite(Class<T> type, T instance){
favorites.put(Object.requireNonNull(type), type.cast(instance));
}
checkedSet, checkedList, checkedMap 같은 메서드들이 해당 방식을 적용했다.
나의 키워드
배열 vs List
타입안정성
Object[] arr = new String[]
arr.add(1); // 런타임 에러
와일드카드
배열같은경우 공변이라 Object[] 에 String[]을 담을 수 있지만
리스트는 불공변이라 List<Object>에 List<Stirng>을 담을 수 없다.
때문에 좀 더 유연하게 작성하고자 와일드 카드를 제공한다.
'language > JAVA' 카테고리의 다른 글
[JAVA] String 활용 (0) | 2024.06.22 |
---|---|
[JAVA] 배열, 스트림 활용 (0) | 2024.06.22 |
[JAVA][Exception] Map Duplicate key (0) | 2024.06.15 |
[JAVA] Garbage Collection (0) | 2024.03.26 |
[JAVA] Overriding 과 Overloading 의 차이점 (0) | 2024.02.19 |