코딩하는 털보

STUDY HALLE - 14주차 본문

Diary/Study Halle

STUDY HALLE - 14주차

이정인 2021. 2. 24. 18:33

목표

자바의 제네릭에 대해 학습하세요.

학습할 것

  • 제네릭
    • 제네릭을 사용하는 이유
    • 자바 제네릭
  • 제네릭 사용법
    • 제네릭 클래스 선언하기
    • 제네릭 타입 호출하기
    • 다이아몬드
    • 여러개의 타입 파라미터
    • raw 타입
  • 제네릭 메소드
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
    • 바운디드 타입
    • 와일드 카드
  • Type Erasure
    • Erasure
    • 브릿지 메소드
  • 제네릭 타입 주의사항

전체적으로 아래 문서를 참조하였고 부분적으로는 각 파트에 명시함.

https://docs.oracle.com/javase/tutorial/java/generics/index.html


자바 제네릭

Generics를 사용하는 이유

간단히 말해서 제네릭은 클래스, 인터페이스 및 메서드를 정의 할 때 자료형이 매개 변수가되도록한다. 메서드 선언에 사용되는 다른 파라미터들과 마찬가지로 타입 파라미터는 서로 다른 입력으로 동일한 코드를 재사용 할 수있는 방법을 제공한다. 차이점은 일반적인 매개 변수에 대한 입력은 값이고 타입 매개 변수에 대한 입력은 자료형이라는 것이다.

제네릭을 사용하는 코드는 제네릭을 사용하지 않은 코드에 비해 많은 이점이 있다.

  • 컴파일 타임에 더 강력한 타입 검사. (type-safed)
    Java 컴파일러는 강력한 타입 검사를 코드에 적용하고 코드가 type-safety를 위반하면 오류를 발생시킨다. 컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽다.
  • 캐스트 제거.

    제네릭이 없는 다음 코드는 캐스팅이 필요하다.

    List list = new ArrayList();
    list.add("안녕하세요");
    //타입 캐스팅
    String s = (String) list.get(0);

    제네릭을 사용하도록 다시 작성하면 코드에 캐스팅이 필요없다.

    List<String> list = new ArrayList<String>();
    list.add("안녕하세요");
    // 캐스팅 없음
    String s = list.get(0);
  • 프로그래머가 일반 알고리즘을 구현할 수 있다.
    프로그래머는 제네릭을 사용하여 다양한 자료형의 컬렉션에서 작동하고 사용자 정의 할 수 있으며 type-safety하고 가독성있는 제네릭 알고리즘을 구현할 수 있다.

Generic Types

제네릭 타입은 매개 변수화된 자료형을 사용하는 제네릭 클래스 또는 인터페이스이다.

제네릭을 사용하지 않고 작성한 모든 자료형의 객체에서 사용할 수 있는 Box class.

public class Box {
    private Object object;

    public void set(Object object) {this.object = object;}
    public Object get() {return object;}
}

예제에서는 메서드가 Object를 받거나 반환하므로 primitive 타입이 아니라면 원하는대로 자유롭게 전달할 수 있다. 그러나 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 방법이 없다.

예를들어 Integer를 Box에 넣어서 Integer를 얻을 것으로 예상했지만 다른 부분에서 실수로 String을 전달하여 런타임 에러가 발생할 수 있다.

제네릭을 사용하여 작성한 모든 자료형의 객체를 담아 사용할 수 있는 Box class.

public class Box<T> {
    // T는 "타입"을 나타냄
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}
}

이 클래스로 생성되는 Box 객체는 특정한 타입을 매개변수로 받아 클래스 내부에서 사용하게 된다. 제네릭 타입은 프리미티브 타입을 타입 매개변수로 받을 수 없는데... 아마 내부적으로 Object로 변환하는 로직이 있고 프리미티브 타입은 Object를 상속받지 않기 때문이 아닐까? 이것으로 List<int> 같은 컬렉션을 사용할 수 없었던 이유를 알게되었다.

만약 Integer를 타입 매개변수로 받은 Box객체에 실수로 String을 전달한다면?

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {
        //int는 제네릭 타입에 사용할 수 없다. 인텔리제이는 Integer를 대신 추천해준다.
        //Box<int> box = new Box<int>();
        Box<Integer> box = new Box<Integer>();

        //실행하지 않아도 빨간 밑줄이 쳐진다. 즉 컴파일 에러!
        box.set("나는 문자열입니다요.");
    }
}

