[JAVA] 제네릭 (Generic) 정리

동긔·2024년 9월 30일

JAVA

목록 보기
9/11

1. 제네릭 (Generic)이란?

제네릭(Generic)컴파일 시 타입 체크를 강화하여 타입 안전성을 제공하는 자바의 기능입니다. 제네릭을 사용하면 클래스, 인터페이스, 메서드가 다양한 타입을 처리할 수 있게 됩니다. 이를 통해 코드의 재사용성을 높이고, 잘못된 타입 사용을 컴파일 시점에서 방지할 수 있습니다.

정리하면, 타입을 유연하게 처리하며, 잘못된 타입 사용으로 발생할 수 있는 런타임 타입 에러를 컴파일 과정에서 검출하기 위해 사용하는 기능입니다.

1.1 제네릭의 필요성

1) 제네릭 도입 이전 문제

제네릭이 도입되기 전에는, 모든 타입을 담을 수 있는 Object 타입을 이용하여 데이터를 처리했습니다. 컬렉션 예시로, ArrayList 같은 자료 구조에 타입 제한이 없었고, 어떤 객체든 추가할 수 있었습니다. 하지만 데이터를 추출할 때마다 명시적으로 타입 캐스팅이 필요했으며, 캐스팅 과정에서 잘못된 타입으로 변환할 경우 런타임 에러가 발생했습니다.

import java.util.ArrayList;

public class NonGenericExample {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();  // 타입을 지정하지 않음
        list.add("Hello");                 // String 추가
        list.add(123);                     // Integer 추가

        String str = (String) list.get(0); // 형 변환 필요
        System.out.println(str);

        String numStr = (String) list.get(1); // ClassCastException 발생 (런타임 에러)
    }
}

위 코드에서 list.add(123)정수(Integer)가 추가되었지만, String으로 잘못된 캐스팅을 시도하면서 런타임 에러가 발생합니다. 이는 코드 작성 시 문제가 드러나지 않고, 프로그램을 실행했을 때만 알 수 있기 때문에 매우 위험한 방식입니다.

2) 제네릭 도입의 이유

이러한 문제를 해결하고자 JDK 1.5에서 제네릭이 도입되었습니다. 제네릭을 사용하면, 컬렉션이나 메서드구체적인 타입을 지정할 수 있으며, 이로 인해 다음과 같은 이점을 얻을 수 있습니다.

  • 컴파일 시점에서 타입 체크: 런타임 에러가 발생하기 전에 컴파일러타입 일관성을 검사하여 안전성을 보장합니다.
  • 캐스팅 불필요: 명시적 타입 캐스팅을 하지 않아도 되므로, 코드가 더 간결해지고 타입 오류를 줄일 수 있습니다.
  • 코드의 재사용성: 동일한 코드를 여러 타입에 대해 재사용할 수 있으므로, 유연한 프로그래밍이 가능합니다.
import java.util.ArrayList;

public class GenericExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();  // String 타입 지정
        list.add("Hello");
        // list.add(123);  // 컴파일 에러: Integer는 추가할 수 없음

        String str = list.get(0);  // 형 변환 필요 없음
        System.out.println(str);
    }
}

이 코드는 컴파일 시점에 타입 오류를 감지하며, 형변환이 불필요해지고 코드가 안전해집니다.

3) 제네릭의 도입 이유: 컴파일 시점에서 안전성 강화

제네릭의 가장 큰 장점컴파일 시점타입을 미리 확인할 수 있다는 점입니다. 제네릭을 사용하면, 개발자가 잘못된 타입을 사용했을 경우 컴파일러가 이를 즉시 경고하거나 에러를 발생시킵니다. 따라서 런타임 에러로 이어질 수 있는 타입 불일치 문제를 미리 방지할 수 있습니다.

import java.util.ArrayList;

public class GenericExample {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Hello");
        list.add(123);  // 컴파일 에러: Integer는 허용되지 않음
    }
}

