본문 바로가기

WhiteShip Java Study : 자바 처음부터 멀리까지

제네릭

선장님과 함께하는 자바 스터디입니다.

자바 스터디 Github

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

나의 Github

github.com/cmg1411/whiteShip_live_study

 

cmg1411/whiteShip_live_study

✍ 자바 스터디할래. Contribute to cmg1411/whiteShip_live_study development by creating an account on GitHub.

github.com

 

 

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

 

 

 

 

 

 

제네릭

제네릭은 Java 5 부터 추가된 기능이다. 

제네릭 타입을 사용하면, 잘못된 타입을 사용되었을때 컴파일타임에 잡을 수 있다.

에러는 언제나 런타임보다 컴파일타임에 잡는 것이 좋다.

 

 

 

제네릭 용어

제네릭은 제네릭만의 용어가 많다.

 

 

 

 

제네릭이 왜 나왔는가

  • 제네릭은 타입 변환을 하지 않아도 된다.

위의 말이 무엇일까.

제네릭을 사용하지 않을 때의 코드를 보자.

 

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("white");
        list.add("shio");
        list.add("study");

        for (Object o : list) {
            String str = (String) o;
            System.out.println(str);
        }
    }

 

  • 제네릭이 생긴 이후 List 같은 것을 로(raw) 타입이라 한다.
  • 로 타입은 타입을 지정하지 않고 (Object 하위타입) 을 읽어들이고, Object 형으로 데이터를 가진다.
  • 따라서 String 으로 쓰기 위해서는 명시적인 타입 캐스팅이 필요하다.

명시적인 타입 캐스팅은 귀찮으며, 실수를 유발한다.

또한 로 타입 컬렉션은 Object 타입을 넣을 수 있기 때문에, String 만 들어가리라는 보장이 없다.

예를들어 위의 코드에서 누군가 실수로 아래의 코드처럼 만들 수 있다.

 

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("white");
        list.add("shio");
        list.add("study");
        list.add(1);

        for (Object o : list) {
            String str = (String) o;
            System.out.println(str);
        }
    }

list 에 정수 1을 넣는 구문은 컴파일상 아무 문제가 없다.

 

하지만 아래 for 문에서 값을 빼서 처리하는 곳에서 ClassCastException 이 발생한다.

ClassCastException 는 RuntimeException 의 하위 예외로, UncheckedException 이다.

컴파일타임에 알 수 없고, 런타임에야, 프로그램이 돌아가고 있을때야 예외를 발생하여 문제를 일으킨다.

 

int 를 String 으로 캐스팅하려 했기 때문에 이런 일이 발생한다.

 

 

 

제네릭을 쓰면 ? 

  • 제네릭을 쓰면 위의 문제들이 모두 해결된다.
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("white");
        list.add("shio");
        list.add("study");
//        list.add(1);

        for (String s : list) {
            String str = s;
            System.out.println(str);
        }
    }

 

명시적인 형변환 없이, 바로 String 으로 사용할 수 있다.

또한, 정수 1을 넣으려는시도는 컴파일타임에 오류가 나기 때문에 ClassCastException 의 위험도 없다.

 

 

 

제네릭의 특징

1. 불공변

  • 제네릭과 비교하여 배열은 공변이다.
  • 공변이라 함은, Sub 가 Super 의 하위타입일 때, Sub[] 는 Super[] 의 하위타입인 것이다.
  • 예를 들어, String 은 Object 의 하위타입이기 때문에, String[] 은 Object[] 의 하위타입이다.
  • 따라서 아래와 같은 코드는 컴파일오류를 뱉지 않으며, 런타임에서야 예외를 던지는 문제가 발생한다.
Obect[] ob = new String[5]; // 가능
ob[0] = 1; // ArrayStoreException 을 런타임에 발생

 

  • 그에 반해, 제네릭은 불공변이다.
  • List<String> 은 List<Object> 의 하위타입이 되지 않는다.
  • 따라서 아래와 같은 코드는 컴파일타임에 에러를 내기 때문에 사전에 차단할 수 있다.