컴파일 타임에서 강력한 검사를 하기 때문에 String은 집어넣을 수 없다고 컴파일 시점에서 이미 알 수 있게 된다.

그런데 Integer 타입으로 정의된 t 변수에 int를 명시적 변환 없이 대입할 수 있다.

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {
        Box<Integer> box = new Box<Integer>();
        int a=3;
        box.set(a);
        System.out.println(box.get());
    }
}

이것은 Java 컴파일러의 오토박싱 기능이다. 컴파일러에 의해 프리미티브 값 ( 예 : int )이 래퍼 클래스( Integer )의 객체로 변환되는 것이다. 여기서 List<Integer>int형을 자연스럽게 add() 할 수 있었던 이유를 알게되었다.

언박싱이라는 이 반대의 기능도 있다.

public class Unbox {

    public List<Integer> integer;

    public static void main(String[] args) {
        Unbox unbox = new Unbox();
        int sum = 0;
        List<Integer> li = unbox.integer;
        for (Integer i : li) {
            if (i % 2 == 0) {
                sum += i;
            }
        }
        System.out.println(sum);
    }
}

Integer 래퍼클래스의 경우 나머지(%) 및 단항 더하기(+=) 연산자를 적용할 수 없지만 컴파일러가 Integer.intValue() 메소드를 자동으로 호출해서 Integer를 int로 변환해준다.

참고 자료

https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html


제네릭 사용법

제네릭 클래스 선언하기

제네릭 클래스는 다음과 같은 형식으로 정의된다.

class name <T1, T2, ..., Tn> {/ * ... * /}

꺾쇠 괄호( <> )로 구분 된 타입 매개 변수는 클래스 이름 뒤에 온다. 객체가 생성될 때 타입 파라미터를 받는 부분이다.

타입 파라미터와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 위해서 정해진 규칙에 따라 타입 파라미터는 단일 대문자를 사용한다.

가장 일반적으로 사용되는 타입 매개변수 이름은 다음과 같다.

  • E-엘리먼트 (Java Collections Framework에서 광범위하게 사용됨)
  • K-키
  • N-숫자
  • T-타입
  • V-값
  • S, U, V 등-2, 3, 4 종

온갖 재료(자료형)를 담을 수 있는 마녀 화로를 제네릭 클래스로!

public class WitchPot<T> { 
    private T meterial;
}

제네릭 타입 호출하기

코드 내에서 제네릭 클래스를 참조하려면 TInteger 와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야한다 .

GenericClass<Type args> var;

객체 만들기는 일반적인 객체 생성과 유사하지만 인스턴스 생성 부분에서도 선언처럼 <Type args>를 기입해야 한다.

GenericClass<Type args> var = new GenericClass<Type args>();

마녀 화로 객체 만들기

public class WitchPot<T> {
    private T meterial;

    public static void main(String[] args) {
        WitchPot<Integer> pot; = new WitchPot<Integer>();
    }
}

다이아몬드

Java SE 7 부터 컴파일러가 선언을 살펴본 후 타입을 추론 할 수 있다면 일반 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 인자 <>로 바꿀 수 있다.

<>를 다이아몬드라고 한다.

public class WitchPot<T> {
    private T meterial;

    public static void main(String[] args) {
        //선언부에 Integer로 명시되어 있기 때문에 타입 추론을 통해 다이아몬드로도 객체 생성 가능.
        WitchPot<Integer> pot; = new WitchPot<>();
    }
}

여러개의 타입 파라미터

제네릭 클래스는 여러 타입 매개 변수를 가질 수 있다.

interface Pair <K, V> {
    public K getKey();
    public 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;
    }

    public K getKey() {return key;}
    public V getValue() {return value;}

    public static void main(String[] args) {
        //다음 코드는 OrderedPair 클래스의 두 인스턴스를 만든다.
        //선언에서 K 및 V 의 타입을 유추 할 수 있으므로 다이아몬드 표기법을 사용하여 코드를 단축할 수 있다.
        //p1에서 두번째 인자로 받은 타입은 사실 int 이지만 오토박싱에 의하여 자동으로 Integer로 변환된다.
        Pair <String, Integer> p1 = new OrderedPair <> ( "Even", 8);
        Pair <String, String> p2 = new OrderedPair <> ( "hello", "world");
    }
}

