제네릭

이기영·2024년 7월 29일

Java 기초학습

목록 보기
14/14
post-thumbnail

목표

Java의 제네릭에 대해 학습

목차

1. 제네릭 사용법
2. 제네릭 주요 개념
3. 제네릭 메소드 만들기
4. Erasure


1. 제네릭 사용법

Java의 제네릭(Generics)은 컴파일 시 타입을 검사하고, 코드 재사용성을 높이는데 매우 유용한 기능입니다. 제네릭을 사용하면 클래스나 메서드를 작성할 때 타입을 파라미터로 사용할 수 있어 타입 안전성을 보장할 수 있습니다.


1-1) 제네릭 클래스

  제네릭 클래스는 클래스 선언에서 타입 파라미터를 사용하는 클래스입니다. 타입 파라미터는 보통 대문자 T(Type), E(Element), K(Key), V(Value) 등을 사용합니다.

public class Box<T> {
    private T item;

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

    public T getItem() {
        return item;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setItem("Hello, Generics");
        System.out.println(stringBox.getItem());

        Box<Integer> integerBox = new Box<>();
        integerBox.setItem(123);
        System.out.println(integerBox.getItem());
    }
}

  위의 예시에서 Box 클래스는 타입 파라미터 T를 사용하여 저장할 항목의 타입을 지정합니다. Box<String> 인스턴스는 문자열을, Box<Integer>인스턴스는 정수를 저장할 수 있습니다.


1-2) 제네릭 메서드

  제네릭 메서드는 메서드 선언에서 타입 파라미터를 사용하는 메서드입니다. 메서드 선언에서 타입 파라미터를 사용하려면 리턴 타입 앞에 타입 파라미터를 지정합니다.

public class GenericMethodExample {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"Hello", "Generics"};

        printArray(intArray);
        printArray(strArray);
    }
}

  위 예시에서 printArray 메서드는 타입 파라미터 T를 사용하여 배열의 요소를 출력합니다. Integer 배열과 String 배열을 모두 처리할 수 있습니다.


1-3) 제네릭 인터페이스

  제네릭 인터페이스는 인터페이스 선언에서 타입 파라미터를 사용하는 인터페이스 입니다.

public interface Pair<K, V> {
    K getKey();
    V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {
    private K key;
    private V value;

    public OrderedPair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public K getKey() {
        return key;
    }

    @Override
    public V getValue() {
        return value;
    }

    public static void main(String[] args) {
        Pair<String, Integer> pair = new OrderedPair<>("One", 1);
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
    }
}

  위 예제에서 Pair 인터페이스는 두 개의 타입 파라미터 KV를 사용합니다. Orderedpair 클래스는 Pair 인터페이스를 구현하며, 타입 파라미터 KV를 사용합니다.


요약

  • 제네릭 클래스
    클래스 선언에서 타입 파라미터를 사용하여 타입 안전성을 보장하고 코드 재사용성을 높입니다.

  • 제네릭 메서드
    메서드 선언에서 타입 파라미터를 사용하여 다양한 타입의 데이터를 처리할 수 있습니다.

  • 제네릭 인터페이스
    인터페이스 선언에서 타입 파라미터를 사용하여 타입 안전성을 보장하고 코드 재사용성을 높입니다.



2. 제네릭 주요 개념

여기서는 Bounded typesWildcards에 대해 설명하겠습니다.


2-1) 바운디드 타입(Bounded Types)

  바운디드 타입은 타입 파라미터가 특정 클래스나 인터페이스의 서브타입 또는 슈퍼타입이어야 한다는 제약을 추가하는 기능입니다. 이를 통해 타입 파라미터에 대해 더 구체적인 제약을 걸 수 있습니다.


2-1-1) 상한 바운드(Upper Bound)

  상한 바운드는 타입 파라미터가 특정 클래스나 인터페이스의 서브타입이어야 한다는 제약입니다. extends 키워드를 사용합니다.

public class BoundedTypeExample {
    public static <T extends Number> void printDoubleValue(T number) {
        System.out.println(number.doubleValue() * 2);
    }

    public static void main(String[] args) {
        printDoubleValue(10);       // Integer 타입
        printDoubleValue(10.5);     // Double 타입
        printDoubleValue(5.75f);    // Float 타입
    }
}

  위 예시에서 printDoubleValue 메서드는 타입 파라미터 TNumber의 서브 타입이어야 한다고 제한합니다. 따라서 Integer, Double, Float 타입만 사용할 수 있습니다.


