Dev Lang/JAVA

[Java] 제네릭 클래스의 이해와 활용

개발자 성현 2025. 1. 6. 00:59

학습 키워드

  • Generic Class & Interface
  • Type Parameter
  • Type Safety
  • Code Reusability
  • Bounded Type Parameter
  • Multiple Type Parameters
  • Raw Type
  • Type Inference

 

학습 내용

1. 제네릭 클래스의 기본 구조와 필요성

Java에서 제네릭이 없던 시절의 코드를 보면 다음과 같은 문제점이 있었다.

// 제네릭 이전의 코드
class OldBox {
    private Object item;

    public void setItem(Object item) {
        this.item = item;
    }

    public Object getItem() {
        return item;
    }
}

// 사용 예시
OldBox box = new OldBox();
box.setItem("Hello");  // String -> Object (자동 형변환)
String str = (String) box.getItem();  // Object -> String (명시적 형변환 필요)
Integer num = (Integer) box.getItem(); // 런타임 에러! ClassCastException

위 코드의 문제점:

  1. 모든 데이터를 Object로 다루어야 함
  2. 타입 캐스팅이 필수적
  3. 잘못된 타입 캐스팅 시 런타임 에러 발생
  4. 컴파일 시점에서 타입 안정성 보장 불가

 

2. 제네릭을 통한 타입 안정성 확보

제네릭을 사용하면 컴파일 시점에서 타입을 체크할 수 있다.

class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}

// 타입 안정성이 보장된 사용 예시
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");  // 컴파일 OK
stringBox.setItem(100);      // 컴파일 에러! - 타입 불일치
String str = stringBox.getItem();  // 캐스팅 불필요

 

3. 코드 재사용성 향상을 위한 제네릭 활용

하나의 제네릭 클래스로 다양한 타입을 처리할 수 있다.

// 다양한 타입의 페어를 처리하는 제네릭 클래스
class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
}

// 다양한 타입 조합으로 재사용
Pair<String, Integer> studentScore = new Pair<>("John", 95);
Pair<Integer, String> errorCode = new Pair<>(404, "Not Found");
Pair<Double, Double> coordinate = new Pair<>(37.5665, 126.9780);

 

4. 제네릭의 고급 활용

4.1 한정된 타입 매개변수 (Bounded Type Parameter)

특정 타입의 하위 타입으로만 제한할 수 있다.

class NumberBox<T extends Number> {
    private T number;

    public void set(T number) {
        this.number = number;
    }

    public double getDoubleValue() {
        return number.doubleValue();  // Number 클래스의 메서드 사용 가능
    }
}

NumberBox<Integer> intBox = new NumberBox<>();    // OK
NumberBox<Double> doubleBox = new NumberBox<>();  // OK
NumberBox<String> stringBox = new NumberBox<>();  // 컴파일 에러!

4.2 와일드카드를 활용한 유연성 확보

class DataProcessor {
    // 모든 타입의 Box 읽기 가능
    public static void printBox(Box<?> box) {
        System.out.println(box.getItem());
    }

    // Number의 하위 타입만 가능
    public static void processNumbers(Box<? extends Number> box) {
        Number num = box.getItem();  // 안전한 타입 변환
        System.out.println(num.doubleValue());
    }

    // Integer의 상위 타입만 가능
    public static void addInteger(Box<? super Integer> box) {
        box.setItem(10);  // Integer 타입 저장 가능
    }
}

 

5. 실전 활용 예시

제네릭을 활용한 데이터 구조 구현

class GenericStack<T> {
    private ArrayList<T> elements;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public GenericStack() {
        elements = new ArrayList<>(DEFAULT_CAPACITY);
    }

    public void push(T item) {
        elements.add(item);
        size++;
    }

    public T pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(--size);
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public T peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.get(size - 1);
    }
}

// 다양한 타입으로 스택 생성 가능
GenericStack<String> stringStack = new GenericStack<>();
GenericStack<Integer> intStack = new GenericStack<>();
GenericStack<Student> studentStack = new GenericStack<>();

 

이러한 제네릭의 활용을 통해

  1. 컴파일 시점에서의 타입 체크로 런타임 에러 방지
  2. 타입 캐스팅 제거로 코드 간결성 확보
  3. 알고리즘의 재사용성 향상
  4. 타입 특화된 메서드 구현 가능
  5. 컬렉션 프레임워크의 타입 안정성 보장

등의 이점을 얻을 수 있으며, 현대 Java 프로그래밍에서 필수적인 요소로 자리잡았다.