제네릭의 필요성

  1. 타입 안정성 보장
    1. 컴파일 타임에 타입 체크 -> 실행 중 타입 에러 방지
    2. 런타임에 타입 소거 -> 실행 중 Object 타입으로 변환
  2. 형변환 필요 없음
  3. 재사용성 증가
// 제네릭 클래스

class Container<T, U> {
    private T first;
    private U second;
    
    public Container(T first, U second) {
        this.first = first;
        this.second = second;
    }
    
    public T getFirst() {
        return first;
    }
    
    public U getSecond() {
        return second;
    }
}
// 제네릭 메서드

class Util {
    public static <T> void printArray(T[] arr) {
        for (T item : arr) {
            System.out.println(item);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        String[] strArr = {"Java", "Spring", "Generic"};
        Integer[] intArr = {1, 2, 3, 4, 5};

        Util.printArray(strArr); // Java, Spring, Generic
        Util.printArray(intArr); // 1, 2, 3, 4, 5
    }
}

 

// 제네릭 클래스 활용 예시

class Box<T> {  // <T>는 Box가 사용할 타입을 의미
    private T value;

    public void setValue(T value) { // T 타입 값을 받을 수 있음
        this.value = value;
    }

    public T getValue() { // T 타입 값 반환 가능
        return value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>(); 
        stringBox.setValue("Hello");
        System.out.println(stringBox.getValue());  // 출력: Hello

        Box<Integer> intBox = new Box<>(); 
        intBox.setValue(100);
        System.out.println(intBox.getValue());  // 출력: 100
    }
}

 


 

제네릭의 상한/하한 제한와일드카드

  • <?> : 모든 타입을 허용 (? -> 와일드카드)
  • <? extends T> : T 또는 T의 하위 클래스만 허용
    • `List<? extends Number>` : Integer, Double, Float 등의 하위 타입 허용
    • 와일드카드 사용 시, "읽기 전용"
    • 어떤 타입이 입력될지 모르기 때문에 add 방지
    • 쓰기가 가능한 이유 : 어떤 타입이든 전달할 때 Number 클래스로 감싸서 전달하기 때문에 문제가 없다.
  • <? super T> : T 또는 T의 상위 클래스만 허용
    • `List<? super Integer>` : Integer의 상위 타입 (Number, Object) 허용
    • 와일드카드 사용 시, "쓰기 전용"
    • 가져온 데이터의 타입을 확신할 수 없기 때문에 Object로 데이터를 get
    • 읽기가 가능한 이유 : Integer의 상위 클래스라고 해도 Number, Object 등의 클래스가 오기 때문에, Integer형 데이터를 처리하는 데에 문제가 없다. (타입 안정성 문제 없음)
만약, Integer 클래스 상위에 Double 클래스가 있다는 비정상적인 상속 구조가 존재한다고 가정하면,
`? super Integer`가 실제로 List<Double>이 될 가능성이 있기 때문에 add(Integer)가 불가능해진다. (타입 안정성 깨짐)
class Printer<T> {
    public void printList(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }
}

 

 

 

와일드 카드 문제점의 해결 방법

1. Producer-Extends, Consumer-Super(PECS) 원칙 활용 (가장 권장)

-> 와일드 카드의 단점을 감수하고 필요한 기능에 맞는 방식 사용

데이터를 읽기만 하는 경우 -> `? extends T`

데이터를 쓰기만 하는 경우 -> `? super T`

 

2. 제네릭 타입 제한 + 캐스팅 조합 (안전하게 타입 변환)

public static <T> void copyList(List<T> dest, List<? extends T> src) {
    for (T item : src) {  // `? extends T` → `T` 업캐스팅
        dest.add(item);    // 안전하게 추가 가능
    }
}

 

3. 캐스팅을 감수하고 Object 타입의 제네릭을 사용 (가장 지양)

public static void processList(List<Object> list) {
    list.add("Hello"); // ✅ 추가 가능
}

 

 


 

제네릭 타입 배열 생성 시 주의점

아래와 같이 제네릭 타입 배열을 생성하려고 하면 컴파일 에러가 발생한다.

배열에 할당할 크기가 지정되어야 하는데, 어떤 타입이 배열에 저장될지 몰라서 크기를 지정할 수 없기 때문이다.

따라서 예시와 같이 객체를 저장하는 배열을 할당하고, 어떤 타입의 객체도 저장이 가능하도록 객체를 제네릭 타입으로 지정하면 된다.

// 불가능
T[] array = new T[10]; // 컴파일 에러

// Object 사용 -> 가능
Object[] array = new Object[10]; // 타입 캐스팅 필요

// 제네릭 컬렉션 사용 -> 가능
class GenericArray<T> {
    private List<T> list = new ArrayList<>();

    public void add(T item) {
        list.add(item);
    }

    public T get(int index) {
        return list.get(index);
    }
}