본문 바로가기
Java관련/Java

Java 제네릭

by devstep 2022. 11. 28.

Java 제네릭

학습주제

  • 용어
  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure : 바이트코드 보면서 개념적으로 이해

제네릭

  • 메서드나 클래스에 컴파일시 타입 체크를 해주는 기능
    • 제네릭 아닌 것은 컴파일시에 타입체크를 안하나?
  • 객체 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높히고 타입 캐스팅을 제거하여 형변환을 생략할 수 있습니다.

용어

용어 영문
매개변수화 타입 parameterized Type List
제네릭 타입 generic type List
정규 타입 매개 변수 formal type parameter E
실제 타입 매개 변수 actual type parameter String
원시 타입(로 타입) raw type List
비한정적 와일드카드 타입 unbounded wildcard type List<?>
한정적 와일드카드 타입 bounded wildcard type List<? extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable
제네릭 메서드 generic method static List asList(E[] a)
타입 토큰 type token String.class

컴파일 후 원시 타입

class Box<T> {
    T item;

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

Box<String>, Box<Integer> 는 지네릭 클래스 Box에 서로 다른 타입을 대입하여 호출한 것일 뿐 별개의 클래스를 의미하는 것이 아닙니다.
Box<String>, Box<Integer>컴파일 후에 제네릭 타입이 제거되어 원시 타입인 Box로 바뀝니다.

객체 별 다른 타입인 제네릭, 제한 사항

Box<String>, Box<Integer> 처럼 제네릭 클래스 Box의 객체를 생성할 때 객체별로 다른 타입을 지정할 수 있다.

  • ⭐ 그럼으로 static멤버에 타입 변수를 사용할 수 없다. 타입 변수는 인스턴스 변수로 간주되기 때문입니다.
  • ⭐그렇다면 static 메서드는 사용할 수 있을까? 코드로 확인해보자.
  • 지네릭 타입의 배열 생성도 허용되지 않는다.
    • new 연산자는 컴파일 시점에 타입이 무엇인지 정확히 알아야 한다.
    • instanceof 연산자도 컴파일 시점에 타입을 알아야하기 때문에 instanceof에는 raw type을 사용한다.
    • 지네릭 배열의 실제 구현을 보려면 src.zip에서 Collections.toArray(T[] a)의 소스를 찾아보라고 한다.
    • 1) 제네릭 배열을 생성할 필요가 있을 때는, 리플렉션 api의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성
    • 2) Ojbect 배열을 복사한 다음 T[]로 형변환

원시 타입과 Object를 타입 매개변수로 사용하는 것의 차이

List와 List<Object>를 예시로 사용하겠습니다.

  • List : 제네릭 타입을 완전히 적용하지 않은 것
  • List<Object> : 모든 타입을 허용한다는 의사를 컴파일러에게 전달한 것

List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 raw type을 사용하면 타입 안정성을 잃게 된다.

  • List raw type 사용 - 런타임시 ClassCastException 발생
    • class java.lang.Integer cannot be cast to class java.lang.String
public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0);
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }
  • List<Object> 사용
    • 컴파일되지 않으므로 타입 안정성 보장
    private static void unsafeAdd(List<Object> list, Object o) {
        list.add(o);
    }

제네릭 사용법

객체의 생성

Box의 객체를 생성할 때는 참조변수와 생성자에 대입된 매개변수화된 타입이 일치해야 한다. 일치하지 않으면 에러가 발생한다.

  1. ⭐ 매개변수화 두 타입이 상속관계여도 에러 발생
    • Box<Apple> appleBox = new Box<Apple>() 참조변수와 매개변수화 타입이 정확하게 같아야 한다.
    • //1. 타입 불일치 - 에러 Box<Apple> appleBox = new Box<Grape>(); //에러 //2. 매개변수화 두 타입이 상속관계(Apple이 Fruit의 자손) - 에러 Box<Fruit> appleBox = new Box<Apple>(); //에러 //3. 객체 추가 가능 Box<Fruit> fruitBox = new Box<Fruit>(); fruitBox.add(new Apple()); //OK //4. 두 제네릭 클래스가 상속관계이고, 대입된 매개변수화 타입 일치 - 허용 Box<Apple> appleBox = new FruitBox<Apple>(); //OK. 다형성
  2. JDK 1.7 부터 추정가능한 경우 타입 생략 가능
  3. Box<Apple> appleBox = new Box<>();

타입 변수

임의의 참조형 타입을 의미하는 것으로 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋습니다.

타입변수 약자 예시
T Type Game<T>
E Element ArrayList<E>
K,V Key,Value Map<K,V>