2-1-2) 하한 바운드(Lower Bound)

  하한 바운드는 타입 파라미터가 특정 클래스나 인터페이스의 슈퍼타입이어야 한다는 제약입니다. super 키워드를 사용합니다. 이는 주로 와일드카드와 함께 사용됩니다.

import java.util.List;
import java.util.ArrayList;

public class LowerBoundExample {
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        System.out.println(numList);
    }
}

  위 예시에서 addNumbers 메서드는 타입 파라미터가 Integer의 슈퍼타입이어야 한다는 제약을 겁니다. 따라서 NumberObject 타입의 리스트에 Integer 값을 추가할 수 있습니다.



2-2) 와일드카드 (Wildcards)

  와일드카드는 제네릭 타입을 사용할 때 불특정 타입을 나타내기 위해 사용됩니다. 세 가지 종류의 와일드카드가 있습니다.


2-2-1) 불특정 와일드카드(Unbounded Wildcard)

  불특정 와일드카드는?를 사용하여 어떤 타입이든 받아들일 수 있습니다.

import java.util.List;

public class UnboundedWildcardExample {
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<String> strList = List.of("Hello", "Generics");
        printList(intList);
        printList(strList);
    }
}

  위 예시에서 printList 메서드는 어떤 타입의 리스트든 받아들일 수 있습니다.


2-2-2) 상한 와일드카드 (Upper Bounded Wildcard)

  상한 와일드카드는 <? extends T> 형태로 사용하며, 특정 타입의 서브타입만 받아들일 수 있습니다.

import java.util.List;
import java.util.ArrayList;

public class UpperBoundedWildcardExample {
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);
        System.out.println("Sum of intList: " + sumOfList(intList));
        System.out.println("Sum of doubleList: " + sumOfList(doubleList));
    }
}

  위 예시에서 sumOfList 메서드는 Number의 서브타입을 받아들입니다.


2-2-3) 하한 와일드 카드(Lower Bounded Wildcard)

  하한 와일드카드는 <? super T> 형태로 사용하며, 특정 타입의 슈퍼타입만 받아들일 수 있습니다.

import java.util.List;
import java.util.ArrayList;

public class LowerBoundedWildcardExample {
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    public static void main(String[] args) {
        List<Number> numList = new ArrayList<>();
        addNumbers(numList);
        System.out.println(numList);
    }
}

  위 예시에서 addNumber 메서드는 Integer의 슈퍼타입을 받아들입니다.


2-3) 제네릭 타입의 제한 사항

1. 프리미티브 타입 사용 불가
제네릭 타입 파라미터에는 프리미티브 타입을 사용할 수 없습니다. 대신 박싱된 타입을 사용해야합니다.

// 불가능
// List<int> list = new ArrayList<>();

// 가능
List<Integer> list = new ArrayList<>();

2. 정적 컨텍스트에서 사용 제한
제네릭 타입 파라미터는 정적 변수나 정적 메서드에서 사용할 수 없습니다.

public class GenericClass<T> {
    // 불가능
    // private static T instance;

    // 가능
    private T instance;

    public static <T> void staticMethod(T param) {
        // 제네릭 타입 파라미터는 메서드 수준에서만 사용 가능
    }
}

3. 런타임 시 타입 소거(Type Erasure)
자바의 제네릭은 컴파일 타임에 타입 체크를 하고, 런타임에는 타입 정보가 소거됩니다. 따라서 런타임에 타입 파라미터의 실제 타입을 알 수 없습니다.

public class GenericClass<T> {
    public void printClassName() {
        // 런타임 시에는 타입 정보를 알 수 없음
        System.out.println(T.class.getName());  // 컴파일 오류
    }
}

Erasure는 아래에서 다시한번 자세히 설명하겠습니다.


요약

  • 바운디드 타입
    타입 파라미터에 상한(extends) 또는 하한(super) 제한을 걸어 특정 타입 게층 내에서만 사용할 수 있도록 합니다.

  • 와일드 카드
    ?를 사용하여 제네릭 타입을 불특정 타입으로 나타내며, extendssuper를 사용하여 상한 및 하한을 지정할 수 있습니다.

  • 제네릭 메서드
    메서드 선언에서 타입 파라미터를 사용하여 다양한 타입의 데이터를 처리할 수 있습니다.

  • 제네릭 타입의 제한 사항
    프리미티브 타입 사용 불가, 정적 컨텍스트에서 사용 제한, 런타임 시 타입 소거 등의 제한이 있습니다.