위 예제에서 컴파일 시점잘못된 타입(Integer)을 추가하려고 하면 컴파일러가 오류를 발생시켜 프로그램 실행 전에 오류를 수정할 수 있게 해줍니다. 이는 제네릭의 도입 이전과 비교해 안정성을 대폭 강화한 것입니다.

4) 제네릭을 사용해야 하는 이유

  • 안전한 타입 체크: 런타임 에러를 방지하고, 코드에서 발생할 수 있는 타입 오류컴파일 시점에 미리 잡아낼 수 있습니다.
  • 가독성 및 유지보수성: 불필요한 캐스팅이 제거되므로 코드가 더 간결하고 읽기 쉬워집니다.
  • 코드 재사용성: 다양한 타입을 처리할 수 있는 유연한 클래스 및 메서드를 작성할 수 있습니다.

1.3 공변과 불공변

제네릭과 관련된 중요한 개념으로는 공변(covariant)불공변(invariant)이 있습니다.

  • 공변(Covariant): A가 B의 하위 타입일 때, T<A>T<B>의 하위 타입이면 T는 공변입니다.
  • 불공변(Invariant): A가 B의 하위 타입일 때, T<A>T<B>의 하위 타입이 아니면 T는 불공변입니다.

1) 배열은 공변

자바에서 배열은 공변(covariant)입니다. 이는 하위 타입의 배열이 상위 타입의 배열로 대체될 수 있음을 의미합니다.

Integer[] integers = {1, 2, 3};
Object[] objects = integers;  // Integer[]는 Object[]의 하위 타입

위 코드에서 Integer[] 배열은 Object[] 배열로 참조할 수 있습니다. 이는 공변이기 때문에 가능하며, 배열의 타입 간 상속 관계가 그대로 적용되기 때문입니다.

public class ArrayCovarianceExample {
    public static void main(String[] args) {
        Integer[] integers = new Integer[]{1, 2, 3};  // Integer 배열 생성
        printArray(integers);                         // Object 배열을 받는 메서드에 Integer 배열 전달
    }

    // Object[]를 인수로 받는 메서드
    public static void printArray(Object[] arr) {
        for (Object e : arr) {
            System.out.println(e);
        }
    }
}

/*
출력 결과
1
2
3
*/

배열은 공변이기 때문에, 하위 타입의 배열을 상위 타입으로 받을 수 있으며, 이를 활용해 메서드에서도 하위 타입의 배열을 상위 타입으로 처리할 수 있습니다. 예를 들어, 배열의 요소를 출력하는 메서드를 작성할 때 Object[] 배열을 인수로 받는 메서드에 Integer[]를 전달할 수 있습니다.

이것이 가능한 이유는 배열이 공변이기 때문입니다. IntegerObject의 하위 타입이며, Integer[] 역시 Object[]의 하위 타입으로 간주되어 타입 변환이 가능합니다.

2) 제네릭은 불공변

반면에, 제네릭불공변(invariant)입니다. 즉, 하위 타입의 제네릭 타입상위 타입의 제네릭 타입으로 대체될 수 없습니다. List<Integer>List<Object>의 하위 타입이 아닙니다. 제네릭 타입은 서로 독립적이며, 상속 관계가 적용되지 않습니다.

List<Integer> integerList = Arrays.asList(1, 2, 3);
List<Object> objectList = integerList;  // 컴파일 에러: 제네릭은 불공변

위 코드에서는 List<Integer>List<Object>로 대체될 수 없기 때문에 컴파일 에러가 발생합니다. 제네릭은 불공변이므로, 타입 간의 상속 관계가 제네릭 타입 간에는 적용되지 않습니다.

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class GenericInvariantExample {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3);
        printCollection(list);   // 컴파일 에러 발생
    }

    // Object 타입의 컬렉션을 받는 메서드
    public static void printCollection(Collection<Object> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
}

/*
에러 발생
error: incompatible types: List<Integer> cannot be converted to Collection<Object>
*/

