제네릭이 나오게 된 이유?

제네릭이 도입되기 이전에는 상황에 따라 각기 다른 데이터를 다루기 위해 최상위 클래스인 Object를 사용하여 코드를 작성하곤 하였다.

 

그를 통해서 어떠한 데이터라도 받아 저장할 수 있었으나 몇가지 문제가 있었는데.

  • 저장했던 값을 사용해야 할 때 명시적으로 캐스팅을 해서 사용해야 한다.
  • 잘못된 캐스팅을 통한 오류가 발생할 수 있다. (String → Integer 등..)
  • 들어온 타입에 대한 검증하는 로직이 추가로 들어가야한다. ( instanceof 결국 런타임에서 발견 )
  • 에러의 유무를 컴파일 단계에서 체크할 수 없다. (사전에 방지할 수 없다.)

즉 모호하고 찾기 어려운 잠재적인 오류를 가지게 된다. Reifiable type의 문제점!

 

이러한 문제를 방지하기 위해서는 컴파일 시에 타입 체크를 해서 사전에 실수를 방지하고, 명시적인 캐스팅에 대한 런타임 에러가 해결되어야 했다.

 

그를 위해 제네릭이 등장하게 되었다.

 

 

 

제네릭의 기능?

 

Generic은 해당 기능들을 제공한다.

  • 컴파일러를 통해 타입 체크가 가능하다. 즉 컴파일 과정에서 문제를 제거할 수 있다.
  • 컴파일 시점에서 사용되는 타입을 체크하여 해당 타입에 맞게 컴파일한다.
  • 일반적으로는 캐스팅 비용과 타입 체크의 비용이 들어가지 않는다.
  • 타입의 경계를 지정하여 제한할 수 있다.

 

Generic을 사용하게 발생하는 제약으론

  • Primitive Type을 사용할 수 없다.
  • 컴파일 이후에 타입이 소거되기 때문에 런타임에서 타입 체크를 할 수 없다. Non-Reifiable
    • 해당 문제는 Java 간의 하위 호환성을 위해 VM에서 지우는 방식이다.
    • 타입 소거로 인해 캐스팅 코드가 만들어질 수 있다.

등이 있다.

 

 

 

구체화 타입, 비 구체화 타입, Erasure?

 

Reifiable type?

객체의 Type 정보를 Runtime 시에 확정하여 Application이 끝날 때까지 유지되는 타입이다.

 

제네릭 도입 전, 유사한 사용법을 위해 사용했던 Object 배열 방식은 런타임 시에 타입을 검증한다.

// 컴파일
Object[] array = new Long[10];

// 런타임
Long[] var2 = new Long[10];
var2[0] = Long.valueOf(1L);

 

 

non-Reifiable type? - Erasure

객체의 Type 정보를 컴파일 시점 시에 확인하여, 동일한 동작을 보장한 뒤 Runtime 전에 소거된다.

 

자바의 제네릭은 비 구체적인 타입을 가지며, 컴파일 시에 타입을 검증한다.


// 컴파일
List<String> list = new ArrayList<>();

// 런타임
ArrayList list = new ArrayList();

 

 

Type(class) Erasure

클래스의 파라미터 타입을 제외하고 첫 번째로 바인딩된 타입을 사용하거나 없는 경우 Object 타입으로 설정한다.

 

미 바인딩 시

public class Value<T> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

--------------------
// 컴파일 후

public class Value {
    private Object value;

        public Object getValue(){
        return value;
    }

        public void setValue(Object value){
        this.value = value;
    }
}

 

바인딩 시

public class Value<T extends A> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

--------------------
// 컴파일 후

public class Value {
    private A value;

        public A getValue(){
        return value;
    }

        public void setValue(A value){
        this.value = value;
    }
}

 

 

Method Type Erasure

메서드의 파라미터 타입은 바인딩되지 않은 경우 Object, 바인딩되었을 때에는 해당 클래스로 변환된다.

 

미 바인딩 시

public static <E> void someMethod(List<E> list) {
        System.out.println(list.toString());
}

--------------------
// 컴파일 후

public static void someMethod(List<Object> list) {
        System.out.println(list.toString());
}

 