3. 제네릭 메소드 만들기

제네릭 메서드는 메서드 선언에서 타입 파라미터를 사용하는 메서드입니다. 이는 메서드가 호출될 때 타입을 명시할 수 있게 하여 다양한 타입의 데이터를 처리할 수 있게 합니다.


3-1) 제네릭 메서드 선언

  제네릭 메서드를 선언할 때는 메서드의 리턴 타입 파라미터를 선언합니다. 이는 메서드의 인수와 리턴 타입에서 사용할 수있습니다.

기본형태

public <T> void methodName(T param) {
    // 메서드 구현
}

예제 1: 제네릭 메서드

  배열의 요소를 출력하는 제네릭 메서드의 예제 입니다.

public class GenericMethodExample {
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"Hello", "Generics"};

        printArray(intArray);  // 제네릭 메서드 호출
        printArray(strArray);  // 제네릭 메서드 호출
    }
}

예제 2: 제네릭 메서드와 타입 변환

  제네릭 메서드는 반환 타입에서도 제네릭을 사용할 수 있습니다. 다음은 두 개의 인수를 비교하여 큰 값을 반환하는 제네릭 메서드의 예제입니다.

public class GenericMethodExample {
    public static <T extends Comparable<T>> T findMax(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }

    public static void main(String[] args) {
        System.out.println(findMax(3, 5));           // 정수 비교
        System.out.println(findMax("apple", "pear")); // 문자열 비교
    }
}

3-2) 제네릭 메서드의 타입 파라미터 제한

  앞서 설명한 바운디드 타입을 통해 파라미터의 타입을 제한할 수있습니다. 특정 클래스나 인터페이스의 서브타입으로 제한될 수 있습니다.

public class BoundedGenericMethodExample {
    public static <T extends Number> void printDoubleValue(T number) {
        System.out.println(number.doubleValue() * 2);
    }

    public static void main(String[] args) {
        printDoubleValue(10);        // Integer 타입
        printDoubleValue(10.5);      // Double 타입
        printDoubleValue(5.75f);     // Float 타입
    }
}

  위 예시에서 printDoubleValue 메서드는 타입 파라미터 TNumber의 서브 타입이도록 제한하고 있습니다.


3-3) 여러 개의 타입 파라미터

  제네릭 메서드는 여러 개의 타입 파라미터를 가질 수 있습니다. 이는 타입 파라미터를 쉼표로 구분하여 선언합니다.

public class MultiGenericMethodExample {
    public static <K, V> void printKeyValue(K key, V value) {
        System.out.println("Key: " + key + ", Value: " + value);
    }

    public static void main(String[] args) {
        printKeyValue("One", 1);
        printKeyValue("Apple", "Fruit");
    }
}

  위 예시에서 printkeyValue 메서드는 두 개의 타입 파라미터 KV를 가집니다.


3-4) 제네릭 메서드의 호출

  제네릭 메서드는 메서드 호출 시 타입을 명시적으로 지정하지 않아도 컴파일러가 타입을 추론해줍니다. 하지만 명시적으로 타입을 지정해 주는 방법도 있습니다.

public class ExplicitTypeGenericMethodExample {
    public static <T> void printElement(T element) {
        System.out.println("Element: " + element);
    }

    public static void main(String[] args) {
        // 컴파일러가 타입을 추론
        printElement(123);
        printElement("Hello");

        // 명시적으로 타입 지정
        ExplicitTypeGenericMethodExample.<String>printElement("Generics");
    }
}

요약

  • 제네릭 메서드 선언
    리턴 타입 앞에 타입 파라미터를 선언합니다.

  • 타입 파라미터 제한
    extends 키워드를 사용하여 타입 파라미터를 특정 클래스나 인터페이스의 서브타입으로 제한할 수 있습니다.

  • 여러 개의 타입 파라미터
    쉼표로 구분하여 여러 개의 타입 파라미터를 선언할 수 있습니다.

  • 제네릭 메서드 호출
    컴파일러가 타입을 추론하지만, 필요에 따라 명시적으로 타입을 지정할 수도 있습니다.



4. Erasure

제네릭 타입 소거(Erasure)는 Java의 제네릭이 컴파일 타임에만 검사를 수행하고, 런타임에는 제네릭 타입 정보를 제거하는 과정입니다.