List<Object> obList = new ArrayList<String>(); // 컴파일에러 !
obList.add(1);

 

 

 

2. 타입추론

  • 타입추론은 메서드를 호출하는 코드에서 타입인자가 정의한대로 제대로 쓰였는지 살펴보는 컴파일러의 기능이다.

2-1. 제네릭 메서드의 타입 추론

  • 다음과 같은 메서드가 있다. 박스객체의 리스트와 T 타입의 객체 t 를 받아서, T 타입의 박스를 만들어 박스객체 리스트에 추가시킨다.
public Class BoxClass {

  public static <T> void addBox(T t, List<Box<T>> boxes) {
    Box<T> box = new Box<>();
    box.set(t);
    boxes.add(box);
  }
}

 

 

원래 이 메서드를 사용하려면 아래와 같이 써야 한다.

 

 BoxClass.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);

이를 명시적 타입인수라 한다.

하지만 Java8 부터는 타입추론으로 의해 아래와 같이 쓸 수 있게 되었다.

 

 BoxClass.addBox(Integer.valueOf(10), listOfIntegerBoxes);

컴파일러는 메서드를 호출하는 함수를 보고 제네릭 메서드의 타입 매개변수 T 를 추론할 수 있는 것이다.

 

 

 

2-2. 객체의 타입추론

  • 우리는 컬렉션을 생성할 때 아래와같이 빈 다이아몬드 연산자를 쓴다.
List<String> strList = new ArrayList<>();

 

원래는 아래와 같이 써야 하는데 말이다.

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

 

컴파일러는 코드 문맥으로 생성할 타입 인자를 추론할 수 있다.

 

 

 

 

 

 

3. 소거 (Erasure)

  • 제네릭은 타입의 정보가 런타임에는 소거 된다.
  • 원소의 타입을 컴파일타임에만 검사하고 보증함으로써, 런타임에는 타입 정보를 알 수 조차 없게 한다.
  • 이를 실체화가 되지 않는다 라고 한다.
  • 이는 제네릭이 생기기 이전의 레거시코드가 호환될 수 있도록 한 조치이다.
  • 반면에 배열은 타입 정보를 런타임에도 가지고 있으며, 이를 실체화 된다고 한다. 

제네릭이 런타임에 타입 정보가 소거되는지 알아보자.

 

제네릭을 마구 쓴 클래스를 작성해 보았다.

public class Erasure<T> {

    private List<T> list;

    public void setList() {
        list = new ArrayList<>();
    }

    public <R> List<R> get(List<R> item) {
        if (item.equals(list)) return item;
        else return null;
    }

    public static void main(String[] args) {
        Erasure<String> str = new Erasure<>();
        str.setList();
        str.get(new ArrayList<Integer>());
    }
}

 

아래는 위 코드의 바이트코드이다.

바이트코드는 런타임에 읽는 코드이므로 런타임에 타입 정보가 소거된다면 여기에 타입정보가 없어야 할 것이다.

살펴보자.

 

public class com/whiteship/white_ship_study/week14/Erasure {

  private Ljava/util/List; list

  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week14/Erasure; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  public setList()V
   L0
    LINENUMBER 12 L0
    ALOAD 0
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    PUTFIELD com/whiteship/white_ship_study/week14/Erasure.list : Ljava/util/List;
   L1
    LINENUMBER 13 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week14/Erasure; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1