제네릭 주요 개념 - 바운디드 타입

  • 타입 매개변수에 지정할 수 있는 타입의 종류 제한
// 과일박스에 장난감을 담는 것 가능.
FruitBox<Toy> fruitBox = new FruitBox<>();
fruitBox.add(new Toy());

// 제한된 제네릭 클래스 : Fruit의 자손만 타입 지정 가능.
class FruitBox<T extends Fruit> { 
  ArrayList<T> list = new ArrayList<>();
}

class FruitBox<T extends Fruit & Eatable> {...}

제네릭 주요 개념 - 와일드 카드

제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않습니다. 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하기 때문입니다.
이럴 때 사용하기 위핸 고안된 것이 와일드카드 입니다.

?만으로는 Object 타입과 다를 게 없으므로 extend(upper bound), super(lower bound)를 제한할 수 있습니다.
그리고 와일드카드에는 &를 사용할 수 없습니다.


제네릭 메소드 만들기

  1. 메서드 선언부에 제네릭 타입이 선언된 메서드
  2. static <T> void sort(List<T> list, Comparator<? super T> c)
  3. 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 별개의 것이다. 같은 타입문자 T를 사용했지만 서로 다른 것이다.
  4. class FruitBox<T> { ... static <T> void sort(List<T> list, Comparator<? super T> c) }
  5. static 메서드에 제네릭 타입을 선언하고 사용하는 것 가능
    • 메서드에 선언된 제네릭 타입은 지역 변수를 선언한 것과 같다고 생각하면 됩니다. 해당 타입 매개변수를 메서드 내에서만 사용할 것이므로 메서드가 static인 것은 상관이 없다.

제네릭 메서드 호출

제네릭 메서드 생성

    //전
    static Juice MakeJuice(FruitBox<? extends Fruit> box) {
        String tmp ="";
        for(Fruit f : box.getList) tmp += f + "";
        return new Juice(tmp);
    }

    //제네릭 메서드로 바꾼 후
    static <? extends Fruit> Juice MakeJuice(FruitBox<T> box) {
        String tmp ="";
        for(Fruit f : box.getList) tmp += f + "";
        return new Juice(tmp);
    }

제네릭 메서드 호출

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
Juicer.<Fruit>makeJuice(fruitBox);

컴파일러가 타입을 추정할 수 있기 때문에 생략해도 됩니다. fruitBox의 선언부를 통해 대입된 타입을 컴파일러가 추정할 수 있기 때문입니다.

그러나 제네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없습니다.
같은 클래스 내에 있는 멤버들끼리는 참조변수(this등)나 클래스 이름만으로 호출이 가능하지만 대입된 타입이 있을 때는 반드시 써줘야하는 것이 규칙입니다.


Erasure : 바이트코드 보면서 개념적으로 이해

제네릭 타입을 이용해 소스파일을 체크하고 필요한 곳에 형변환을 넣어줍니다. 그리고 제네릭 타입을 제거합니다.
컴파일된 .class 파일에는 제네릭 타입에 대한 정보가 없다.

  1. 제네릭 타입의 경계(bound)를 제거한다.

    • 제네릭 타입이 면 T는 Fruit로 치환된다.
    • 인 경우는 Object로 치환된다.
    • 그리고 클래스 옆의 선언은 제거된다.
  2. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.

    List의 get()은 Object 타입을 반환하므로 형변환이 필요합니다.

    // 코드
    T get(int i) {
      return list.get(i);
    }
    
    //변환 후
    Fruit get(int i) {
      return (Fruit)list.get(i);
    }
  3. 와일드카드가 포함되어 있는 경우에 적절한 타입으로 형변환이 추가된다.

      //코드
      static Juice MakeJuice(FruitBox<? extends Fruit> box) {
        String tmp ="";
        for(Fruit f : box.getList) tmp += f + "";
        return new Juice(tmp);
    }
    
    //변환 후
    static Juice MakeJuice(FruitBox box) {
        String tmp ="";
        Iterator it = box.getList().iterator();
        while(it.hasNext()) {
            tmp += (Fruit)it.next() + "";
        }
        return new Juice(tmp);
    }

참고자료

'Java관련 > Java' 카테고리의 다른 글

[프로젝트]exception 을 static으로 만든 이유  (0) 2023.04.07
필요없는 검사 예외 사용은 피하라  (0) 2023.01.15
Java 애너테이션  (0) 2022.11.23
Java enum  (0) 2022.11.04
Java 클래스  (0) 2022.09.30

댓글