제네릭은 불공변이기 때문에, 컬렉션을 다루는 메서드에서 상위 타입을 허용하도록 선언하더라도, 하위 타입의 제네릭을 전달할 수 없습니다. 코드를 보면, List<Integer>Collection<Object>를 받는 메서드에 전달하려고 하면 컴파일 에러가 발생합니다.

컴파일 에러가 발생하는 이유는 List<Integer>Collection<Object>의 하위 타입이 아니기 때문입니다.

3) 왜 제네릭은 불공변인가?

배열과는 다르게, 제네릭은 타입 안전성을 보장하기 위해 불공변성을 채택했습니다. 제네릭이 불공변이어야 잘못된 타입 사용을 막을 수 있습니다. 만약 List<Integer>List<Object>의 하위 타입으로 간주되었다면, Object 타입의 객체(예: String)를 List<Integer>에 추가할 수 있었을 것입니다. 이는 타입 안전성을 해치고, 런타임 에러를 초래할 수 있습니다.

List<Integer> integerList = Arrays.asList(1, 2, 3);
List<Object> objectList = integerList;  // 만약 허용된다면...
objectList.add("String");  // 타입 불일치 문제 발생
Integer num = integerList.get(0);  // Integer로 캐스팅 시 런타임 에러

위 코드에서, objectList.add("String")과 같은 코드가 허용되면 정수 리스트에 문자열을 추가할 수 있습니다. 그러나 리스트의 원소가 Integer 타입이라고 가정했기 때문에 런타임 시 타입 오류가 발생하게 됩니다.

1.4 제네릭과 와일드카드의 등장

제네릭이 도입되기 전에는 타입 안정성이 없어서 런타임 시 에러가 발생할 수 있었습니다. 제네릭이 도입됨으로써 컴파일 시점에 타입 체크가 가능해졌지만, 제네릭이 불공변이기 때문에 특정 타입의 컬렉션을 모두 처리하는 메서드를 정의할 때 제한이 생겼습니다.

이 문제를 해결하기 위해 와일드카드(?)가 등장했습니다.

와일드카드는 제네릭의 타입 불일치 문제를 해결하기 위한 방편으로, 제네릭이 불공변인 상황에서 타입 유연성을 제공하여 상속 관계를 활용한 타입 처리를 가능하게 합니다.

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class WildcardExample {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3);
        printCollection(list);  // 와일드카드를 사용하면 정상 동작
    }

    // 와일드카드 ? 를 사용하여 모든 타입 허용
    public static void printCollection(Collection<?> c) {
        for (Object e : c) {
            System.out.println(e);
        }
    }
}

위 코드에서는 와일드카드(?)를 사용하여 타입 유연성을 높였으며, List<Integer>Collection<?>로 처리할 수 있게 되었습니다. 와일드카드를 통해 타입 안정성을 유지하면서 제네릭의 유연성을 확보할 수 있습니다.

와일드카드의 더 자세한 내용은 다음 [JAVA] 시리즈인 와일드카드 파트에서 설명하겠습니다.

1.5 제네릭 사용법

1) 제네릭을 선언하는 방법

제네릭은 클래스, 인터페이스, 메서드에 적용할 수 있습니다. 제네릭을 사용하면 클래스, 인터페이스, 메서드에서 타입을 유연하게 처리할 수 있고, 구체적인 타입은 사용 시점에 결정됩니다.

  • 클래스에서 제네릭을 선언할 때는 클래스 이름 뒤<T>와 같은 형식으로 타입 매개변수를 선언합니다.
  • 인터페이스도 마찬가지로 인터페이스 이름 뒤<T> 형식으로 타입 매개변수를 선언합니다.
  • 메서드에서는 메서드 선언부에 리턴 타입 앞<T>와 같은 형식으로 타입 매개변수를 선언합니다.

2) 여러 타입 매개변수 사용

제네릭은 동시에 여러 개의 타입 매개변수를 선언할 수 있습니다. 여러 타입을 선언할 때는 콤마(,)로 구분합니다.

