자바/자바

(자바)자바 제너릭 (Generics) 정리

불광동 물주먹 2025. 8. 21. 01:09

자바 제너릭 (Generics) 정리

1. 자바와 컴파일의 특징

자바는 흔히 컴파일 언어라고 하지만, 실제로는 하이브리드 언어다.

  • .java → javac → .class(바이트코드)로 컴파일
  • JVM이 이 .class를 읽어들여 인터프리터처럼 실행하다가, JIT 컴파일러로 최적화된 기계어를 만들어 실행

즉, 컴파일 + 인터프리터 두 방식을 혼합한다.

왜 굳이 컴파일을 할까?

  1. 문제 조기 발견
    실행하기 전에 타입이나 문법 문제를 확인할 수 있다.
    예: Integer b = a; (여기서 a가 Object라면) → 컴파일 에러 발생.
  2. 최적화 가능
    전체 코드를 보고 중복 제거, 인라이닝 같은 최적화를 할 수 있다.

정적 타입 언어답게, 자바는 상위타입 → 하위타입 암시적 대입을 금지한다.

 
Object obj = Integer.valueOf(1); // 업캐스팅 OK
Integer num = obj;               // 다운캐스팅은 불가 (컴파일 에러)

 

명시적 캐스팅 없이는 불가하다. 이런 엄격함이 자유도를 줄이는 대신, 버그를 조기에 방지해준다.


2. 제너릭이 없던 시절

예전엔 모든 컬렉션이 raw type 이었다.

 
List list = new ArrayList();
list.add(10);
list.add("oops");  // 컴파일 에러 없음

Integer n = (Integer) list.get(1); // 실행 중 ClassCastException

 

즉,

  • 런타임에 오류가 터진다 (발견 시점이 늦음)
  • 읽을 때마다 캐스팅 필요 (귀찮음 + 가독성↓)

이 문제 때문에 자바 5부터 제너릭(Generics) 이 도입됐다. 이제는 컴파일 단계에서 잘못된 타입 삽입을 막을 수 있다.

 
List<Integer> list = new ArrayList<>();
list.add(10);
// list.add("oops"); // 컴파일 에러

3. 제너릭 클래스 기초

 
class Box<T> {
    private T item;

    public Box(T item) { this.item = item; }
    public T getItem() { return item; }
}
  • T : 타입 매개변수(Type Parameter)
  • Box<Toy> : 타입 인자(Type Argument) 를 넘겨 실제 타입을 지정한 것
  • <T> : 타입 매개변수 섹션

타입을 Object로 두는 것과 제너릭은 다르다.

  • Object로 하면 캐스팅 필요 + 오류가 런타임에 발견됨
  • 제너릭은 컴파일 타임에 타입 체크로 안전성을 확보

4. 제너릭과 상한 경계 (Upper Bound)

때로는 “이 타입은 무조건 ○○을 구현해야 한다”는 제약을 걸고 싶을 때가 있다. 이때 상한 경계(bound) 를 쓴다.

 
interface Boxable {
    default boolean isBreakable() { return false; }
}

class Toy implements Boxable { }

class Box<T extends Boxable> {   // T는 Boxable 하위 타입만 가능
    private T item;
    public Box(T item) { this.item = item; }
    public T getItem() { return item; }
}
  • Box<T> → 아무 타입이나 가능
  • Box<T extends Boxable> → Boxable을 구현한 타입만 가능

즉, 컴파일 시점에 계약을 명시하는 셈이다.
여기에 여러 개의 제약도 걸 수 있다. (단, 클래스는 1개만, 인터페이스는 여러 개 가능)


5. 제너릭 메서드

클래스 자체가 제너릭일 수도 있지만, 메서드만 제너릭일 수도 있다.

 
class Box<T extends Boxable> {
    private T item;
    private String dest;

    public Box(T item, String dest) {
        this.item = item;
        this.dest = dest;
    }

    // 제너릭 메서드 (U는 Boxable 하위 타입만 가능)
    public <U extends Boxable> boolean hasSameDestination(Box<U> other) {
        return this.dest.equals(other.dest);
    }
}
  • 클래스의 T와 메서드의 U는 별개 타입 변수다.
  • 즉, Box<Toy>와 Box<Glass>도 주소 비교가 가능하다.

6. 제너릭 생성자와 static 메서드

  • 생성자에도 제너릭 선언이 가능하다.
 
public <U extends Number> Box(T item, String dest, U price) {
    this.item = item;
    this.dest = dest;
    System.out.println("가격: " + price.intValue());
}

 

→ Integer, Double 등 어떤 Number든 받을 수 있다.

  • static 메서드는 클래스의 T를 쓸 수 없다. 필요하다면 메서드 자체에서 타입을 선언해야 한다.
public static <U extends Boxable> List<U> collect(List<Box<U>> boxes) {
    List<U> result = new ArrayList<>();
    for (Box<U> b : boxes) result.add(b.getItem());
    return result;
}

7. 원시 타입(raw type) 사용 자제

 
Box rawBox = new Box(new Toy()); // 가능은 하지만 경고 발생
  • 타입 안전성이 사라지고, 매번 캐스팅해야 한다.
  • 레거시 코드 호환 외에는 피하는 게 좋다.