raw 타입

raw 타입은 제네릭을 사용하지 않았던 과거 자바 버전과의 호환성을 위해서 존재하는 타입 매개변수가 없는 제네릭 타입이다.

raw 타입에 매개변수화된 제네릭 타입을 할당할 수 있다.

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {
        //raw 타입 생성
        Box rawBox = new Box();

        Box<String> pBox = new Box<>();

        //raw type에 parameterized type 대입
        rawBox = pBox;
    }
}

반대로 매개변수화된 제네릭 타입에 raw 타입을 대입하면? 또는 raw 타입으로 제네릭 메소드를 호출한다면?

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {

        Box rawBox = new Box();

        //parameterized type에 raw type 대입
        Box<String> pBox = rawBox;
        System.out.println("OK");
    }
}
public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {

        Box rawBox = new Box();
        //raw 타입으로 제네릭 메소드 호출
        rawBox.set(3);
        System.out.println(rawBox.get());
        System.out.println("OK");
    }
}

두 경우에는 컴파일 단계에서 아래와 같은 경고메시지를 받는다. 다만 경고일 뿐 컴파일은 진행된다.

> javac Box.java
Note: Box.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
> java Box                        
OK

컴파일러가 힌트로 준 옵션을 사용하여 다시 컴파일하면 구체적인 내용을 확인할 수 있다.

> javac -Xlint:unchecked Box.java
Box.java:11: warning: [unchecked] unchecked conversion
        Box<String> pBox = rawBox;
                           ^
  required: Box<String>
  found:    Box
1 warning

> java Box                        
OK

오라클 문서에 의하면 unchecked는 컴파일러가 type-safety를 보장하기 위한 타입 검사에 필요한 충분한 타입 정보가 없음을 의미한다고 한다.

@SuppressWarnings 애노테이션으로 경고를 무시할 수 도 있다.

@SuppressWarnings("unchecked")
public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {

        Box rawBox = new Box();

        Box<String> pBox = rawBox;
        System.out.println("OK");
    }
}
> javac -Xlint:unchecked Box.java 
> java Box                        
OK

그렇지만 raw 타입은 제네릭 타입 검사를 우회(type-unsafety)하여 안전하지 않은 코드에 의해 런타임 오류로 연장될 수 있으므로 raw 타입을 사용하지 않아야 한다고 말하고 있다.

참고 : https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html


제네릭 메소드

제네릭 메소드는 타입 매개변수를 사용하는 메소드이다. 제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메소드에서 타입 매개변수의 scope는 선언 된 메소드로 제한된다.

제네릭 메소드의 구문에는 메소드의 리턴 타입 전에 나타나는 꺾쇠 괄호 안에 타입 매개변수 목록이 포함된다. static 제네릭 메소드의 경우 타입 매개변수 섹션이 메소드의 리턴 타입 전에 나타나야한다.

public <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }
public static <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }

타입 인자를 받고 해당 타입 인자에 해당하는 타입의 객체를 파라미터로 받는 static 제네릭 메소드

public class Util {
    public static <T> WitchPot<T> put(T t) {
        return new WitchPot<>(t);
    }
}
public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        WitchPot<String> pot = Util.<String>put(frog);

        System.out.println(pot.meterial); //개구리

    }
}

메소드 호출하기 (명시적으로 타입 파라미터 지정)

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }
}

컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파라미터는 생략이 가능하다.

Java SE 8부터는 컴파일러의 타입 추론 개념이 확장되어 메소드 인자에 포함된 매개변수화된 타입까지 검사한다.

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        //반환 대상이 WitchPot<String> 인 것을 확인하고 String 으로 추론한다.
        WitchPot<String> pot = Util.put(frog);

        System.out.println(pot.meterial); //개구리

    }
}

제네릭 주요 개념 (바운디드 타입, 와일드 카드)

바운디드 타입

