개발 기록

>[Java] 제네릭(Generics) - 기본 개념과 사용법 본문

JAVA

>[Java] 제네릭(Generics) - 기본 개념과 사용법

1z 2024. 2. 5. 22:50

 

1. 개념  

 

Java 제네릭(Generics)은 타입 안정성(type safety)을 제공하고 재사용 가능한 코드를 작성할 수 있도록 하는 기능이다.

 

리스트를 생성할 때 해당 리스트가 문자열(String) 요소만을 저장할 수 있도록 타입을 제한 하기 위해서, 아래처럼 생성 할것이다.

List<String> textList 에서 <String>이 바로 제네릭 타입 매개 변수로, 실 타입 매개변수라고 한다. 리스트가 어떤 타입의 요소를 저장할지 지정하며, 컴파일러가 요소의 타입이 'String' 인지 체크하기 때문에 다른 타입의 데이터를 넣을시 컴파일 오류가 발생한다.

List<String> textList = new ArrayList<>();

 

 

Java의 컬렉션 프레임워크는 제네릭을 기반으로 설계되어 있는데 List 인터페이스를 봐보면 알 수 있다. List 인터페이스에 선언되어 있는 List<E>의 E 형식 타입 매개변수라고 한다. 

 

☞ 실 타입 매개변수, 형식 타입 매개변수는 제네릭 클래스 또는 인터페이스를 정의할 때 타입을 파라미터화하는데 사용한다.

 형식 타입 매개변수: 제네릭 클래스 또는 인터페이스의 선언에서 사용되며, 타입을 추상화 ex. <T>, <E>, <K, V>

 실 타입 매개변수: 실제로 사용할 때 지정되는 구체적인 타입

 

개념은 알겠고 그렇다면 왜 제네릭을 사용해야 할까? 

 

※ 제네릭으로 얻는 이점 

① 타입 안정성: 제네릭은 컴파일 시에 타입 검사를 수행하여 타입 불일치로 인한 런타임 오류를 사전에 방지한다.

② 코드 재사용성 및 유연성: 다양한 타입에 인자를 처리할 수 있으므로 동일한 로직을 재사용할 수 있다.

③ 가독성: 타입캐스팅 같은 명시적인 형 변환을 줄일 수 있다.

 

※  제네릭의 한계 

타입 소거로 인한 제한: 타입 소거로 인해 런타임 시 제네릭 타입의 실제 타입 정보를 알기 어렵다.

원시 타입과의 호환성: 제네릭 코드는 원시 타입(raw type)과 호환성이 있어 제네릭 타입에서 타입 안전성을 보장하지 못할 수 있다.

 


2. 사용법   

(1) 매개변수화 타입 class<T>, interface<T> 

매개변수화 타입은 제네릭 타입의 한 종류로, 타입 매개변수를 포함하는 클래스 또는 인터페이스를 말한다. 

 

형식 타입 매개변수

사용할 타입을 추상적으로 나타내며, 타입 매개변수는 보통 T, E, K, V 등으로 표현한다.

// Box 클래스가 어떤 타입의 객체를 다룰지 추상적으로 나타낸다.
public class Box<T> {
    private T content;

    public T getContent() {
        return content;
    }

    public void setContent(T content) {
        this.content = content;
    }
}

 

▶  타입 매개변수

: 제네릭 클래스 또는 인터페이스의 형식 타입 매개변수에 실제 타입을 바인딩한다. 제네릭 클래스 또는 인터페이스를 인스턴스화할 때 구체적인 타입을 지정한다.

public class Main {

	public static void main(String[] args) {
		// Box<String> 형식 타입 매개변수 T에 대해 실 타입 매개변수를 String을 지정하여 Box 클래스를 인스턴스화한다.
		Box<String> stringBox = new Box<>("Hello");
		System.out.println(stringBox.getContent()); // 출력: Hello

		// Box<Integer>는 형식 타입 매개변수 T에 대해 실 타입 매개변수를 Integer로 지정하여 Box 클래스를 인스턴스화한다.
		Box<Integer> intBox = new Box<>(123);
		System.out.println(intBox.getContent()); // 출력: 123
	}
}

 

 

(2) 제네릭 메서드<T> R method(T t) 