  public get(Ljava/util/List;)Ljava/util/List;
    // parameter  item
   L0
    LINENUMBER 16 L0
    ALOAD 1
    ALOAD 0
    GETFIELD com/whiteship/white_ship_study/week14/Erasure.list : Ljava/util/List;
    INVOKEINTERFACE java/util/List.equals (Ljava/lang/Object;)Z (itf)
    IFEQ L1
    ALOAD 1
    ARETURN
   L1
    LINENUMBER 17 L1
   FRAME SAME
    ACONST_NULL
    ARETURN
   L2
    LOCALVARIABLE this Lcom/whiteship/white_ship_study/week14/Erasure; L0 L2 0
    LOCALVARIABLE item Ljava/util/List; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 21 L0
    NEW com/whiteship/white_ship_study/week14/Erasure
    DUP
    INVOKESPECIAL com/whiteship/white_ship_study/week14/Erasure.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 22 L1
    ALOAD 1
    INVOKEVIRTUAL com/whiteship/white_ship_study/week14/Erasure.setList ()V
   L2
    LINENUMBER 23 L2
    ALOAD 1
    NEW java/util/ArrayList
    DUP
    INVOKESPECIAL java/util/ArrayList.<init> ()V
    INVOKEVIRTUAL com/whiteship/white_ship_study/week14/Erasure.get (Ljava/util/List;)Ljava/util/List;
    POP
   L3
    LINENUMBER 24 L3
    RETURN
   L4
    LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
    LOCALVARIABLE str Lcom/whiteship/white_ship_study/week14/Erasure; L1 L4 1
    MAXSTACK = 3
    MAXLOCALS = 2
}

 

마지막에 main 메서드의 인자로 들어가는 String 타입 말고 찾은 타입이 있는가?

 

Ljava/util/List

모두 이런 식으로 타입 정보는 소거된 것을 볼 수 있다.

 

equals 에서는

Ljava/lang/Object

처럼 모든 타입을 포괄하는 Object 로 바꾼 것을 볼 수 있다.

 

이처럼 런타임에는 제네릭 타입정보는 모두 소거 된다.

 

타입 소거에 대한 예시는 아래와 같다.

github.com/cmg1411/effectiveJava/blob/master/src/main/java/Chapter5/Day29/item29.md

(제가 쓴 글입니다)

 

 

 

 

제네릭의 사용

제네릭은 크게 클래스와 메서드를 만들 때 사용할 수 있다.

차례대로 알아보도록 하자.

 

1. 제네릭 타입

  • 제네릭 타입은 클래스와 인터페이스에서 타입을 매개변수(파라미터)로 가지는 것을 말한다.
public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

클래스명 다음에 <T> 를 (정규) 타입 매개변수라 한다.

의미적으로는 해당 클래스나 인터페이스 내에서는 T 타입을 사용하겠다는 것이다.

 

아래 코드를 보자.

public class Box<T> {

    private T in;

    public void push(T element) {
        in = element;
    }

    public T pop() {
        return in;
    }
}

 

  • Box 클래스의 타입 매개변수로 <E> 를 사용한 것을 볼 수 있다.
  • push() 에서는 E 타입을 매개변수로 받고 있다.
  • pop() 에서는 E 타입을 리턴타입으로 한다.

 

E 는 어떤 타입도 올 수 있으며, Stack 객체를 만들 떄 결정된다.

만약 Box<String> 으로 객체를 생성한다면, 위 클래스에서 E 는 모두 String 이 되어 동작하게 된다.

 

    public static void main(String[] args) {
        Box<String> s = new Box<>();

        s.push("hi");
        System.out.println(s.pop());
    }
  • s 객체는 Box<String> 으로 지정했기 떄문에, 모든 E 가 String 인 것 처럼 동작한다. (아래 코드처럼)
public class Box<String> {

    private String in;

    public void push(String element) {
        in = element;
    }

    public String pop() {
        return in;
    }
}
  • 따라서, push() 함수에서 정수 1 을 넣으려는 시도는 컴파일 오류를 뱉는다.

 

 

정규 타입 매개변수 알파벳은 아무 알파벳을 사용해도 오류가 나지 않는다.
하지만 관용적으로 사용하는 알파벳들이 있다.

T : 아래의 경우가 아닌 대부분 T를 사용한다.
E : Element 의 E 이다. 리스트 같은 컬렉션의 요소에 사용한다.
K : key 의 K 이다. Map 의 key 에 사용한다.
V : value 의 V 이다. Map 의 value 에 사용한다.

 

 

 

멀티 타입 파라미터

  • 타입 파라미터가 두개 이상 필요한 경우가 있다.
  • 그럴 땐, 타입 파라미터를 <> 안에 쉼표로 필요한 만큼 선언하면 된다.
