JAVA 기초

[JAVA] 기초 - 제네릭(Generic)

beginner-in-coding 2024. 12. 1. 21:02

01. 제네릭(Generic)

  • 컴파일 시점에 데이터 타입을 미리 지정할 수 있는 기능
  • 객체의 타입을 명확히 지정하여, 잘못된 타입의 데이터를 다루는 실수를 방지할 수 있음.
  • 제네릭 문법: 보통 **꺾쇠괄호(<>)**를 사용해 타입 매개변수를 정의
class 클래스명<T> { /* 제네릭 클래스 정의 */ }
interface 인터페이스명<T> { /* 제네릭 인터페이스 정의 */ }

*T는 Type의 약자로, 보통 관습적으로 사용되며 다른 이름도 가능


02. 종류

   : 제네릭 클래스, 제네릭 메서드, 그리고 와일드카드(Generic Wildcards)로 나뉨.


      (1) 제네릭 클래스: 클래스가 다룰 데이터 타입을 클래스 정의 시점이 아닌 객체 생성 시점에 결정할 수 있음.

 

*정의*

class Box<T> {
    private T item;

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

    public T get() {
        return item;
    }
}

*사용*

Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer number = intBox.get();

 

      (2) 제네릭 메서드: 메서드에 사용되는 타입을 메서드 호출 시점에 지정할 수 있음( 클래스 자체가 제네릭이 아닐 때도 제네릭 메서드를 정의할 수 있음)

 

*정의*

class Utils {
    public static <T> T getMiddle(T[] array) {
        return array[array.length / 2];
    }
}

*사용*

String[] names = {"Alice", "Bob", "Charlie"};
String middleName = Utils.getMiddle(names); // "Bob"

Integer[] numbers = {1, 2, 3, 4};
Integer middleNumber = Utils.getMiddle(numbers); // 2

      (3) 와일드카드(Generic Wildcards): ?를 사용하여 알 수 없는 타입을 표현 (제네릭 타입의 범위 제한에 사용됨)

            : 종류 (Unbounded Wildcard(?), Bounded Wildcard(<? extends T> or <? super T>)

  • Unbounded Wildcard (?): 아무 타입이나 허용.
public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}
  • Bounded Wildcard

      *상한 제한 (<? extends T>): TT의 하위 클래스만 허용*

public void printList(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num);
    }
}

      *하한 제한 (<? super T>): TT의 상위 클래스만 허용*

public void addToList(List<? super Integer> list) {
    list.add(42);
}
 

03. 특징

      (1) 타입 안정성(Type Safety)

  예시(ArrayList)
제네릭을 사용하지 않을 경우 ArrayList는 기본적으로 다양한 객체(Object)를 저장할 수 있음.
만약 제네릭을 사용하지 않으면, 객체를 꺼낼 때
**타입 캐스팅(type casting)**을 해야 하고, 런타임에 잘못된 타입이 들어가면 ClassCastException이 발생할 수 있음.
제네릭을 사용할 경우 컴파일 타임에 데이터 타입을 지정하여 잘못된 타입의 객체가 추가되지 않도록 방지함.
따라서 코드에서 명시적으로 캐스팅할 필요가 없어지고,런타임 오류를 줄일 수 있음.

     

* 제네릭을 사용하지 않을 경우*

ArrayList list = new ArrayList();
list.add("String");
list.add(10); // 정수 추가

String s = (String) list.get(0); // 캐스팅 필요
Integer i = (Integer) list.get(1); // 가능
String error = (String) list.get(1); // 런타임 오류 발생 (ClassCastException)

* 제네릭을 사용할 경우*

ArrayList<String> list = new ArrayList<>();
list.add("String");
// list.add(10); // 컴파일 오류 발생!

String s = list.get(0); // 캐스팅 불필요

      (2) 코드 재사용성(Code Reusability)

         : 제네릭을 사용하면 다양한 데이터 타입에 대해 하나의 클래스로 처리할 수 있음.

ArrayList<String> stringList = new ArrayList<>();
ArrayList<Integer> intList = new ArrayList<>();

ArrayList는 제네릭 클래스로 구현되어 있어, 특정 타입으로 제한하지 않고 모든 데이터 타입에 대해 동일한 로직으로 동작할 수 있고, 따로 구현할 필요 없이 원하는 타입에 따라 ArrayList를 재사용할 수 있음.


      (3) 가독성과 유지보수성

         - 코드 가독성: 제네릭을 사용하면 ArrayList가 다루는 데이터 타입이 명확하므로, 코드를 읽는 사람에게 의도를 쉽게 전달할 수 있음.

         - 유지보수성: 잘못된 데이터 타입이 들어가거나 꺼내는 오류를 컴파일 시점에 확인할 수 있으므로, 버그를 조기에 발견하고 수정할 수 있음.


      (4) 타입 변환 비용 제거

         : 제네릭을 사용하면 명시적인 타입 캐스팅이 필요 없으므로, 타입 캐스팅에 소모되는 시간적 비용을 줄일 수 있. 이는 특히 성능이 중요한 상황에서 유리함.


04. 제네릭 제한사항

   1. 기본 타입 사용 불가: 제네릭은 참조 타입만 사용 가능, 기본 타입(int, double...)은 래퍼 클래스(Integer, Double...)로 사용.

List<int> list; // 불가능
List<Integer> list; // 가능

   2. 런타임 시 타입 소거(Type Erasure): 컴파일 시 타입 검사를 수행, 런타임 시에는 타입 정보 제거( => 제네릭 타입은 컴파일 후 Object로 처리)

   3. 정적 멤버에서 제네릭 사용 불가: 정적 멤버는 클래스 수준에서 공유되므로, 제네릭 타입 매개변수 사용 불가

class Example<T> {
    static T staticField; // 불가능
}