본문 바로가기
개발 새발/JAVA

[Java] 자바 - 지네릭스

by recordari 2025. 4. 17.

자바 지네릭스(Generics)

자바의 지네릭스(Generics)는 컴파일 시점에 타입을 체크해서 안정성을 높여주는 기능이다.
기존에는 Object로 모든 타입을 처리했기 때문에, 저장할 때와 꺼낼 때 **형변환(casting)**이 필수였다.
지네릭스를 사용하면 컴파일러가 타입을 체크해주므로, 형변환이 필요 없어지고 오류 가능성도 줄어든다.

Box box = new Box();
box.setItem(new Apple());
Apple apple = (Apple) box.getItem(); // 형변환 필요

지네릭스를 적용하면 다음처럼 형변환 없이 사용할 수 있다.

Box<Apple> box = new Box<>();
box.setItem(new Apple());
Apple apple = box.getItem(); // 형변환 필요 없음

즉, 지네릭스는 instanceof + 조건문을 간단하게 작성한 것이다.

 

지네릭 클래스 선언

// 기존 클래스
class Box {
    Object item;

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

위의 클래스는 모든 타입을 저장할 수 있지만 꺼낼 때마다 형변환을 해줘야 하고, 잘못된 타입을 꺼내면 런타임 오류가 발생할 수 있다.

지네릭 클래스로 바꾸면 타입 안정성이 생긴다:

class Box<T> {
    T item;

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

T는 타입 변수로, Type의 첫 글자를 따온 것이며 임의의 참조형 타입이다.

 

지네릭스 용어

class Box<T> {}
  • Box<T> : 지네릭 클래스
  • T : 타입 변수 또는 타입 매개변수
  • Box : 원시 타입 (Raw Type)
  • 타입 변수에 실제 타입을 지정하는 것을 지네릭 타입 호출, 지정된 타입을 매개변수화된 타입(대입된 타입)이라고 한다.

 

지네릭스의 제한

타입 매개변수 T는 인스턴스 변수로 간주되기 때문에 다음과 같은 제약이 있다.

  • static 필드나 메서드에서는 사용할 수 없다.
  • 제네릭 타입의 배열을 직접 생성할 수 없다. (예: new T[10] → 컴파일 에러)
class Box<T> {
    static T item;       // 에러
    T[] arr = new T[10]; // 에러
}

Box<Apple> appleBox = new Box<Apple>(); // Apple 객체만 가능
Box<Grape> appleBox = new Box<Grape>(); // Grape 객체만 가능

 

지네릭 클래스는 상속과 관계없이 정확히 일치하는 타입만 허용한다.

 

지네릭 클래스의 객체 생성과 사용

Box<T> 정의

class Box<T> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item)       { list.add(item); }
    T get(int i)           { return list.get(i); }
    ArrayList<T> getList() { return list; }
    int size()             { return list.size(); }
    public String toString() { return list.toString(); }
}

 

Box<T>의 객체를 생성할 때는 다음과 같이 해야 한다.
참조변수와 생성자에 대입된 타입이 일치해야 한다. 일치하지 않으면 에러가 발생한다.

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<Grape>();   // 에러. 대입된 타입이 다름
Box<Fruit> appleBox = new Box<Apple>();   // 에러. 대입된 타입이 다름

 

하지만, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 괜찮다.

Box<Apple> appleBox = new FruitBox<Apple>(); // 다형성

 

참조변수의 타입으로부터 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있어서 생성자를 반복해서 타입을 지정해주지 않아도 된다.

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // Apple 생략 가능. 위의 코드와 동일

 

 

객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

그러나 타입 T가 Fruit인 경우 Fruit의 자손들은 이 메서드의 매개변수가 될 수 있다. -> void add(Fruit item) 이 되기 때문

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple());  // OK. 
fruitBox.add(new Grape());  // OK. void add(Fruit item)

 

지네릭 타입 제한 (extends, &)

특정 클래스의 하위 클래스만 허용하고 싶을 때는 extends를 사용한다.

class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정 가능
    ArrayList<T> list = new ArrayList<>();
    void add(T item) { list.add(item); }
}

 

만약, Fruit의 자손이면서 Eatable 인터페이스도 구현해야 한다면 "&"로 연결:

class FruitBox<T extends Fruit & Eatable> {}

→ 클래스는 하나만 상속 가능, 인터페이스는 여러 개 연결 가능

 

 

와일드카드 (?)

와일드카드는 어떤 타입이든 될 수 있다는 뜻을 가진다.

FruitBox<? extends Fruit> box1 = new FruitBox<Apple>(); // OK
FruitBox<? extends Fruit> box2 = new FruitBox<Grape>(); // OK
  • <? extends T> : T 또는 T의 자손만 허용 (상한 제한)
  • <? super T> : T 또는 T의 조상만 허용 (하한 제한)
  • <?> : 모든 타입 허용 (? extends Object와 동일)

 

 

지네릭 메서드

메서드 단위로 타입을 일반화할 수 있다.
선언 위치는 반환 타입 앞

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

 

지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다.

class FruitBox<T> {
	∙∙∙
	static <T> void sort (List<T> list, Comparator<? super T> c) {
  	∙∙∙ // T는 지역변수  
    }
}