제네릭

제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법으로 컴파일 시에 더 많은 버그를 탐지할 수 있도록해 코드의 안전성을 더한다.

C++의 템플릿과 처리 과정이나 방법의 차이점이 존재하지만 타입을 제거기술이라는 개념에서는 비슷하다고 볼 수 도 있다.

  • Java 제네릭

    • 컴파일타임에 에러를 검출할 수 있찌만 실제 동작코드는 제네릭을 사용하지 않을때와 동일해 성능도 동일하다.

    • static 변수는 모든 객체가 공유하고, 모든 타입은 Object를 상속해야 하기 때문에 원시형 타입이 사용이 불가능 하다.

  • C++ 템플릿 : 사용하지 않으면 컴파일을 하지 않는다.

    • 컴파일러는 각각 타입에 대해 별도의 템플릿 코드를 생성하기 때문에, static변수를 공유하지 않고, 원시형 타입이 사용가능하다.


장점

  1. 강력한 타입 체크 : 런타임이 아닌 컴파일 타임에 에러를 출력한다.

  2. 편한 캐스팅 : Object로 선언하여 (Integer)와 같이 캐스팅을 하지 않아도 컴파일러에 의해 자동으로 형변환이 이루어진다.

  3. 코드의 재사용성이 높아진다. (코드 중복 최소화)


제네릭 사용법

public class ExampleArrayList {
  private int size;
  private Object[] arr = new Object[5];

  public void add(Object val){
    arr[size++] = val;
  }

  public Object get(int index){
    return arr[index];
  }
}
public class GenericTest {
    ExampleArrayList list;

    @BeforeEach
    public void init(){
        list = new ExampleArrayList();
        list.add("hi");
        list.add("hello");
    }

    @Test
    void ObjectList(){
        Integer val = (Integer) list.get(0);

        System.out.println(val);
    }

}

다음과 같이 Object형 배열을 갖는 ArrayList를 만들어 Integer형 배열로 사용하려고 하는데 String을 넣고 사용한다면 컴파일 타임에는 에러가 발생하지 않는데 런타임시 에러가 발생하게 된다.


누가 Integer배열에 String을 집어넣냐라고 할 수 있지만 내가 작성한 코드가 아니거나, 시간이 지나면 잊어버리기 때문에 잘못 사용될 수 있고 이를 컴파일 타임에 발견하기 위해 사용하게 된다.

애초에 Object로 선언하지 않고 Integer, String으로 선언해서 사용하몀 된다는 말도 나올 수 있는데, 파라미터, 반환 각 클래스별 선언을 해주어야하고 그만큼 코드 중복이 발생하게 된다.

이를 아래와 같이 제네릭을 이용하여 문제를 해결할 수 있다.

public class GenericArrayList<T> {
    private Object[] arr = new Object[5];
    private int size;

    public void add(T val) {
        arr[size++] = val;
    }

    public T get(int idx) {
        return (T) arr[idx];
    }
}
@Test
void GenericListTest(){
  GenericArrayList<Integer> genericArrayList = new GenericArrayList<>();
  genericArrayList.add("hi");
  genericArrayList.add("hello");
}

Integer의 List에 String을 집어넣으려고 한다면 컴파일타임에 에러가 발생한다.

compileError



제네릭 주요 개념

◾ 제네릭에 오는 타입 변수

위의 코드 처럼 T와 같은 변수로 어떤 문자를 사용해도 상관없으나 일반적으로 사용목적에 맞는 특정 문자를 약속으로 사용하고 있다.

  • T : type

  • K : key

  • V : value

  • N : number

  • E : element

  • S : 두번째 파라미터

  • U : 세번째 파라미터

  • V : 네번째 파라미터


◾ 바운디드 타입 (Bounded Type)

타입 변수를 특정 타입에 제한 할 수 있게 한다.

public class GenericArrayList<T extends String>  {
    private Object[] arr = new Object[5];
    private int size;

    public void add(T val) {
        arr[size++] = val;
    }

    public T get(int idx) {
        return (T) arr[idx];
    }
}
public class GenericTest {
    @Test
    void boundedTest(){
        GenericArrayList<Integer> genericArrayList = new GenericArrayList<>();
    }
}

bounded

String으로 제한을 했기 때문에 Integer로 사용하려고 한다면 컴파일타임에 에러를 발생 시킨다.


◾ 와일드 카드

?를 일반적으로 와일드 카드라고 부르며, 제네릭 객체메서드의 매개변수로 받을 때 데이터 타입에 상관없이 사용하고자 할때 사용할 수 있다

종류

Unbounded 와일드카드

