Generic

하마·2025년 2월 26일

Java

목록 보기
5/8

제네릭이란?


  • 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
  • 클래스, 메소드 등에 사용되는 <T> (타입 매개변수) 를 의미
    • 타입은 실제 데이터 타입으로 작성

  • 타입을 미리 지정하지 않고, 사용 시점에 유연하게 결정할 수 있는 문법
  • 코드 재사용성타입 안정성 을 보장받을 수 있음
    • 코드 재사용성: 다양한 타입에서 동일한 코드로 재사용 가능
    • 타입 안정성: 잘못된 타입 사용을 컴파일 시점에 방지

제네릭을 사용하지 않았을 때


1️⃣ 재사용 불가

public class Box {
    private Integer item; // ⚠️ Integer 타입으로 고정

    public Box(Integer item) { // ⚠️ Integer 타입으로 고정
        this.item = item;
    }

    public Integer getItem() {
        return this.item;
    }
}
public class Main {
    public static void main(String[] args) {
        // ✅ Integer 타입 박스
        Box box1 = new Box(100);

        // ❌ String 타입을 저장하려면 새로운 클래스를 만들어야 함
        Box box2 = new Box("ABC"); 

    }
}
  • Box 클래스는 속성이 Integer 로 고정되어 있어 재사용을 할 수 없음

  • 다시 사용하려면 다른 클래스를 만들어야 함 (낮은 유연성)

2️⃣ 낮은 타입 안정성

public class ObjectBox {
    private Object item; // ⚠️ 다형성: 모든 타입을 저장할 수 있지만 안전하지 않음

    public ObjectBox(Object item) {
        this.item = item;
    }

    public Object getItem() {
        return this.item;
    }
}
public class Main {
    public static void main(String[] args) {
        // ✅ ObjectBox 사용
        ObjectBox objBox = new ObjectBox("Hello");
        String str = (String) objBox.getItem(); // 형변환 필요
        System.out.println("objBox 내용: " + str); // Hello

        // ⚠️ 실행 중 오류 발생 (잘못된 다운 캐스팅: ClassCastException)
        objBox = new ObjectBox(100); // 정수 저장
        // ❌ 오류: Integer -> String 
        String error = (String) objBox.getItem();
        System.out.println("잘못된 변환: " + error);
    }
}
  • Object 클래스를 활용하여 다형성을 이용하면 다양한 데이터 타입 저장이 가능함

    • 실행 중 Unchecked Exception이 발생할 가능성이 높음
  • 데이터 조작 시 형 변환이 반드시 필요함


제네릭을 사용하면?


제네릭 클래스

  • 클래스 선언부에 <T> 가 선언된 클래스
  • <T> (타입 매개변수) 를 사용해 다양한 데이터 타입을 안전하게 처리할 수 있음
public class GenericBox<T> { // ✅ 제네릭 클래스
    private T item;

    public GenericBox(T item) {
        this.item = item;
    }

    public T getItem() {
        return this.item;
    }
}
public class Main {
    public static void main(String[] args) {
        // 1. ✅ 재사용 가능(컴파일시 타입소거: T -> Object)
        GenericBox<String> strGBox = new GenericBox<>("ABC");
        GenericBox<Integer> intGBox = new GenericBox<>(100);
        GenericBox<Double> doubleGBox = new GenericBox<>(0.1);

        // 2. ✅ 타입 안정성 보장(컴파일시 타입소거: 자동으로 다운캐스팅)
        String strGBoxItem = strGBox.getItem();
        Integer intGBoxItem = intGBox.getItem();
        Double doubleGBoxItem = doubleGBox.getItem();
        System.out.println("strGBoxItem = " + strGBoxItem);
        System.out.println("intGBoxItem = " + intGBoxItem);
        System.out.println("doubleGBoxItem = " + doubleGBoxItem);
    }
}

📌 타입 소거(Erasure)란?

  • 컴파일 시점에 제네릭 타입 정보를 제거하는 과정
    • <T> 부분은 자동으로 Object 로 대체됨
    • 필요한 경우 컴파일러가 자동으로 다운 캐스팅 진행

제네릭 메소드

  • 메소드 내부에서 사용할 타입을 유연하게 지정하는 기능
  • 클래스 제네릭 타입과 별개로 독립적인 <T> 를 가짐
  • 메소드 반환 타입 바로 앞에 제네릭 타입을 선언한다.
public class GenericBox<T> {

    // 속성
    private T item;

    // 생성자
    public GenericBox(T item) {
        this.item = item;
    }