class Pair<K, V> {  // K와 V 두 개의 타입 매개변수를 사용
    private K key;
    private V value;

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

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

public class MultipleGenericsExample {
    public static void main(String[] args) {
        Pair<String, Integer> pair = new Pair<>("Age", 25);  // String, Integer로 타입 지정
        System.out.println("Key: " + pair.getKey() + ", Value: " + pair.getValue());
    }
}
  • 이 예제에서 Pair<K, V> 클래스는 두 개의 타입 매개변수를 받아들이며, 이를 통해 두 개의 서로 다른 타입을 처리할 수 있습니다.

3) 와일드카드 (Generic Wildcards)

자바 제네릭에서는 와일드카드(?)를 사용하여 타입을 유연하게 처리할 수 있습니다. 와일드카드는 알 수 없는 타입을 의미하며, 상한 제한이나 하한 제한을 지정할 수 있습니다.

  • 상한 제한(Upper Bound): <? extends T>T의 하위 클래스만 허용합니다.
  • 하한 제한(Lower Bound): <? super T>T의 상위 클래스만 허용합니다.
import java.util.ArrayList;
import java.util.List;

public class WildcardExample {

    // 상한 제한: Number와 그 하위 클래스만 허용
    public static void printNumbers(List<? extends Number> list) {
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);

        printNumbers(intList);  // Integer는 Number의 하위 클래스이므로 허용
    }
}
  • List<? extends Number>Number와 그 하위 클래스만 허용하는 제네릭 리스트입니다.

1.6 제네릭에서 자주 사용하는 타입 인자

자바 제네릭을 사용할 때 자주 사용되는 타입 매개변수에 대한 표준화된 약어들이 있습니다. 이는 자바 개발자들 사이에서 관습적으로 많이 사용되며, 의미를 쉽게 이해할 수 있도록 돕습니다.

타입 인자의미
TType: 일반적으로 임의의 타입을 나타낼 때 사용합니다.
EElement: 컬렉션과 같은 자료구조의 요소를 나타낼 때 사용합니다.
KKey: 맵(Map)의 키를 나타낼 때 사용합니다.
VValue: 맵(Map)의 값을 나타낼 때 사용합니다.
NNumber: 숫자를 나타낼 때 사용합니다.

이러한 약어를 사용하면 코드를 읽을 때 타입 매개변수의 의미를 쉽게 파악할 수 있습니다. 제네릭 클래스나 메서드를 정의할 때, 적절한 이름을 사용하면 코드 가독성을 높이는 데 도움이 됩니다.

2. 제네릭의 기본 개념

2.1 제네릭 클래스

제네릭 클래스(Generic Class)타입 매개변수(Type Parameter)를 사용하여 여러 타입을 처리할 수 있는 클래스를 정의하는 방법입니다. 제네릭 클래스를 사용하면 특정 타입에 종속되지 않고, 클래스 정의 시에는 구체적인 타입을 명시하지 않은 채 구현할 수 있으며, 클래스의 인스턴스를 만들 때 타입을 명시하여 사용할 수 있습니다.

1) 제네릭 클래스의 정의

제네릭 클래스는 클래스 이름 뒤에 꺽쇠(<>)를 사용해 타입 매개변수를 선언하여 정의됩니다. 자바에서는 관습적으로 대문자 알파벳 한 글자로 타입 매개변수를 선언하는 것이 일반적이며, 보통 T(Type), E(Element), K(Key), V(Value) 등이 사용됩니다.

2) 제네릭 클래스의 문법

class ClassName<T> {  // 여기서 T는 타입 매개변수
    private T field;  // T 타입의 필드를 선언

    public void setField(T field) {  // T 타입의 파라미터를 받는 메서드
        this.field = field;
    }

    public T getField() {  // T 타입을 반환하는 메서드
        return field;
    }
}

T타입 매개변수로, 구체적인 타입으로 대체될 예정입니다. 실제로 이 클래스를 사용할 때는 타입을 명시해야 합니다.