바인딩 시

public static <E extends String> void someMethod(List<E> list) {
        System.out.println(list.toString());
}

--------------------
// 컴파일 후

public static void someMethod(List<String> list) {
        System.out.println(list.toString());
}

 

 

 

제네릭 사용법

 

제네릭의 변수 네이밍 관례

  • E : Element = Collection
  • K : Key
  • N : Number
  • T : Type Parameter
  • V : Value
  • S, U, V : 두 번째, 세 번째, 네 번째에 선언된 타입

 

제네릭 선언 및 사용 방법

public class Value<T> {
    private T value;

        public T getValue(){
        return value;
    }

        public void setValue(T value){
        this.value = value;
    }
}

---------------------------------

public static void main() {

        Value<String> stringValue = new Value<>();
        stringValue.setValue("hello");

        Value<Long> longValue = new Value<>();
        longValue.setValue(10L);

}

 

 

 

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

 

Type Unbound 란? - Unbounded WildCard

모든 Type을 허용하는 것을 나타낸다.

별도의 상태를 저장하지 않는, 파라미터에 의존하지 않는 메서드를 만들 때 사용한다.

// List<Object> list

List<?> list

 

 

Type bound 란?

특정 타입의 객체를 다른 타입의 객체로 변환할 수 있는 범위를 말한다. - 가변성

  • 공변성 : 받은 Type을 확장(extend)하는 하위 Type 들에 대해서도 허용한다. 서브타입 와일드카드

    여러 하위 클래스들이 하나의 상위 클래스 (분류)에 속한다.

    메서드의 반환 타입에 사용할 수 있으나, 파라미터로는 사용할 수 없다.

     

    리스코프 치환 원칙 ( is a kind of )을 생각하면 좋다.

      // <E extends String> 형식은 지원하는 데이터 유형의 범위를 선언하는 데 사용된다.
    
      List<E extends Object> 
    
      List<E extends Collection>
    
      // 파라미터로 주어진 데이터 유형을 특정 범위로 제한하는 데 사용된다.
      List<? extends Object> 

     

    Multiple type 제약 지정

    해당 경계 안에는 제약 조건을 모두 만족하는 값만을 넣을 수 있다.

      // 해당 인터페이스들을 구현한 객체만을 받을 수 있다.
      List<E extends Serializeble & AutoCloseable & Cloneable>  
  • 무공변성 : 받은 Type 만을 허용한다.

      List<String> list = new ArrayList<>();
    
      String str = "string";
    
      public int add(int a, int b)
  • 반공변성 : 받은 Type이 구현하는 상위 타입만을 허용한다. 슈퍼 타입 와일드카드

    메서드의 파라미터에 사용할 수 있으나, 반환 타입으로는 사용할 수 없다.

      List<? super SomeClass> // 반환하는 값에 대해서 사용이 가능하다.
                                                      // 값을 쓰는 것이 가능하다.

 

 

제네릭 메서드 만들기

메서드의 선언부에 제네릭 타입을 사용한 메서드를 의미한다.

 

제네릭 타입의 선언 위치는 반환 타입 앞에 <>를 감싼 상태로 선언한다.

 

와일드카드의 경우 선언 순서를 변경할 수 있다.

public static <E> T someMethod(List<E> list) {
        return list.get(0);
}

public static <T> void print(T val) {
        System.out.println(val.toString());
}

public static <T, S> void print(T strVal, S longVal) {
    System.out.println(strVal.toString() + longVal.toString());
}

// 참고용?
public static void someExample(List<? extends String> val) {
        System.out.println(val.get(INDEX).toString());
}

public static <E extends String> void someExample2(List<E> val) {
      System.out.println(val.get(INDEX).toString());
}

---------------------------

private static int INDEX = 1;

public static void main(String[] args) {

        List<String> list1 = new ArrayList<>();
        list1.add("string");
        print(someMethod(list1.get(INDEX)));

        List<Long> list2 = new ArrayList<>();
        list2.add(1L);
        print(someMethod(list2.get(INDEX)));

        print(list1.get(INDEX), list2.get(INDEX));

}

+ Recent posts