public class Student<K, V> {
    private K studentNumber;
    private V name;

    public MultiTypeParameter(K studentNumber, V name) {
        this.studentNumber = studentNumber;
        this.name = name;
    }
    
    private K getStudentNumber() {
        return studentNumber;
    }
    
    public V getName() {
        return name;
    }
}

 

학번을 K 로 받고, 이름을 V 로 받았다.

이 또한 객체를 생성할때 결정된다.

 

    public static void main(String[] args) {
        Student<Integer, String> student = new Student<>(12, "mingeor");
        System.out.println(student.getName());
    }

 

 

 

2. 제네릭 메서드

  • 제네릭 메서드는 매개 타입과 리턴 타입으로 타입 파라미터를 갖는 메서드이다.
  • 리턴 타입 앞에 <> 를 추가하여 타입 파라미터를 적는다.
  • 매개변수의 타입과 리턴타입, 메서드 안에서 타입 파라미터를 사용한다.
<> 는 비공식적으로 다이아몬드 연산자라고 하기도 한다.
다이아몬드 연산자 안에는 primitive 타입을 쓸 수 없다. int 는 Integer 로 처럼 Wrapper 클래스를 사용해야 한다.

 

제네릭 메서드의 형식은 아래와 같다.

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

 

 

 

제네릭 메서드를 어떻게 사용하는지 알아보자.

아래의 compare() 메서드는 Key 값과 Value 값이 모두 같은지를 검사하는 메서드이다.

public class GenericMethod {
    //제네릭 클래스 (inner)
    static class Pair<K, V> {
        private final K key;
        private final 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 static <K, V> boolean compare(Pair<K, V> m1, Pair<K, V> m2) {
        boolean keyCompare = m1.getKey().equals(m2.getKey());
        boolean valueCompare = m1.getValue().equals(m2.getValue());
        return keyCompare && valueCompare;
    }

    public static void main(String[] args) {
        Pair<String, Integer> p1 = new Pair<>("한국", 1);
        Pair<String, Integer> p2 = new Pair<>("한국", 2);

        System.out.println(compare(p1, p2));
    }
}

 

  • compare() 는 K, V 라는 두 타입 매개변수를 이용하므로 반환타입 앞에 선언되었다.
  • 따라서 매개변수에서 K, V 를 사용할 수 있다.
  • main 메서드에서 p1, p2 를 사용하고 compare(p1, p2) 를 호출하면 compare() 는 다음과 같은 코드처럼 동작하게 된다.
    public static <String, Integer> boolean compare(Pair<String, Integer> m1, Pair<String, Integer> m2) {
        boolean keyCompare = m1.getKey().equals(m2.getKey());
        boolean valueCompare = m1.getValue().equals(m2.getValue());
        return keyCompare && valueCompare;
    }

 

 

 

 

 

제네릭 주요 개념

제네릭에는 위의 타입 파라미터를 응용하여 여러가지 개념이 생긴다.

또한 와일드카드라는 특이한 놈도 있는데, 알아보도록 하자.

 

 

1. 제한된 타입 파라미터 (바운디드 제네릭)

