📜 제목으로 보기

기본개념

제네릭(generic)이란?

  • 컴파일타임에 타입체크 하여 코드의 안전성을 높여준다.
  • 우리는 항상 사용하고 있다.

  • 정의시 <T>타입매개변수라 한다.
  • List<String>자리에 뭔가를 주었다면 매개변수화된 타입이라고 부른다.

제네릭 [타입 지정] 사용 이유

컴파일러가 을 알게되어, 런타임 전에 [다른형 생성(add)시] 컴파일 에러가 나 해결됨구체형>

@DisplayName("")
@Test
void lesson9_techTalk_sadly_runtimeError() {
    // 여러타입 가능한 자료구조라 ArrayList내부에 제네릭으로 정의해놨는데
    // 1. 제네릭을 지정 안했줬다면? 
    // 1) 변수추출시, Object로 변<구>제 된다.
    final List<Object> list = new ArrayList<>();
    list.add("woowacourse");
    list.add(1); // 2) 제네릭을 타입 미지정 결과, 아무 형이나 (실수에 의해) 넣을 수 있다.
    //3) 타입이 다른 것이 들어갔는데, 확인안하고 하나의 타입으로 지정해서 써버리면, 형의 차이때문에, runtimeError가 발생한다.
    //4) Object형이기 때문에 casting해서 하나의 형으로 받아줬다 -> 그 과정에서 실수로 들어간 다른형이, 런타입 에러를 발생시킨다?
    final String result = (String) list.get(0) + (String) list.get(1);

    //5) 즉, 여러형이 들어갈수 잇는 자료구조에서, 제네릭을 안쓴다면, 꺼낼때마다 확인해서 써야한다.
}

@DisplayName("")
@Test
void lesson9_techTalk_lovely_compileError() {
    // 여러타입 가능한 자료구조라 ArrayList내부에 제네릭으로 정의해놨는데
    // 2. 제네릭<T>타입매개변수를 지정해서 사용한다면?
    // 1) 컴파일러님께서 알 수 있게 되어, runtime 전에 해결된다.
    final List<String> list = new ArrayList<>();
    list.add("woowacourse");
    // 1) runtimeError까지 가기전에, CompileError를 발생시켜 아무 형이나 집어넣을 수 없게 형 검사를 해준다.
    list.add(1);
}

컴파일러가 아는 것만 받다보니, [꺼낼 때 (타입검사 후 ->) 캐스팅(타입변환)] 안해줘도됨구체형>

@DisplayName("")
@Test
void lesson10_techTalk() {
    final List<Object> list = new ArrayList<>();
    list.add("woowacourse");
    final Object o1 = list.get(0); // 1) 제네릭 타입미지정시, 캐스팅 안해주면 Object
    final String result = (String) list.get(0); // 2) 캐스팅 해줘야 생성했던 형/원하는 형으로 구체형 추출가능
}

@DisplayName("")
@Test
void lesson10_techTalk_2() {
    final List<String> list = new ArrayList<>();
    list.add("woowacourse");
    final String result = list.get(0); // 1) 제네릭 타입지정해줬다면, 컴파일러가 아는 것만 들어가 있어서, 꺼낼때 캐스팅이 필요없다.
}

주의점) 배열과 다르게, 호..제는 변제와 동치이므로, 완전히 동치일때만 가능하다.구>구>

  • 우항의 호출시 <구체형> 제한은 좌항의 변.<구>.제와 완전히 동치이다.
    • 반면, 우항의 호출시<>. 구체형.지정사용은 변수에 들어가는 놈일 뿐이다.
  • 즉, 좌항의 변구제와 우항의 호구제는 동일해야만 한다.
    • 우항의 호구지와는 다른 개념
@DisplayName("")
@Test
void lesson11_techTalk_3() {
    final Object[] arrays = new Integer[1];

    List<Object> list1 = new ArrayList<Integer>(); // 호.<구>.제 는  좌변과 완전히 형이 동치여야한다.
    // 컴팔일러가 알고 있는 변수와 다른 형이기 때문에 컴파일에러가 난다.
    
    
    List<Object> list2 = new ArrayList<>(List.of(1)); // 반면 호<>구.지는 object에 들어가는 것으로서 가능하다.

}

image-20220426144841092

제네릭 타입

  • **형(T)를 파라미터로 가지는 class or interface를 말한다. **
    • class나 인터페이스 이름 뒤에 꺽쇠로 타입매개변수 class<T> or interface<T>가 붙는다.