    // 기능
    public T getItem() {
        return this.item;
    }

	// ⚠️ 일반 메서드
    // T item 은 클래스의 <T> 를 따라갑니다.
    public void printItem(T item) {
        System.out.println(item);
    }
    
    // ✅ 제네릭 메서드
    // <S> 는 <T> 와 별개로 독립적이다.
    public <S> void printBoxItem(S item) { 
        System.out.println(item);
    }
}
public class Main {

    public static void main(String[] args) {
        GenericBox<String> strGBox = new GenericBox<>("ABC");
        GenericBox<Integer> intGBox = new GenericBox<>(100);
        
        // ⚠️ 일반메서드: 클래스 타입 매개변수를 따라갑니다.
        // String 데이터 타입 기반으로 타입소거가 발생.
        // String 타입의 다운캐스팅 코드 삽입!
        strGBox.printItem("ABC"); // ✅ String 만 사용가능
        strGBox.printItem(100);   // ❌ 에러 발생 
        
        // ✅ 제네릭 메서드: 독립적인 타입 매개변수를 가집니다.
        // String 타입 정보가 제네릭 메서드에 아무런 영향을 주지 못함.
        // 다운캐스팅 코드 삽입되지 않음.
        strGBox.printBoxItem("ABC"); // ✅ 모든 데이터 타입 활용 가능
        strGBox.printBoxItem(100);   // ✅ 모든 데이터 타입 활용 가능
        strGBox.printBoxItem(0.1);   // ✅ 모든 데이터 타입 활용 가능
    }
}

<T>(타입 파라미터) 기호 네이밍


반복문에서 i 다음으로 j , k 를 사용했던 것처럼
제네릭의 식별자 기호는 통상적으로 아래의 표와 같이 사용한다.
하지만 문법적으로 정해진 건 없음!

타입설명
<T>타입 (Type)
<E>요소, 예를 들어 List
<K>키, 예를 들어 Map<K, V>
<V>리턴 값 또는 매핑된 값 (Variable)
<N>숫자 (Number)
<S, U, V>2, 3, 4번째에 선언된 타입

제네릭 사용 시 주의사항


1️⃣ 제네릭 타입의 객체 생성 불가

제네릭 타입 자체로 타입을 지정해서 객체를 생성할 수 없다.
즉, new 연산자 뒤에 제네릭 타입 파라미터가 올 수 없다.

class Sample<T> {
    public void someMethod() {
        // 타입 파라미터로 객체 생성 불가
        T t = new T();
    }
}

2️⃣ static 멤버에 제네릭 타입이 올 수 없음

아래처럼 static 변수의 데이터 타입으로 제네릭 타입 파라미터가 올 수 없다.

왜냐하면 static 멤버는 클래스가 동일하게 공유하는 변수로서
제네릭 객체가 생성되기도 전에 이미 자료 타입이 정해져 있어야 하기 때문이다.
즉, 논리적 오류인 것이다.

class Student<T> {
    private String name;
    private int age = 0;

    // static 메서드의 반환 타입으로 사용 불가
    public static T addAge(int n) { }
    
    // static 메서드의 매개변수 타입으로 사용 불가
    public static void addAge(T n) { }
}

3️⃣ 제네릭으로 배열 선언 시 주의점

기본적으로 제네릭 클래스 자체를 배열로 만들 수 없다.

class Sample<T> { }

public class Main {
    public static void main(String[] args) {
        Sample<Integer>[] arr1 = new Sample<>[10];
    }
}

하지만 제네릭 타입의 배열 선언은 허용된다.

위의 식과 차이점은 배열에 저장할 Sample 객체의 타입 파라미터를 Integer 로 지정한다는 뜻이다.
즉, new Sample<Integer> 인스턴스는 저장이 가능하며,
new Sample<String>() 인스턴스는 저장이 불가능하다.

class Sample<T> { }

public class Main {
    public static void main(String[] args) {
    	// 에러 발생
	    Sample<Integer>[] arr1 = new Sample<>[10];
        
    	// new Sample<Integer>() 인스턴스만 저장하는 배열을 나타냄
        Sample<Integer>[] arr2 = new Sample[10]; 
        
        arr2[0] = new Sample<>(); 
        arr2[1] = new Sample<>();
        
        // ! Integer가 아닌 타입은 저장 불가능
        arr2[2] = new Sample<String>();
    }
}

참고자료


챕터 3-4 : 제네릭(Generic)
자바 제네릭(Generics) 개념 & 문법 정복하기

0개의 댓글