3) 제네릭 클래스 사용 시 장점

  • 타입 안전성: 클래스 인스턴스를 만들 때 특정 타입을 명시할 수 있으므로 잘못된 타입이 들어가는 실수를 방지합니다. 이는 컴파일 시점에 타입 체크가 이루어지기 때문에 런타임 에러를 방지할 수 있습니다.
  • 재사용성: 클래스 코드를 여러 타입에 대해 재사용할 수 있습니다. 타입별로 클래스를 다시 작성할 필요가 없습니다.

4) 제네릭 클래스 사용 예시

// T는 사용자가 지정하는 타입을 나타냄
class Box<T> {
    private T item;

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

    public T get() {
        return item;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        // Box 클래스에 String 타입을 지정
        Box<String> stringBox = new Box<>();  // T가 String으로 대체됨
        stringBox.set("Hello");
        System.out.println("String Box: " + stringBox.get());

        // Box 클래스에 Integer 타입을 지정
        Box<Integer> integerBox = new Box<>();  // T가 Integer로 대체됨
        integerBox.set(123);
        System.out.println("Integer Box: " + integerBox.get());
    }
}
  • Box<T>제네릭 클래스입니다. T타입 매개변수로, 사용자가 클래스를 사용할 때 원하는 타입으로 대체됩니다.
  • Box<String>: T가 String으로 대체되어, 문자열만 저장할 수 있는 박스가 됩니다.
  • Box<Integer>: T가 Integer로 대체되어, 정수만 저장할 수 있는 박스가 됩니다.

이 예시를 통해 제네릭 클래스가 다양한 타입을 안전하게 처리하면서 코드의 재사용성을 높일 수 있음을 알 수 있습니다.

2.2 제네릭 인터페이스

제네릭 인터페이스(Generic Interface)는 인터페이스 선언에 타입 매개변수를 지정하여 타입에 유연한 인터페이스를 정의하는 방법입니다. 제네릭 인터페이스를 사용하면 다양한 타입에 대해 동일한 동작을 제공하는 재사용 가능한 인터페이스를 만들 수 있습니다.

1) 제네릭 인터페이스의 정의

제네릭 인터페이스는 인터페이스 이름 뒤에 꺽쇠(<>)를 사용하여 타입 매개변수를 선언합니다. 제네릭 인터페이스를 구현하는 클래스는 구체적인 타입을 명시하거나 제네릭 클래스로 구현할 수 있습니다.

2) 제네릭 인터페이스의 문법

interface DataStore<T> {  // T는 제네릭 타입 매개변수
    void save(T data);    // T 타입 데이터를 저장하는 메서드
    T load();             // T 타입 데이터를 로드하는 메서드
}

3) 제네릭 인터페이스의 장점

  • 타입 안전성: 제네릭을 사용하여 인터페이스를 정의하면, 잘못된 타입을 사용하는 실수를 방지할 수 있습니다.
  • 유연성: 제네릭 인터페이스는 다양한 타입에 대해 유연한 처리를 지원하며, 같은 인터페이스를 다른 타입에 대해 재사용할 수 있습니다.
  • 재사용성: 제네릭 인터페이스는 한 번 정의되면 다양한 타입에 대해 재사용할 수 있습니다.

4) 제네릭 인터페이스 사용 예시

(1) 구체적인 타입을 지정하는 경우

인터페이스를 구현할 때 구체적인 타입을 지정하여 제네릭 인터페이스를 사용할 수 있습니다.

// String 타입을 사용하는 DataStore 구현
class StringDataStore implements DataStore<String> {
    private String data;

    @Override
    public void save(String data) {
        this.data = data;
    }

    @Override
    public String load() {
        return data;
    }
}

public class GenericInterfaceExample {
    public static void main(String[] args) {
        DataStore<String> store = new StringDataStore();  // String 타입 명시
        store.save("Hello World");
        System.out.println(store.load());  // "Hello World" 출력
    }
}

(2) 제네릭 클래스로 구현하는 경우

또는 제네릭 인터페이스를 제네릭 클래스로 구현하여 더 유연한 처리를 할 수 있습니다.

// 제네릭 클래스로 DataStore 구현
class GenericDataStore<T> implements DataStore<T> {
    private T data;