제네릭 [T] 사용 이유

  • 상태값(필드값) 등을 Object타입을 사용한 것처럼 모든 타입을 받아들여 저장할 수 있게 된다.

    • Object타입으로 상태값을 저장할 때

        private class Category {
            private final Object value;
              
            private Category(final Object value) {
                this.value = value;
            }
              
            public Object getValue() {
                return value;
            }
        }
      
    • 제네릭타입(class)를 쓸 때

        private class GenericCategory<T> {
            private final T value;
              
            public GenericCategory(final T t) {
                this.value = t;
            }
              
            public T getValue() {
                return value;
            }
        }
      
  • Object때와는 다르게, 컴파일러에게 타입을 지정해주고 사용하면

    1. 컴파일러가 알아서 에러를 내준다.

    2. 타입확인 -> 타입변환 안해줘도 된다.

제네릭 메서드

  • 메서드 선언부에 제네릭타입이 선언된 것
    • 타입매개변수의 범위가 메서드내로 한정되어, 제네릭타입의 변수와 전혀 별개의 변수임.

생성자 대신 주입자 메서드(setter) vs 제네릭 메서드

  • setter는 상태값(필드)에 주입하는 것이고

  • 제네릭 메서드는 상태값(필드)와는 별개로 지역변수에서 사용하는 타입매개변수를 사용한다.

  • 제네릭 메서드 정의

      public <K> void printClassName(K k) {
          System.out.println("제네릭 타입의 필드 타입매개변수 = " + this.value.getClass().getName());
          System.out.println("제네릭 메서드 타입매개변수 = " + k.getClass().getName());
      }
    
    • 생성자든, 메서드든, 호<>.구체형.지정사용하기
      //1. 제네릭 타입(class)에 저장하는 T는 Noodle형 객체를 호<>.구.지 -> 변.<구>.제로 자동으로 형 제한
      final GenericCategory<Noodle> noodleGenericCategory = new GenericCategory<>(new Noodle());
      //2. 제네릭 메서드의 타입은, Pasta형을 호<>.구.지정사용
      noodleGenericCategory.printClassName(new Pasta());
    
    • 출력
      제네릭 타입의 필드 타입매개변수 = MainTest$Noodle
      제네릭 메서드 타입매개변수 = MainTest$Pasta
    

제네릭 타입을 모든형이 아닌, 제한된 타입으로 제한

  • Noodle 및 Noodle 자식형들로만 넣고 싶을때? Coke 타입은 못들어오게 막고 싶을 때가 있을 것이다.

    image-20220426154754694

P-E, C-S 원칙 + Extends의 와일드카드를 사용하여 T에는 본인 및 그 자식들만 들오게 -> 나머지는 컴파일에러

  • P-E, C-S 원칙: 생성자/add 등의 Produce상황에서는 <? Extends >의 와일드카드를 사용하여 T에는 본인 및 그 자식들만 들오도록 상한 경계를 준다.

    • 컴파일러들이 상한경계+그자식들만 알고 있어 나머지는 컴파일에러를 낸다.

    image-20220426160310189

  • 와일드카드는 코드속 ?로서, 모든 타입이 다 가능하게 한다

  • 그외에 <? super >의 하한 경계는 Consume을 정의할 때 사용한다.

image-20220426160519444

파라미터 정의시 사용되는 제네릭타입의 타입제한(와일드 카드)

  1. 제네릭타입(class)는 1개 타입 or 상/하한의 타입을 받아 저장하는 자료구조(?)에 사용된다.

  2. 제네릭타입 자료구조의 타입 지정(제한)을 하면서 && 그 제네릭타입의 (생성)기능호출은 더 바깥에서 Helper (Static)클래스의 메서드 파라미터에서 변.<구>.제로서 정하여 사용한다고 가정한다.

    • 참고로.. 호<>.구.지정사용시.. 와일드카드를 사용못하네… 상한 지정해주고 -> 정의부에서 ? extends 상한 으로 바꿔서 파라미터 정의해주기

    image-20220426162024410 image-20220426162222205 image-20220426162250293 image-20220426162312172

해석

01 생성자or메서드 파라미터에 상한의 와일드카드 쓴 제네릭 자료구조 -> (외부에선 하위객체위주로 인자대입)내부 하위class 형 타입을 필드로 가짐 -> 상한class를 setter는 못해줌 But 하위필드를 꺼내 상한(상위class)변수에 할당 및 생성은 가능.
private static class GenericCategoryHelper {
    public static void popNoodle(final GenericCategory<? extends Noodle> extendsNoodleCategory) {
        // 1) Noodle 및 그이하가 꺼내지면 -> 하위class로 생각 -> 우항에 꺼낼 수 있다. -> 경계이면서 상위class인 Noodle에 다 받을 수 있다. (모든 경우)
        final Noodle noodle = extendsNoodleCategory.getValue();

        // 2) 하지만, 내부 하위class 필드를 가진다고 보면 -> 경계이면서 상위class인 Noodle의 값을 우항으로 대입할 수 없다.
        // -> 모든 경우가 만족안되서 컴파일 에러가 난다.
        //            extendsNoodleCategory.setValue(new Noodle());
    }

image-20220426164742844

