hanker

JAVA - JAVA에서 제네릭이란 무엇일까? 본문

JAVA

JAVA - JAVA에서 제네릭이란 무엇일까?

hanker 2024. 12. 1. 00:00
반응형

제네릭(Generic)은 Java에서 컴파일 시점에 타입을 지정할 수 있도록 해주는 기능이다.

제네릭을 사용하면 코드의 재사용성을 높이고, 타입 안정성을 강화할 수 있으며, 명시적 형변환(casting)을 줄여 코드 가독성을 개선할 수 있습니다.

 

가장 흔한 예로는 List, Map 과 같은 컬렉션 프레임워크가 있다.

https://hanke-r.tistory.com/entry/JAVA-JAVA%EC%97%90%EC%84%9C-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

 

JAVA - JAVA에서 컬렉션 프레임워크란 무엇일까?

이번 글에서는 Java에서 컬렉션 프레임워크가 무엇인지에 대해서 알아보자. JAVA의 컬렉션 프레임워크(Collection Framework)란? 컬렉션 프레임워크는 자바에서 데이터를 효율적으로 저장하고 관리하기

hanke-r.tistory.com

 

 

 

이렇게 정의만 봐서는 와닿지 않는다. 

더 자세하게 알아보자.


제네릭의 주요 특징

 

 

1. 타입 안정성:

컴파일 시점에 타입을 체크하기 때문에 잘못된 타입이 사용될 가능성을 줄인다.

이로 인해 런타임 에러를 방지하고, 코드 안정성을 높일 수 있습니다.

List list = new ArrayList(); // 제네릭 사용 안 함
list.add("Hanker");
Integer num = (Integer) list.get(0); // 런타임 시 ClassCastException 발생 가능

// ----------------------------------------------------------------------------

// 제네릭 사용
List<String> list = new ArrayList<>();
list.add("Hanker");
// 컴파일러가 String 타입만 추가할 수 있도록 보장

위 코드에서 list에 문자열을 추가하고 이를 정수로 캐스팅하려고 하면 오류가 발생한다. 

반면에 아래 코드처럼 제네릭을 사용하면  이러한 위험을 컴파일 시점에 차단할 수 있다.

 

2. 코드 재사용성:

같은 클래스나 메서드에서 다양한 타입을 처리할 수 있으므로, 중복된 코드를 줄이고 재사용성을 높이는 데 유리하다.

public class Container<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

위 Container 클래스는 T 타입의 데이터를 처리할 수 있으므로, 하나의 클래스만으로 다양한 타입을 지원할 수 있다.

이렇게 하면 동일한 로직을 여러 타입에 대해 작성하지 않아도 되서, 개발 및 유지보수의 효율성을 높일 수 있다.

Container<String> stringContainer = new Container<>();
Container<Integer> integerContainer = new Container<>();

 

 

3. 형변환 제거:

제네릭을 사용하면 명시적 형변환을 줄일 수 있다.

예를 들어, 컬렉션에 저장된 요소를 꺼낼 때 타입 변환이 필요하지 않다.

List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 형변환 필요

// ---------------------------------------------------------
// 제네릭 사용
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 형변환 불필요

이렇게 형변환을 줄여주는 것은 단순히 코드를 깔끔하게 만드는 것뿐만 아니라, 런타임 오류를 방지 하는 데 중요한 역할을 한다.

 

 

 


 

제네릭의 제한

 

 

1. 기본 타입 사용 불가:

제네릭에서는 기본 타입(int, double, char 등)을 사용할 수 없다. 대신 박싱된 클래스(Integer, Double 등)를 사용해야한다.

List<int> list = new ArrayList<>(); // 컴파일 에러
List<Integer> list = new ArrayList<>(); // 정상

 

2. 정적(static) 컨텍스트 사용 제한:

제네릭 타입 매개변수는 클래스의 정적 필드나 정적 메서드에서 사용할 수 없다.

 

3. 제네릭 타입 소거(Type Erasure):

Java의 제네릭은 런타임 시에 타입 정보가 소거되어 컴파일된 바이트코드에는 타입 정보가 남아 있지 않다. 이를 "타입 소거"라고 부르며, 이는 제네릭의 하위 호환성을 보장하기 위한 설계이다. (말이 너무 어렵다 밑에서 자세하게 또 다뤄보자)

 

 

 

 


제네릭과 타입 소거 (Type Erasure)

 

Java에서 제네릭은 타입 소거(Type Erasure) 방식을 통해 구현된다.

이는 컴파일 시점에 제네릭 타입이 타입 매개변수로 처리되고, 런타임 시에는 소거되어 실제로는 타입 정보가 남아 있지 않게 된다.

이 방식으로 Java는 제네릭이 없는 기존 코드와도 호환성을 유지할 수 있다.

하지만, 이로 인해 제네릭 타입으로 객체를 생성하는 것, 정적 필드에서 제네릭 타입을 사용하는 것 등이 제한된다.

List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
// 런타임에는 둘 다 단순히 List 타입으로 인식됩니다.

타입 소거로 인해 제네릭 타입을 런타임에 구별할 수 없기 때문에, 제네릭 타입의 클래스에서 타입 매개변수를 활용한 인스턴스 생성은 불가능하며, 런타임 시 제네릭 타입의 정확한 타입 매개변수를 알 수 없다.

 

 


와일드 카드 사용

 

이 주제도 특징에 넣으려고 했으나, 글이 길어질 것 같아서 따로 적어본다.

 

Java 제네릭은 타입의 범위를 제한하거나 유연하게 사용하기 위해 와일드카드(?)를 제공한다.

와일드카드는 제네릭을 사용할 때 타입이 명확하지 않거나 여러 타입을 허용해야 하는 경우에 유용하다.

 

 

- 와일드 카드의 종류

? extends T :

T를 상속하거나 T 자체인 타입을 허용한다. 이를 통해 제네릭 타입의 하위 클래스만 허용할 수 있다.

예를 들어, 숫자를 처리하는 메서드에 List<? extends Number>를 전달하면, Integer, Double 등의 리스트를 모두 처리할 수 있다.

    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);

        printNumbers(intList);    // 1, 2, 3 출력
    }

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

 

 

? super T:

T의 상위 타입만 허용한다. 이를 통해 제네릭 타입의 상위 클래스와 그 자체를 포함하는 범위를 허용할 수 있다.

예를 들어, 특정 타입의 하위 클래스도 처리할 수 있도록 하기 위해 List<? super Integer>를 사용할 수 있다.

public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();

        addIntegers(numberList);
        addIntegers(objectList);
    }

    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);

        System.out.println(list);
    }

 

 

?:

모든 타입을 허용한다. 이 경우 특정 타입의 제한 없이 제네릭을 사용할 수 있다.

public static void main(String[] args) {
    List<String> stringList = Arrays.asList("Hello", "Hanker");
    List<Integer> intList = Arrays.asList(1, 2, 3);

    printList(stringList); // "Hello", "Hanker" 출력
    printList(intList);    // 1, 2, 3 출력
}

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

 

 

 


정리

 

java의 제네릭은 코드의 타입 안전성을 높이고 재사용성을 향상시키며, 런타임 에러를 줄이는 데 큰 역할을 한다.

특히, 컬렉션과 같은 다양한 타입을 다루는 경우에 제네릭은 필수적이며, 타입 안정성을 통해 개발자의 실수를 줄일 수 있는 강력한 도구이다.

반응형