제네릭 타입에서 타입 인자로 사용할 수있는 타입을 제한하려는 경우가 있을 수 있다. 예를 들어 숫자에 대해 작동하는 메소드는 Number또는 해당 하위 클래스의 인스턴스만 허용하려고 할 수 있다. 이것이 바운디드 타입의 용도이다.

바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends키워드, 상위 바운드를 나열한다.

<T extends UpperBound>

여기서의 extends 키워드는 특별하게도 implements의 기능까지 포함하기 때문에 상위 바운드는 인터페이스가 될 수 있다.

또는 여러 개의 상위 바운드를 가질 수 도 있다.

<T extends B1 & B2 & B3>

만약 여러 상위 바운드 중에서 클래스가 있다면 해당 클래스가 가장 앞에 와야한다. 안그러면 컴파일 에러!

<T extends Class1 & Interface1 & Interface2>

제네릭 메소드의 타입 파라미터 제한하기

public class WitchPot<T> {
    private T meterial;

    public T get() {
        return this.meterial;
    }

    static <U extends Meterial> WitchPot<U> put(U u) {
        return new WitchPot<U>();
    }

    public static void main (String [] args) {
        //Meterial을 상속받은 객체만 받도록 제한되었기 때문에 문자열은 컴파일 에러!
        WitchPot.put( "나는 문자열입니다요.");
    }
}

바운디드 타입의 또 다른 특징은 상위 바운드에 해당하는 클래스의 메소드를 코드에서 사용할 수 있다는 점! 그냥 T인 경우에는 그에 대한 아무런 메소드도 코드에 적지 못했던 것과 비교된다.

제네릭 클래스에서 바운디드 타입의 상위 클래스의 메소드 사용하기

public class Meterial {
    public String name = "개구리";

    public String getName() {
        return this.name;
    }
}
package javageneric;

public class WitchPot<T extends Meterial> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public String get() {
        //바운디드 타입의 상위 클래스인 Meterial의 메소드 getName() 사용.
        return this.meterial.getName();
    }

    static <U extends Meterial> WitchPot<U> put(U u) {
        return new WitchPot<U>(u);
    }

    public static void main (String [] args) {
        Meterial meterial = new Meterial();
        WitchPot<Meterial> pot = put(meterial);
        System.out.println(pot.get());
    }
}

말했던 것 처럼 바운디드 타입이 아니라면 특정 타입으로 사용하길 기대하는 메소드를 사용하지 못할 수 있다.

    public static <T> int countGreaterThan(T[] tArray, T t) {
        int count = 0;
        for (T e : tArray)
            //compareTo() 메소드를 사용할 수 없기 때문에 컴파일 에러
            if (e.compareTo(t) > 0)
                ++count;
        return count;
    }

compareTo()가 추상화 되어있는 Comparable 인터페이스를 사용하여 문제 해결!

    public static <T extends Comparable<T>> int countGreaterThan(T[] tArray, T t) {
        int count = 0;
        for (T e : tArray)
            if (e.compareTo(t) > 0)
                ++count;
        return count;
    }

참고 자료

https://docs.oracle.com/javase/tutorial/java/generics/bounded.html

제네릭과 상속 및 하위 타입

일반적인 상속 관계에서 아래처럼 상위 클래스 타입으로 대입이 가능한 것 처럼,

public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

제네릭 타입도 이와 같이 타입 파라미터로 주어진 타입에 하위 타입으로 대입할 수 있다.

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

        public void set(T t) {this.t = t;}
        public T get() {return t;}

        public Number show(T e) {
            return this.t = e;
        };

        public static void main(String[] args) {
            Box<Number> box = new Box<>();
            System.out.println(box.show(new Integer(10)).intValue());
            System.out.println(box.show(new Double(20)).intValue());
        }
    }

그러나 기존 타입의 상속관계가 제네릭 타입까지 이전되지는 않는다. 예를들어 NumberInteger의 상위 클래스이지만 Box<Number>Box<Integer>의 상위 클래스가 아니다.

Box <Integer>가 Box <Number>의 하위 유형이 아님을 보여주는 다이어그램

제네릭 타입 사이의 상속관계 구현을 위해서는 일반 클래스 처럼 상속관계를 명시해야 한다. 좋은 예로 ArrayList<E>List<E>를 상속받고 List<E>Collection<E> 를 구현하고 있다.