  • 타입 파라미터는 아무거나 들어올 수 있다.
  • 하지만 '~중에 아무거나' 를 구현하려면 어떻게 해야할까.
  • 이때 사용할 수 있는 것이 제한된 타입 파라미터이다.
  • 제한된 타입 파라미터에는 extends 키워드를 사용할 수 있다.

extends

// 제네릭 메서드
    public static <T extends Serializable> write(T object) {
    
        File file = new File("test.txt");
        
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) {
            outputStream.writeObject(object);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

  • ObjectOutputStream 의 메서드 writeObject() 는 직렬화할 수 있는 객체만 매개변수로 넣을 수 있다.
  • 객체가 Serializable 을 구현하고 있지 않으면, NotSerializableException 을 뱉는다.
  • 물론, try-catch 예외처리를 통해 해결할 수 있지만,  위와같이 쓰면 애초에 write 함수에서 Serializable 의 하위 타입만 받아서 메서드를 실행시키도록 할 수 있는 것이다.

그러면 위처럼, Serializable 을 구현하지 않은 클래스로만든 객체는 컴파일조차 되지않는 오류를 뱉는다.

 

 

결국

 

<T extends Cla> 는 Cla 타입을 포함안 하위타입 아무거나 라는 뜻을 가진다.

 

 

 

2. 와일드카드 

와일드카드는 제네릭이 불공변이기 때문에 사용한다는 관점도 있다.
매개변수로 객체를 받는데 아무 타입의 객체를 받고 싶다면 모든 객체의 부모인 Object 를 사용하면 된다.
하지만 매개변수로 리스트를 받는데 아무 타입의 리스트를 받고 싶을 때에는 List<Object> 는 모든 List<> 의 부모타입이 아니다.
제네릭은 불공변이기 때문에.
따라서 List<?> 라는 와일드카드가 나왔다.

 

  • 와일드카드는 다이아몬드 연산자 <> 안에 ? 를 쓴 <?> 로 표현된다.
  • 와일드카드는 의미적으로, 어떤 용도로도 쓰일 수 있는 비장의 카드라는 뜻이다.
  • 여기서부터 제네릭이 헷갈리기 시작한다. 어떤 용도로 쓸 수 있는 것이 아무것이나 쓸 수 있는 것 아닌가 ? 그럼 타입 매개변수랑 다른게 뭔가 ?
  • 맞다. 비슷하다. 하지만 면밀히 보면 다르다. 제일 확연한 차이는 사용되는 위치이다.
    • 타입 매개변수는 제네릭 타입에서는 클래스 이름 옆에 <T>, 메서드의 반환타입 앞의 <T> 를 붙였었다. 이는 해당 클래스에서, 해당 메서드에서는 T 타입을 사용할 것이라는 뜻이었다. 해당 클래스와 메서드 전체에 적용되는 타입 매개변수를 선언해준 격이다.
    • 하지만 와일드카드는 다르다. 아래 메서드를 보자.
    public static void printList(List<?> list) {
        list.stream().forEach(System.out::println);
    }

매개변수의 List<?> list 는 아무 타입 매개변수 리스트를 인자로 받을 수 있음을 뜻한다.

위처럼 와일드카드는 매개변수의 타입을 지정할 때 주로 쓰인다.

List<?> list 의 타입은 인자를 넘기는 타이밍에 정해지지만, 그 타입이 list 라는 리스트의 타입 외에 메서드에서 영향을 미치는 곳은 존재하지 않는다.

 

다시 타입 매개변수로 넘어가서, 위의 메서드를 타입 매개변수로 적어보자.

 

    public static <T> void printList(List<T> list) {
        list.stream().forEach(System.out::println);
    }

이렇게 쓰면 와일드카드를 사용했을 때와 기능은 같다.

하지만 이 타입 매개변수는 아래와 같은 기능도 할 수 있다.

 

    public static <T> T printList(List<T> list) {
        int length = list.size();
        int count = 0;
        
        for (T o : list) {
            count++;
            System.out.println(o);
            if (count == length) {
                return o;
            }
        }
        return null;
    }

<T> 타입은 반환값에도, 매개변수 타입에도, 메서드 코드 중에서도 사용할 수 있다.

이는 분명히 와일드카드는 못하는 것이고 다른점이라 할 수 있다.

 

 

 

또 중요한 차이로는, 와일드카드는 <> 안에 있어야 한다.

타입 매개변수는 

public <T> void go(T param) {...}

처럼 단독으로 쓰일 수 있지만,

public void go(? param) {...}

이런건 없다.

 

 

 

 

 

2-0. 와일드카드 타입 : ?

  • 위에서 설명한 <?> 이다.
  • 와일드카드 타입은 <?>, 한정적 와일드카드 타입인 <? extends E>, <? super E> 의 세가지 이용법이 있다.

 

2-1. 한정적 와일드카드 타입 : extends

  • 한정적 타입 매개변수와 비슷하다.
  • ~중 아무거나 를 표현하고 싶을 때 쓸 수 있다.
public class Chooser<T> {
    private final List<T> choiceList;

    public Chooser(Collection<T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }
    
    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    }
}

 

이 클래스의 필드 choiceList 에는 Chooser 클래스를 만든 타입의 컬렉션을 넣을 수 있다.

 