  1. 제네릭타입은 주로, 해당 자료구조 사용처 메서드에서, 파라미터에서 상하한제한을 와일드카드로 건다
    1. 파라미터쪽으로 외부에서는 최상위 형이하로 들어온다
    2. 이하로 들어온다고 보장받으면 꺼내는 것(getter), 꺼낸 것을 주입하여 생성하는 것은 OK이다.
      • 꺼낸 값을 변수가 부모&추상체에 해당하는 상한을 지정해주면그 이하를 모저리 받을 수 있기 때문이다.
        • 우항: 꺼낸 것 -> Noodle과 Noodle이하
        • 좌항: 변수, 주입되서 생성 -> Noodle로 지정해주면 그 이하 다 받아줌
02 생성자or메서드 파라미터에 하한의 와일드카드 쓴 제네릭 자료구조 -> (외부에선 상위객체위주로 인자대입)내부 상위class필드를 가짐 -> 하한이하의 하위class 대입(저장) 가능 + 꺼내면, 상위class기 때문에 경계의 하위class에 값꺼내 할당은 불가능
public static void pushNoodle(final GenericCategory<? super Noodle> superNoodleCategory) {
    //1) Noodle 및 그 이상이 꺼내지면 -> 그 자료구조(내부 변수) 상위class 필드에 <-  하위class객체(Noodle)를 setter할 수 있다
    superNoodleCategory.setValue(new Noodle());

    //2) 하지만, 상위class 필드를 꺼내 ->  하위타입인 하한(Noodle) 담을 순 없다.
    //final Noodle noodle = superNoodleCategory.getValue();
}

image-20220426170448075

예시1) 제네릭타입 E -> 제네릭메서드에 E(타입매개변수)에 대한 상/하한 제한 with PECS

제네릭메서드에 ? extends E(제네릭타입의 타입매개변수)로 상한제한(내부하위필드or하위값)을 하면, 꺼낸 값(E하위)이 내부필드(제타의 내부필드 E고정)에 대해 안전하여 제타E에, 외부에서 들어올 제네릭메서드 파라미터 값들을 안전하게 값 할당 -> 제타필드를 Produce해주기가 가능

image-20220426173542177

제네릭메서드에 ? super E(제네릭타입의 타입매개변수)로 하한제한(내부상위필드or상위값)을 하면, 상위필드를 가진 자료구조로 생각하고 내부필드(제타의 내부필드 E고정)가 하위값으로 소비하는 것에 안전하여 외부에서 들어올 제네릭메서드 파라미터 값들(X) 상위필드 자료구조 -> 제타E의 하위값들 안전하게 대입 -> 제타필드를 Consumer해주기가 가능

image-20220426173637423

PECS에서 P와 C는 제네릭타입(class, interface)속 필드를 생성(초기화)해주느냐, 갖다쓰느냐의 이야기였다.

제네릭 타입소거

  • 컴파일타임에 타입을 검사
  • 런타임에는 소거
    • 해당 정보를 알 수 없다.
    • Java 5에서 등장했기 때문
  • 4이전 기존코드도 돌아가게 하기 위해 타입소거가 등장함.

image-20220426175809150

오브젝트

오브젝트 속 if instanceof(타입검사)를 줄이는 generic

내 생각

  1. Object가 모든 타입을 받아들였지만, 각 instance검사후 타입캐스팅(타입변환후) 작업하는 것처럼하는것 처럼
  2. 추상체는 여러 구상체를 받을 수 있지만, 각 구상체마다 하는 일이 달라진다면, 전략패턴을 썼었고
  3. 여러 구상체를 받는 외부 추상체를 파라미터로 받아 들어오는 구상체마다 instanceof로 자신 내부의 일이 달라지는 클래스가 있다면
    • 외부 추상체를 T형으로 받고, class옆에는 ? extends T의 상한제한을 걸어주면
    • 외부 추상체로만 알아서 instanceof를 쓰던 타 클래스에서 제네릭 적용을 통해 외부 특정 구상체를 아는 타 클래스가 되도록 한다.
  4. 외부 특정 구상체를 아는 제네릭 타클래스가 만약, 추상클래스 + 템메패턴이라면 -> 자식클래스 또한 제네릭 타클래스<T>의 제네릭을 받는 추상클래스로 선언할 수 있고, new 자식()을 만들어도, 추상클래스로서 {}중괄호로 실시간 다르게 구현해야만 한다.