ArrayList<E> implements List<E>
List<E> extends Collection<E>

diagram showing a sample collections hierarchy: ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>.

참고 사진 및 자료

https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html

와일드 카드

제네릭 타입 코드 에서 와일드 카드 라고하는 물음표 ( ? ) 는 알 수 없는 유형을 나타낸다. 와일드 카드는 파라미터 변수, 필드 또는 지역변수의 타입 등 다양한 상황(때때로 리턴 타입에도 사용할 수 있음.)에서 사용할 수 있다. 와일드 카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 수퍼 타입의 타입 인자로는 사용될 수 없다.

Upper Bounded Wildcards

상한 와일드 카드를 사용하여 바운디드 타입의 상위 제한을 완화할 수 있다.

<? extends UpperBound>

이런 제네릭 타입은 UpperBound 클래스 또는 인터페이스의 하위 타입과 매칭될 수 있다.

예를들어 제네릭의 상속관계에서 봤던 것 처럼 List<Number>List<Integer>의 상위 클래스가 아니다. 그렇기 때문에 List<Number>를 파라미터로 가지는 메소드에 List<Integer>를 인자로 호출하면 컴파일 에러가 발생한다.

public class WildCardTest {

    public static double sumOfList(List<Number> list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);

        //컴파일 에러 발생 java.util.List<java.lang.Integer> cannot be converted to java.util.List<java.lang.Number>
        System.out.println("sum = " + sumOfList(li));
    }
}

위와 같은 제한을 완화하기 위해서 Upper Bounded Wildcards를 사용할 수 있다. List<Integer>도 메소드 인자로 사용할 수 있도록 와일드 카드를 추가한다. List<? extends Number>List<Number>보다 덜 제한적이다.

public class WildCardTest {
    //Upper Bounded Wildcards
    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);

        System.out.println("sum = " + sumOfList(li));
    }
}

하지만 우리는 아래 코드로 같은 기능을 하는 메소드를 와일드 카드 없이 만들 수 있다.

public class WildCardTest {

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

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        System.out.println("sum = " + sumOfList(li));
    }
}

두 가지 방법의 차이는...

public static double sumOfList(List<? extends Number> list) : 와일드 카드는 참조가 불가능하기 때문에 메소드에서 참조될 수 없다.

public static <T extends Number> double sumOfList(List<T> list) : T elem; 처럼 메소드내에서 타입을 참조하여 사용할 수 있다.

Unbounded Wildcards

두 가지 시나리오에서 와일드 카드를 별도의 상하한계 없이 사용한다. <?>

  • Object 클래스에서 제공하는 기능만을 사용하여 구현할 수 있는 메서드를 작성하는 경우 .
  • 코드가 타입 매개변수에 의존하지 않는 제네릭 클래스의 메소드를 사용하는 경우.(예를 들어, List.size() 또는 List.clear().)

List<Object>를 파라미터로 받는 아래의 메소드는 리스트의 요소들을 순회하여 보여주려고 하지만 List<Integer> 같은 타입은 List<Object>의 하위 클래스가 아니기 때문에 의도했던 것과 다르게 인자로 List<Object>외의 다른 타입을 받을 수 없다.

public class WildCardTest {

    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String>  ls = Arrays.asList("one", "two", "three");

        //컴파일 오류
        printList(li);
        printList(ls);
    }
}

이럴때 와일드 카드로 Object를 대신한다면 모든 콘크리트 타입 리스트를 인자로 받을 수 있다.

public class WildCardTest {

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

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String>  ls = Arrays.asList("one", "two", "three");
        printList(li);
        printList(ls);
    }
}

그렇다 한들 List<?>List<Object>와 동일하게 동작하지는 않는다. List<Object>에는 어떤 객체이든 담을 수 있지만 List<?>에는 null만 담을 수 있다는 것을 알고있어야 한다.

Lower Bounded Wildcards

하한 와일드 카드는 상한 와일드 카드와 비슷한 방식으로 제네릭 타입을 특정 타입의 상위 클래스로 제한한다.

<? super LowerBound>

참고로 상하한을 동시에 지정할 수는 없다.

List<Integer>List<Number>의 하위 클래스가 아니다. 그렇기 때문에 List<Integer>를 파라미터로 가지는 메소드에 List<Number>를 인자로 호출하면 컴파일 에러가 발생한다.