        List<Number> numberList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        Chooser<Number> chooser1 = new Chooser<>(numberList); // 가능 
        Chooser<Number> chooser2 = new Chooser<>(integerList); // 불가능

 

이번에도 제네릭의 불공변의 특성 때문에, Integer 는 Number 의 하위 타입이지만

Collection<Integer> 은 Collection<Number> 의 하위 타입이 아니기 때문에 Integer 타입 리스트는 생성자 인수로 넣을 수 없다.

 

이때 한정적 와일드카드 타입을 사용할 수 있다.

 

    public Chooser(Collection<? extends T> choices) {
        this.choiceList = new ArrayList<>(choices);
    }

 

<? extends T> 는 T 타입을 포함한 T 의 하위타입 아무거나 와 같은 뜻이다.

이렇게 바꾸면 불가능하던 Integer 타입 리스트도 생성자의 인수로 넣을 수 있다.

 

 

 

2-2. 한정적 와일드카드 타입 : super

public class Box<E> {

    private final List<E> sockets;

    public Box() {
        this.sockets = new ArrayList<>();
    }

    public void putItems(E object) {
        sockets.add(object);
    }

    public List<? super E> getNewItemList(List<? super E> list) {
        list.addAll(sockets);
        return list;
    }




    public static void main(String[] args) {
        Box<String> strBox = new Box<>();
        strBox.putItems("hi");
        strBox.putItems("bye");

        List<Object> objectList = new ArrayList<>();

        System.out.println(strBox.getNewItemList(objectList));
    }
}

 

이번 클래스를 보자.

Box 는 제네릭 타입이다. E 라는 타입 매개변수를 가지는 리스트 socket 를 필드로 가지고 있다.

 

socket 에 요소를 집어넣는 putItem 이라는 메서드를 가지고 있다.

 

그리고 getNewItemList() 는 새로운 리스트를 줘서 socket 의 내용을 거기다가 복사해 담아서 반환하는 메서드이다.

 

여기서 생각해보자.

옮겨담을 리스트는 E의 상위타입이면 옮겨 담을 수 있을 것이다.

코드의 예와 같이, List<String> 의 내용물인 String 타입은, List<Object> 의 내용물인 Object 타입에 옮겨담을 수 있다.

 

따라서 옮겨담을 리스트의 타입 매개변수는 sockect 의 타입 매개변수의 상위타입이면 옮겨담을 수 있는 것이다.

 

<? super E> 는 이 의미를 완전히 내포한다. E 의 상위타입 아무거나 라는 뜻이다.

 

 

 

 

PECS 공식이라는 것이 있다.
Producer-extends, Consumer-super 의 약자로, 생산자는 extends, 소비자는 super 을 쓰라는 것이다.

위의 예에서 Chooser 클래스의 생성자에서 Collection<? extends E> 매개변수는 Chooser 클래스가 무언가를 생산하게 만드는 생산자이다. 따라서 extends 를 썼다.

List<? super E> list 는 Box 클래스가 Box 클래스의 자원을 소비하게 만드는 소비자이다. 따라서 super 를 썼다.

 

 

 

 

 

 

 

 

이펙티브 자바 3E

이것이 자바다 - 신용권

타입추론 durtchrt.github.io/blog/java/generics/9/

'WhiteShip Java Study : 자바 처음부터 멀리까지' 카테고리의 다른 글

람다식(feat. 익명 구현 클래스 vs 람다식)  (0) 2021.03.07
NIO  (0) 2021.02.19
I/O  (0) 2021.02.17
ServiceLoader  (0) 2021.02.14
Annotation  (0) 2021.02.13