타입파라미터를 ?만 사용하는 방법으로 어떤 클래스나 인터페이스 타입이 올 수 있다.

<?>
List<?> list = new ArrayList<>();
==> List<? extends Object> list = new ArrayList<>();

위와 같이 내부적으로 Object가 생략된 형태로 타입에 상관없는 메서드를 정의할 때 사용할 수 있다.

UpperBounded 와일드 카드

<? extends T>

T와 그 자손들을 구현한 객체들만 매개변수로 가능하다.

LowerBounded 와일드 카드

<? super T>

T와 그 조상 객체들로만 매개변수로 가능하다.


◾ 제네릭과 와일드카드

타입 파라미터가 의미있게 사용된다면(지금은 타입을 모르지만 정해지면 목적에 맞게 사용) 제네릭 아니면 와일드카드를 사용 (지금도 타입을 모르고 앞으로도 모를 것이다! 할때 사용)

List의 size, clear등 타입과 상관없는 기능등을 구현하고 add와 같은 타입에 상관있는 기능을 사용하지 않고자 한다면 와일드 카드

@Test
public void example() {
  List<Integer> integerList = Arrays.asList(1, 2, 3);
    wildCardExample(integerList);
    genericExample(integerList);
  }

  static void wildCardExample(List<?> list) {
     list.add(list.get(1));  //컴파일 에러
  }
  static <T> void genericExamplelist(List<T> list) {
     list.add(list.get(1));
  }

wildCardExample의 list.add는 타입에 관심이 없기 때문에 add메서드를 사용 못해 컴파일 에러가 난다.

null은 가능하다.


◾ 캡쳐 에러

caputre Error

와일드 카드를 사용하는 데 컴파일러가 타입추론을 하지못하거나 구체적인 타입이 필요할때 발생한다.

해결 방법

  1. 제네릭으로 변경
  2. 강제적으로 캡쳐하도록 헬퍼클래스 사용
  3. 제너릭 와일드 카드 사용없이 Raw type 사용



제네릭 메소드 만들기

public class GeneralClass {
  public static void <T> genericMethodExample1(T t) {
    System.out.println("hi" + t);
  }
}

static 옆에 <T>를 붙여 만든 메서드로 위와 같이 사용이 가능하고 주로 일반 클래스에서 특정 메서드만 제네릭하게 만들때 사용한다.

제네릭 클래스 안에서 메서드를 만들면 그게 제네릭 메서드가 아니냐라고 할 수 있는데 아래의 코드를 돌려보면 에러를 발생하는 것을 볼 수 있다.

public class GenericClass<T> {
  public static void genericMethodExample2(T t) {
    System.out.println("hi" + t);
  }
}

에러가 발생하는 이유는 static의 특징과 제네릭의 특징때문인데 제네릭타입은 인스턴스가 생성될 때 결정이 되고, static은 인스턴스 생성과 별도로 메모리에 적재된다.

static으로 선언된 메서드가 메모리에 적재될때 제네릭 타입을 매개변수로 받게 되면 타입을 알 수 없기 때문에 에러가 발생하는 것이다. 반면에 제네릭 메서드는 호출되는 시점에 타입이 결정되기 때문에 에러가 발생하지 않는다.



Genric Type Erasure

제네릭은 눈속임과 비슷하고 코드 작성시점 (컴파일 타임)의 편의성을 위한 기능으로 실제 강력한 타입제한을 하지 않는다.

타입을 컴파일 타임에만 검사하고 런타임에는 타입 정보를 지워 알 수 없게 하기 때문이다.

List<T> 가 사실은 List<? extends Object>으로 바뀌게 되고 내부적으로 type casting, Bridge Method등을 제공한다.


실체화와 실체 불가타입

  • 실체화 : 타입정보를 런타임에 완벽하게 사용할 수 있는 타입

    String[], Number[]와 같은 배열

  • 실체 불가 타입 : 런타임에는 타입 정보를 갖지않고 컴파일 타임에 타입 소거가 되는 타입

    List, List와 같은 리스트


런타임에 타입을 추론하는 Array가 아닌 컴파일타임에 타입을 추론하는 List를 제네릭과 함께 사용해야 안전성을 보장 받을 수 있다.





Reference

https://chchang.tistory.com/entry/C%EC%9D%98-template%EA%B3%BC-Java-generic-method-%EC%99%80%EC%9D%98-%EA%B3%B5%ED%86%B5%EC%A0%90%EA%B3%BC-%EC%B0%A8%EC%9D%B4%EC%A0%90

https://vvshinevv.tistory.com/55

https://jinbroing.tistory.com/228

https://jyami.tistory.com/99