이 개념은 자바의 제네릭 구현 방식의 핵심으로, 호환성과 성능 측면에서 중요한 역할을 합니다.


제네릭 타입 소거(Erasure)란?

  제네릭 타입 소거는 자바 컴파일러가 제네릭 타입을 처리하는 방법입니다. 제네릭 코드가 컴파일되면 모든 제네릭 타입 정보는 제거(소거)되고, 제네릭 타입을 사용한 코드는 타입 안전성을 보장하는 기본형 코드로 변환됩니다.

  즉, 제네릭 타입 파라미터는 제거되고, 필요에 따라 해당 타입은 Object나 바운드로 대체됩니다.



왜 타입 소거가 필요한가?

  타입 소거는 자바의 제네릭이 Java5 에서 도입되었을 때, 이전 버전과의 하위 호환성을 유지하기 위해 도입되었습니다.

  기존의 Java 코드와 새로운 제네릭 코드가 함께 동작할 수 있도록 하기 위해, 제네릭 타입 정보는 컴파일 타임에만 사용되고 런타임에는 제거됩니다. 이를 통해 제네릭 도입 이전에 작성된 코드와 제네릭 코드가 동일한 바이트코드를 공유할 수 있었습니다.



타입 소거의 작동방식

  타입 소거는 다음과 같은 방식으로 작동합니다.

1. 제네릭 타입 파라미터 제거
모든 제네릭 타입 파라미터가 제거되고, 필요에 따라 Object 또는 Upper Bound로 대체됩니다.

2. 타입 검사 및 캐스팅 추가
타입 안정성을 보장하기 위해 컴파일러는 필요한 곳에 강제 캐시팅을 추가합니다.

3. 바운드 적용
타입 파라미터가 바운드(extends 키워드)로 제한된 경우, 제네릭 타입은 해당 바운드 타입으로 대체됩니다.

예제: 타입 소거 전후

제네릭 코드

public class Box<T> {
    private T item;

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

    public T getItem() {
        return item;
    }
}

타입 소거 후 코드

public class Box {
    private Object item;

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

    public Object getItem() {
        return item;
    }
}

  보시는 바와 같이 제네릭 타입 파라미터 TObject로 대체되게 됩니다.


예제: 바운드 타입 소거

제네릭 코드

public class Box<T extends Number> {
    private T item;

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

    public T getItem() {
        return item;
    }
}

타입 소거 후 코드

public class Box {
    private Number item;

    public void setItem(Number item) {
        this.item = item;
    }

    public Number getItem() {
        return item;
    }
}

  여기서는 T extends NumberNumber로 대체됩니다.


타입 소거의 영향

  타입 소거는 제네릭 타입을 런타임에 사용할 수 없게 만듭니다. 이는 몇 가지 제약을 발생시킵니다.

1. 런타임 타입 검사 불가
제네릭 타입 파라미터의 런타임 타입 정보를 알 수 없습니다.

public <T> void checkType(T item) {
    if (item instanceof T) {  // 컴파일 오류
        // ...
    }
}

2. 제네릭 배열 생성 불가
제네릭 타입 파라미터를 사용해 배열을 생성할 수 없습니다.

public <T> void createArray() {
    T[] array = new T[10];  // 컴파일 오류
}

3. 강제 캐스팅 필요
타입 소거로 인해 제네릭 타입 정보가 제거되므로, 종종 강제 캐스팅이 필요하게 됩니다.

Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
String item = (String) rawBox.getItem();  // 강제 캐스팅 필요

와일드카드와 타입 소거

  와일드카드(?), 상한 바운드(? extends T), 하한 바운드(? super T)도 타입 소거의 영향을 받습니다. 컴파일 타임에 타입 검사가 이루어지고, 런타임에는 와일드카드 정보가 소거됩니다.


요약

  • 타입소거는 제네릭 타입 정보를 컴파일 타임에만 사용하고, 런타임에는 제거하는 과정입니다.

  • 하위 호환성을 유지하기 위해 자바에서 도입된 방식입니다.

  • 타입 파라미터Object 또는 바운드 타입으로 대체됩니다.

  • 제약사항 : 런타임 타입 검사 불가, 제네릭 배열 생성 불가, 강제 캐스팅 필요



profile
안녕나를소개하지이름은HaBu직업은Programer취미는tai chi meditation

0개의 댓글