Java/개념

제네릭스 (Generics)

태감새 2023. 1. 15. 18:33

제네릭이란?

컴파일시에 타입을 체크함으로써 코드의 안정성을 높여주는 기능

  • 컴파일시 타입체크를 진행하여 강력한 타입 검사 실시
  • 타입 캐스팅을 자동으로 해줌

예시 (ArrayList)

ArrayList<String> arr = new ArrayList<String>();
arr.add("abc");
arr.add(new Integer(2));    // error

제네릭는 <>를 사용해서 타입을 지정해준다.

위의 ArrayList는 으로 제네릭를 지정했다. 그래서 arr 객체에 String 클래스가 아닌 클래스는 추가할 수 없다.

ArrayList 클래스 일부

ArrayList 클래스를 보면 같은 제네릭를 볼 수 있다. E를 타입 변수라고 하고 주로 E나 T를 자주 사용한다. Map같이 두 변수를 사용하는 경우에는 <K,V>와 같은 형식으로 두개를 사용한다. 우리가 ArrayList를 생성할 때 을 넣은 것처럼 클래스를 입력해주면 E 값이 입력한 클래스로 변경된다. 그래서 형변환 없이 입력한 클래스의 메서드를 사용할 수 있는 것이다.

이처럼 제네릭가 붙은 클래스를 '제네릭 클래스(타입)'라 하고 일반 클래스를 '원시 타입'라고 부른다.

제네릭의 규칙

1. 인스턴스를 생성할 때는 제네릭타입이 일치해야 한다. (와일드 카드 제외)

ArrayList<Object> arr = new ArrayList<Object>();
ArrayList<Object> arr = new ArrayList<String>(); //error

상속 관계에 있더라도 생성이 불가능하니 주의하자.

2. 생성된 객체에는 자손 객체의 입력이 가능하다.

arr.add("abc");  // 가능

제네릭 타입을 제한하는 방법

이렇게 두면 어떤 클래스든 입력이 가능하다.
객체를 생성하면 타입을 제한할 수 있지만 특정 타입의 객체만 생성하고 싶다면 어떻게 해야할까?


제네릭 타입을 제한하기 위해 extends나 super를 사용한다.


<T extends Guitar>는 Guitar 클래스를 포함한 그 자손 클래스만 입력이 가능하다는 뜻이다.

super는 아래 와일드 카드 부분에서 설명하겠다.

와일드 카드

제네릭 클래스 객체 중에서 특정 타입만을 허용하고 싶을 때 와일드 카드를 사용한다.
와일드 카드를 사용하면 제네릭 클래스의 객체를 생성할 때 제네릭 타입에 자손 클래스를 입력할 수 있다.

  • <? extends Guitar> : Guitar와 그 자손들 가능
  • <? super Guitar> : Guitar와 그 조상들 가능
  • <?> : 제한 없음.
public static void main(String[] args) {
    Test(new ArrayList<String>());
    Test(new ArrayList<Integer>());
}
public static void Test(ArrayList<? extends Object> a){
    System.out.println("성공");
}
// 성공
// 성공

이처럼 와일드 카드를 사용하면 제네릭 타입에도 다형성을 적용할 수 있다.

사용 예시

public class Run {
    public void popTest(Category<? extends Guitar> guitar) {
        Guitar g = guitar.get();
        guitar.set(new Guitar()); // compile error
    }

    public void pushTest(Category<? super Guitar> guitar) {
        Guitar g = guitar.get(); // compile error
        guitar.set(new Guitar());
    }
}
  • 상한경계(extends)를 사용하는 경우는 set이 불가능하다.
    • 입력하는 객체의 타입이 상위 클래스일 가능성이 있기 때문
  • 하한경계(super)를 사용하는 경우는 get하여 경계 클래스로 참조하는 것이 불가능하다.
    • get하는 객체가 경계 클래스보다 상위 클래스일 가능성이 있기 때문

그럼 언제 상한과 하한을 사용해야 할까?

  • 상한 경계
    • 타입을 안전하게 가져올 수 있음
    • 무언가를 생성하는 경우에는 상한경계를 사용하는 것이 적절
  • 하한 경계
    • 타입을 안전하게 저장할 수 있음
    • 소비를 하는 경우에는 하한경계를 사용하는 것이 적절
class Category<E> {
    private List<E> list = new ArrayList<>();

    public void pushAll(Collection<? extends E> box) {
        for (E e : box) {
            list.add(e);
        }
    }

    public void popAll(Collection<? super E> box) {
        box.addAll(list);
        list.clear();
    }
}

제네릭의 제약

1. 스태틱 변수, 메서드에는 타입 변수를 사용할 수 없다. (제네릭 메서드 제외)

class Box<T> {
    static T item;                   // error
    static int compare (T t1,T t2);  // error
}

2. 배열 생성이 불가능하다.

T[] arr = new T[3];  // error

new 연산자는 컴파일 시점에 타입을 정확히 알아야하기 때문이다.

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 의미한다.

메서드의 매개변수의 타입을 지정한다.

선언부의 T와 매개변수 T가 일치해야 한다.

static <T> void sort(...<T>...){...}

이 타입 매개변수는 클래스의 타입 매개변수와는 독립적이다.
제네릭 메서드의 타입 매개변수는 제네릭 메서드 내에서만 유효하다. 지역변수라고 생각하면 쉽게 이해가 된다.

제네릭 타입의 제거

제네릭은 컴파일시 필요한 형변환을 넣어주고 지워진다.

정리

  • 제네릭은 컴파일 시 강력한 타입 체크를 위한 도구이다.
  • <T extends Guitar>와 같이 타입을 제한하면 원하는 타입의 객체만 생성 가능하다.
  • <? extends Guitar>는 생성된 객체의 참조하는 경우 다형성을 제공해준다.

참고

자바의 정석 기본편

https://www.youtube.com/watch?v=w5AKXDBW1gQ 

'Java > 개념' 카테고리의 다른 글

스트림 (Stream)  (0) 2023.01.18
람다식 (Lamnda Expression)  (0) 2023.01.17
(Collection) Map  (0) 2023.01.15
(Collection) Set  (0) 2023.01.15
(Collection) Iterator, Comparator, Comparable  (0) 2023.01.15