    @Override
    public void save(T data) {
        this.data = data;
    }

    @Override
    public T load() {
        return data;
    }
}

public class GenericInterfaceExample2 {
    public static void main(String[] args) {
        DataStore<Integer> intStore = new GenericDataStore<>();  // Integer 타입 명시
        intStore.save(123);
        System.out.println(intStore.load());  // 123 출력

        DataStore<String> stringStore = new GenericDataStore<>();  // String 타입 명시
        stringStore.save("Hello Generics");
        System.out.println(stringStore.load());  // "Hello Generics" 출력
    }
}

2.3 제네릭 메서드

제네릭 메서드(Generic Method)메서드 자체에서 타입 매개변수를 선언하여 다양한 타입을 처리할 수 있는 메서드를 말합니다. 제네릭 메서드는 클래스가 제네릭일 필요는 없으며, 메서드의 타입 매개변수를 독립적으로 정의할 수 있습니다.

1) 제네릭 메서드의 정의

제네릭 메서드는 메서드 리턴 타입 앞타입 매개변수를 명시합니다. 이렇게 하면 해당 메서드에서 사용할 수 있는 타입을 유연하게 지정할 수 있습니다.

2) 제네릭 메서드의 문법

class ClassName {
    // 제네릭 메서드 선언, <T>는 이 메서드에서 사용할 타입 매개변수
    public <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}
  • <T>는 이 메서드의 타입 매개변수입니다. 이 메서드는 호출될 때 T구체적인 타입으로 대체됩니다.
  • 제네릭 메서드는 특정 클래스에 속하지 않아도 됩니다. 즉, 클래스 자체가 제네릭일 필요는 없습니다.

3) 제네릭 메서드 사용 시 장점

  • 타입에 독립적인 메서드를 정의할 수 있습니다. 특정 메서드가 여러 타입을 처리해야 할 때 유연한 방법을 제공합니다.
  • 코드 재사용성이 높아집니다. 동일한 로직을 여러 타입에 대해 중복해서 작성할 필요가 없습니다.

4) 제네릭 메서드 사용 예시

public class GenericMethodExample {

    // <T>를 통해 메서드가 여러 타입을 처리할 수 있게 정의
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};  // Integer 타입 배열
        String[] strArray = {"A", "B", "C"};  // String 타입 배열

        // Integer 타입 배열 출력
        // 1
        // 2
        // 3
        // 4
        printArray(intArray);  // T가 Integer로 대체됨

        // String 타입 배열 출력
        // A
        // B
        // C
        printArray(strArray);  // T가 String으로 대체됨
    }
}
  • <T>타입 매개변수이며, 컴파일 시점에 타입이 결정됩니다.
  • printArray 메서드는 다양한 타입의 배열을 처리할 수 있습니다. 이 예제에서 intArrayInteger[] 타입으로, strArrayString[] 타입으로 호출됩니다.

2.4 제네릭 클래스와 제네릭 메서드의 차이점

  • 제네릭 클래스클래스 수준에서 타입 매개변수를 정의하고, 그 클래스의 모든 메서드와 필드에서 동일한 타입을 사용합니다.
  • 제네릭 메서드메서드 수준에서 타입 매개변수를 정의하고, 메서드가 호출될 때마다 타입을 유연하게 지정할 수 있습니다. 제네릭 메서드는 클래스 자체가 제네릭일 필요는 없습니다.

2.5 제네릭 타입 제한 (Bounded Type)

제네릭에서 타입 제한을 통해 특정 타입이나 그 상위/하위 타입만 허용할 수 있습니다. 이를 통해 제네릭을 사용할 때 타입 안정성을 유지하면서도, 더 구체적인 타입을 요구할 수 있습니다. 이때 제네릭 타입 제한은 클래스 선언부에서 직접 설정할 수도 있고, 와일드카드(?)를 이용해 메서드에서 유연하게 처리할 수도 있습니다.

1) 상한 제한 (Upper Bounded Wildcards)