public class WildCardTest {

    public static void addNumToList(List<Integer> list) {
        for (int i = 1; i <= 10; i ++) {
            list.add (i);
        }
    }

    public static void main(String[] args) {
        List<Number> li = Arrays.asList(1, 2, 3);

        //컴파일 에러 발생 java.util.List<java.lang.Number> cannot be converted to java.util.List<java.lang.Integer>
        addNumToList(li);
    }
}

위와 같은 제한을 완화하기 위해서 Lower Bounded Wildcards를 사용할 수 있다. List<Number>도 메소드 인자로 사용할 수 있도록 와일드 카드를 추가한다. List<? super Integer>List<Integer>보다 덜 제한적이다.

public class WildCardTest {

    public static void addNumToList(List<? super Integer> list) {
        for (int i = 1; i <= 10; i ++) {
            list.add (i);
        }
    }

    public static void main(String[] args) {
        List<Number> li = Arrays.asList(1, 2, 3);

        addNumToList(li);
    }
}

와일드 카드로 제네릭 타입 상속 구현

우리는 이제 List<Integer>List<Number>의 하위 클래스가 아니라는 것을 알고있다.

List<Integer> intList = new ArrayList<>();

//컴파일 오류
List<Number>  numList = intList;

만약 이런 상속관계가 필요하다면 와일드 카드를 이용하여 구현할 수 있다.

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;

와일드 카드를 사용한 제네릭 List 계층구조

diagram showing that List<Integer> is a subtype of both List<? extends Integer> and List<?super Integer>. List<? extends Integer> is a subtype of List<? extends Number> which is a subtype of List<?>. List<Number> is a subtype of List<? super Number> and List>? extends Number>. List<? super Number> is a subtype of List<? super Integer> which is a subtype of List<?>.

참고 사진 및 자료

https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html

와일드 카드 주의사항

와일드 카드를 사용한 제네릭 List 타입은 비공식적으로 read-only로 간주된다. 하지만 아래 작업이 가능하기 때문에 이 말이 완전히 보장되지는 않는다.

  • null 을 추가 할 수 있다 .
  • clear 를 호출 할 수 있다 .
  • iterator를 가져오고 remove를 호출 할 수 있다 .
  • 와일드 카드를 캡처하고 List에서 읽은 요소를 쓸 수 있다.

와일드 카드 캡처

헬퍼 메소드를 이용하여 컴파일러에게 와일드 카드 타입을 유추할 수 있도록 도와주는 방식을 와일드 카드 캡처라고 한다.

컴파일러는 기본적으로 List<?>에 대해 List<Object>로 처리하려고 하며 set() 메소드에 엘리먼트 타입을 컴파일 타임에 확인할 수 없기 때문에 오류가 발생한다.

public class WildCardTest {

    static void foo (List<?> i) {
        //컴파일 오류
        i.set(0, i.get(2));
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1,2,3);
        System.out.println(li);
        foo(li);
        System.out.println(li);
    }
}

헬퍼 메소드를 추가해서 컴파일러가 와일드 카드 타입을 추론할 수 있도록 해준다.

public class WildCardTest {

    static void foo (List<?> i) {
        originalMethodNameHelper(i);
    }

    private static <T> void originalMethodNameHelper(List<T> i) {
        i.set(0, i.get(2));
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1,2,3);
        System.out.println(li);
        foo(li);
        System.out.println(li);
    }
}

Type Erasure

컴파일러는 컴파일 타임에 타입 파라미터를 사용하는 대상의 타입을 컴파일러가 정하는 타입으로 대체하는 Type Erasure를 실행하게 된다. 컴파일된 바이트코드에서는 T 대신 특정 타입으로 대체되어 있다.

Type Erasure의 규칙은 다음과 같다.

  • 제네릭 타입의 타입 파라미터가 상하한이 있는 경우에는 타입 파라미터를 한계 타입으로, 없는 경우 모든 타입 파라미터를 Object로 바꾼다 . 따라서 생성 된 바이트 코드에는 보통의 클래스, 인터페이스 및 메서드 만 포함된다.
  • type-safety를 유지하기 위해 필요한 경우 타입 캐스팅을 사용할 수 있다.
  • 제네릭 타입을 상속받은 클래스에서는 다형성을 유지하기 위해 브리지 메서드를 생성한다.