메서드 선언 시에 타입 매개변수를 사용하여 여러 타입의 인자를 처리할 수 있는 메서드이다. 

<T>: 메서드에 사용되는 타입 매개변수로, 임의의 대문자 알파벳으로 표시

  R: 반환할 값의 타입

T parameter: 제네릭 메서드의 매개변수

// 이 메서드는 입력된 value를 String으로 변환하여 반환한다.
public class GenericMethodExample {

    // 제네릭 메서드 선언
    // <T, R> 두 개의 타입 매개변수를 사용하여 입력값과 반환값의 타입을 독립적으로 다룬다.
    public static <T, R> R convert(T value) {
        // value를 String으로 변환하여 (R) 형변환을 통해 반환
        return (R) String.valueOf(value);
    }

    public static void main(String[] args) {
        // 제네릭 메서드를 사용하여 타입 변환하기
        Integer intValue = 100;
        String stringValue = convert(intValue);
        System.out.println("Converted String value: " + stringValue);

        Double doubleValue = 3.14;
        String doubleStringValue = convert(doubleValue);
        System.out.println("Converted String value: " + doubleStringValue);
    }
}

 

실행 결과

 

 

* 참고: 리턴타입을 알아내는 방법(=매개변수도 마찬가지)

1. 명시적인 타입 지정: 메서드 이름 뒤에 <타입>을 붙여서 반환 타입 지정

String result = GenericMethodExample.<Integer, String>convert(123); // String 타입으로 반환 받음

2. 타입 추론: Java 컴파일러는 제네릭 메서드 호출 시 입력 인자를 보고 타입을 추론할 수 있기 때문에 명시적으로 지정할 필요가 없다. 

// Integer 타입의 인자를 전달하면 컴파일러는 T를 Integer로 추론한다.

Integer result = GenericMethodExample.convert(123);

 

3. 컴파일러 경고 무시

@SuppressWarnings("unchecked") // 경고 무시
String result = GenericMethodExample.convert(123);

 

 

(3) 타입 바운드 - 제네릭 제한 

제네릭 타입 매개변수에 대한 제한을 설정한다. extends 키워드를 사용하여 특정 클래스나 인터페이스의 하위 타입만 허용한다.

public <T extends Number> double sumOfList(List<T> list) {
    double sum = 0.0;
    for (T element : list) {
        sum += element.doubleValue();
    }
    return sum;
}

 

(4) 언바운드 타입 - 와일드 카드 타입 <?>, <? extends ..>, <? super ..> 

<?>와 같은 형태로 표현되며, 어떤 타입도 매치될 수 있는 와일드카드 타입이다. 제한 없이 모든 타입을 다룰 수 있다.

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

 

※ 바운드 와일드 타입 <? extends T>, <? super T>

:와일드카드의 상한 또는 하한을 제한하는 데 사용된다.

ex. <? extends Number>는 Number 또는 Number의 하위 클래스만 허용

ex. <? super Integer>는 Integer 또는 Integer의 상위 클래스만 허용

 

 

(4) 제네릭 서브타이핑 

제네릭에서는 서브타이핑(subtyping) 규칙을 따른다. List<String>은 List<Object>의 서브타입이며, 제네릭 타입 간에도 상속 및 구현 관계가 유지된다.

 

(5) 제네릭 타입의 상속과 구현 

부모 클래스의 타입 매개변수를 자식 클래스에서 유지하거나 변경할 수 있다. 아래 예제는 구현 관계를 보여준다.

// 제네릭 인터페이스 정의
public interface Container<T> {
    void add(T item);
    T get(int index);
}

// 구현 클래스
// 제네릭 클래스 정의
public class ListContainer<E> implements Container<E> {
  // 제네릭 타입 E는 리스트의 요소 타입을 나타낸다.
    private List<E> items = new ArrayList<>();

    @Override
    public void add(E item) {
        items.add(item);
    }

    @Override
    public E get(int index) {
        return items.get(index);
    }
}

 

public class Main {
    public static void main(String[] args) {
        // 제네릭 리스트 인스턴스 생성
        ListContainer<String> stringList = new ListContainer<>();

        // 리스트에 요소 추가
        stringList.add("Java");
}