상한 제한T extends 클래스로 지정하여 특정 클래스와 그 하위 클래스만 허용하는 방식입니다. 즉, 제네릭 타입 파라미터가 특정 클래스 또는 그 하위 클래스여야 한다는 조건을 설정할 수 있습니다.

class NumberBox<T extends Number> {  // T는 Number 또는 그 하위 클래스만 허용
    private T value;

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

    public T get() {
        return value;
    }
}

public class BoundedTypeExample {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>();  // Integer는 Number의 서브클래스
        intBox.set(100);
        System.out.println(intBox.get());

        // NumberBox<String> strBox = new NumberBox<>();  // 컴파일 에러: String은 Number의 서브클래스가 아님
    }
}
  • T extends NumberNumber 클래스의 하위 클래스만 제네릭 타입으로 사용할 수 있다는 것을 의미합니다. 따라서 Integer, Double, Float 같은 숫자 타입만 허용됩니다.
  • 컴파일 시점에 타입이 제한되므로, NumberBox<String>과 같은 잘못된 타입 선언을 방지할 수 있습니다.

2) 하한 제한 (Lower Bounded Wildcards)

하한 제한은 와일드카드(? super T)를 통해 특정 타입과 그 상위 타입만 허용하는 방식입니다. 이는 주로 객체를 추가하거나 수정하는 메서드에서 사용되며, 입력이나 추가 작업이 필요할 때 유용합니다.

하한 제한을 사용하면, 특정 타입뿐만 아니라 그 상위 클래스에서도 안전하게 값을 추가할 수 있습니다.

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

public class LowerBoundedWildcardExample {
    // Integer의 상위 클래스인 Number나 Object만 허용
    public static void addNumbers(List<? super Integer> list) {  
        list.add(1);  // Integer 타입 값 추가 가능
        list.add(2);
    }

    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();  // Number는 Integer의 상위 타입
        addNumbers(numberList);  // Number 타입 리스트에 Integer 추가 가능
        System.out.println(numberList);  // [1, 2] 출력

        List<Object> objectList = new ArrayList<>();  // Object는 Integer의 상위 타입
        addNumbers(objectList);  // Object 타입 리스트에도 Integer 추가 가능
        System.out.println(objectList);  // [1, 2] 출력
    }
}
  • <? super Integer>Integer와 그 상위 클래스(예: Number, Object)만 허용하는 제네릭 리스트입니다.
  • 이 방식은 객체를 추가하는 경우에 유용하며, NumberObject와 같은 상위 타입 리스트에도 안전하게 값을 추가할 수 있습니다.

3) 하한 제한과 와일드카드의 관계

하한 제한은 주로 와일드카드(? super T)를 사용하여 메서드나 제네릭 클래스에서 상위 클래스까지도 허용하고 싶을 때 사용됩니다. 즉, 상위 클래스와의 유연한 타입 처리를 지원합니다.

  • 상한 제한(Upper Bounded)은 <? extends T> 형태로, 특정 클래스와 그 하위 클래스만 허용합니다.
  • 하한 제한(Lower Bounded)은 <? super T> 형태로, 특정 클래스와 그 상위 클래스만 허용합니다.

하한 제한은 제네릭 메서드에서 와일드카드를 통해 사용되므로, 더 자세한 설명은 와일드카드 파트에서 설명하겠습니다.

4) 와일드카드와의 차이점

  • 타입 제한제네릭 클래스나 인터페이스 선언 시 타입 매개변수에 대한 제한을 설정합니다. 예를 들어, T extends Number는 제네릭 클래스 선언 시 특정 타입 제한을 명확히 지정하는 것입니다.
  • 와일드카드메서드 매개변수에서 사용되며, 상한(? extends T) 또는 하한(? super T)을 통해 메서드의 파라미터 타입을 유연하게 처리할 수 있습니다. 와일드카드는 제네릭 클래스메서드 호출 시점에 입력 파라미터의 타입을 제한하는 방식으로 사용됩니다.
profile
배우고 느낀점들을 기록합니다. 열정 넘치는 백엔드 개발자로 남고싶습니다.

0개의 댓글