제네릭 타입 Erasure

Java 컴파일러는 타입 Erasure 프로세스로서 모든 타입 파라미터를 지우고 타입 파라미터가 바인드 된 경우 첫 번째 바인드로 대체하고 타입 파라미터가 바인드 되지 않은 경우 Object 대체한다 .

타입 파라미터가 바인드 되지 않은 상태이므로 Object로 대체된다.

package javageneric;

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public T get() {
        return this.meterial;
    }
    public void set(T meterial) {
        this.meterial = meterial;
    }

}

바이트 코드를 확인해보면 T가 사용되는 부분에 Field meterial:Ljava/lang/Object처럼 Object로 대체됨을 추측할 수 있다.

> javap -c WitchPot 
Warning: File ./WitchPot.class does not contain class WitchPot
Compiled from "WitchPot.java"
public class javageneric.WitchPot<T> {
  public javageneric.WitchPot(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field meterial:Ljava/lang/Object;
       9: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field meterial:Ljava/lang/Object;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field meterial:Ljava/lang/Object;
       5: return
}

이번에는 타입 파라미터에 상한을 두고 결과를 확인해보자.

package javageneric;

public class WitchPot<T extends Meterial> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public T get() {
        return this.meterial;
    }
    public void set(T meterial) {
        this.meterial = meterial;
    }

}

결과는 Object 대신 상한 클래스인 Meterial로 대체된다.

> javap -c WitchPot
Warning: File ./WitchPot.class does not contain class WitchPot
Compiled from "WitchPot.java"
public class javageneric.WitchPot<T extends javageneric.Meterial> {
  public javageneric.WitchPot(T);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field meterial:Ljavageneric/Meterial;
       9: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field meterial:Ljavageneric/Meterial;
       4: areturn

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field meterial:Ljavageneric/Meterial;
       5: return
}

제네릭 메소드도 같은 규칙으로 대체된다.

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    //상한 바운드 Meterial
    public static <U extends Meterial> WitchPot<U> put(U meterial) {
        return new WitchPot<>(meterial);
    }
}
  public static put(Ljavageneric/Meterial;)Ljavageneric/WitchPot;
    // parameter  meterial
   L0
    LINENUMBER 11 L0
    NEW javageneric/WitchPot
    DUP
    ALOAD 0
    INVOKESPECIAL javageneric/WitchPot.<init> (Ljava/lang/Object;)V
    ARETURN
   L1
    LOCALVARIABLE meterial Ljavageneric/Meterial; L0 L1 0 //지역변수가 상한 바운드인 Meterial으로 대체됨
    // signature TU;
    // declaration: meterial extends U
    MAXSTACK = 3
    MAXLOCALS = 1

브릿지 메소드

제네릭 클래스를 상속받거나 제네릭 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일 할 때 컴파일러는 타입 Erasure 프로세스의 일부로 브리지 메서드 라는 합성 메서드를 만들어야 할 수도 있다 . 일반적으로 브리지 메서드에 대해서는 걱정할 필요가 없지만 stack trace에 나타나는 경우 당황 할 수 있기 때문에 알아두는 것이 좋다.

제네릭 클래스 WitchPot과 그를 상속받는 FrogPot

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public void set(T meterial) {
        this.meterial = meterial;
    }
}
public class FrogPot extends WitchPot<Meterial>{

    public FrogPot(Meterial meterial) {
        super(meterial);
    }

    @Override
    public void set(Meterial meterial) {
        super.set(meterial);
    }
}

브릿지 메소드 같은 별도의 과정이 없다면 Erasure 단계를 지나서 예상되는 바이트 코드는 아래와 같다.

public class WitchPot {
    private Object meterial;

    public WitchPot(Object meterial) {
        this.meterial = meterial;
    }

    public void set(Object meterial) {
        this.meterial = meterial;
    }
}
public class FrogPot extends WitchPot{

    public FrogPot(Meterial meterial) {
        super(meterial);
    }

    public void set(Meterial meterial) {
        super.set(meterial);
    }
}

문제는 Erasure 단계 후에 WitchPot에서 상속받은 FrogPot의 set(Meterial meterial) 메소드가 WitchPot의 메소드(set(Object meterial))와 다른 파라미터 타입을 가진다는 것이다. 이렇게 되면 FrogPot의 set(Meterial meterial) 메소드가 WitchPot의 메소드를 오버라이딩 하지 않는다는 것과 같다.

그렇기 때문에 컴파일러는 아래와 같이 컴파일의 Erasure 단계에서 브릿지 메소드를 생성한다. 브릿지 메소드는 Object로 받은 객체를 Meterial로 캐스팅한 뒤에 다시 본래의 set(Meterial meterial) 메소드를 호출하는 로직이 들어있다.

  public void set(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #3                  // class javageneric/Meterial //캐스팅이 발생한다.
       5: invokevirtual #4                  // Method set:(Ljavageneric/Meterial;)V //set 메소드를 호출한다.
       8: return

아마 브릿지 메소드의 바이트 코드 형태는 아래와 같을 것으로 예상된다.

    public void set(Object meterial) {
        set((Meterial) meterial);
    }

궁금해서 이 브릿지 메소드를 직접 만들어 보았는데

public class FrogPot extends WitchPot<Meterial>{

    public FrogPot(Meterial meterial) {
        super(meterial);
    }

    public void set(Object meterial) {
        set((Meterial) meterial);
    }

}

역시나 컴파일 에러 발생...

Error:(13, 17) java: name clash: set(java.lang.Object) in javageneric.FrogPot and set(T) in javageneric.WitchPot have the same erasure, yet neither overrides the other

아마 직접 만들어도 컴파일러가 브릿지 메소드를 생성하려고 하기때문에 충돌이 있는거지 싶다.


제네릭 타입 주의사항

  • 프리미티프 타입을 타입 인자로 사용할 수 없다.

    //이런거 안됨! 컴파일 오류
    Pair < int, char > p = new Pair <> (8, 'a');
  • 타입 매개변수로 인스턴스를 생성할 수 없다.

    public static <E> void test(List <E> list) {
        E elem = new E (); //컴파일 오류
        list.add (elem);
    }
  • 타입 매개변수는 정적 필드로 사용할 수 없다.

    public class Test<T> {
        public static T test; //컴파일 오류
    }
  • 제네릭 타입에 캐스팅 또는 instanceof 사용 불가

        public static <E> void test(List<E> list) {
            if (list instanceof ArrayList<Integer>) {//컴파일 오류
                //...
            }
        }
    //대신 상하한 없는 와일드 카드를 사용하면 가능하다.
    public static void test(List<?> list) {
        if (list instanceof ArrayList<?>) {// OK
            // ...
        }
    }
      public static void main(String[] args) {
          List<Integer> li = new ArrayList<>();
          //List<Number>  ln = (List<Number>) li;  //컴파일 오류

          //이런건 됨!
          ArrayList<Integer> l2 = (ArrayList<Integer>) li;  // OK
      }
  • 제네릭 타입 배열 생성 불가

    List <Integer> [] arrayOfLists = new List<Integer>[2]; //컴파일 오류
  • 제네릭 클래스는 Throwable 클래스를 직접 또는 간접적으로 상속받을 수 없다.

    // Throwable을 간접적으로 상속
    class MathException <T> extends Exception {/ * ... * /} //컴파일 오류
    
    // Throwable을 직접 상속
    class QueueFullException <T> extends Throwable {/ * ... * /} //컴파일 오류
  • 제네릭 메소드의 타입 매개변수의 객체를 catch 할 수 없다.

    public static <T extends Exception, J> void execute(List<J> jobs) {
        try {
            for (J job : jobs)
                // ...
        } catch (T e) {   //컴파일 오류
            // ...
        }
    }

    throws는 가능하다.

    class Parser <T extends Exception> {
        public void parse (File file) throws T {// OK
            // ...
        }
    }
  • 타입 Erasure 단계 후에 동일한 서명을 가지게 되는 메소드 오버로딩 불가능

    public class Example {
        //타입 Erasure 후에는 print(Set)으로 동일하므로 오버로딩 안됨, 컴파일 오류
        public void print(Set<String> strSet) { }
        public void print(Set<Integer> intSet) { }
    }

참고 자료

https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html

Comments