제네릭

kimView·2021년 2월 24일
0
post-thumbnail

1. 제네릭 기본 이해

자바 1.4 까지는 컴파일 단계에서 컬렉션 등에 어떠한 데이터 타입이 추가 또는 조회 되는지 체크 할 수 없었다. 자바 1.5 에 들어와서 제네릭 이 추가되면서 컴파일 단계에서 데이터 타입에 대한 체크를 할 수 있다.
제네릭 적용시 이점은 다음과 같다.

  • 사용하는 데이터 타입을 명확히 할 수 있으며 코드의 가동성이 높아진다.
  • 컴파일 단계에서 데이터 타입에 대한 체크를 할 수 있다.

자바 1.4 예시

	List stringList = new ArrayList<>();
	stringList.add("string1");
    
	String firstString = (String)stringList.get(0);

자바 1.4 까지는 객체를 조회시에 (String)stringList.get(0); 과 같이 타입변환을 명시해야 했다.

제네릭 적용 예시

	List<String> stringList = new ArrayList<>();
	stringList.add("string1");
    
	String firstString = stringList.get(0);

제네릭과 컬렉션 프레임워크

제네릭의 장점은 컬렉션 프레임워크와 사용할시 추가적인 장점이 있다.

	List<Integer> integerList = new ArrayList<>();
    
        for (int i : integerList) {
            // doSomething ...
        }

java 1.5 에서 추가된 향상된 for 문 역시 제네릭 타입을 기반으로 작성되어 있으며 반복 루프문의 int 타입 역시 제네릭 타입인 Integer 를 기반으로 오토 박싱/언박싱 된다.


2. 제네릭과 클래스/메서드 설계

클래스 및 메소드 설계시 제네릭이 없을 경우 생기는 문제는 다음 코드로 확인 할 수 있다.

public class CustomList {
    private List list = new ArrayList();

    public void add(Object element) {
        list.add(element);
    }

    public Object get(int index){
        return list.get(index);
    }
}
CustomList customList = new CustomList();
customList.add(1);
customList.add(2);
String customListString = (String) customList.get(0);

// 컴파일에선 문제 없지만 실제 실행시 오류 발생!
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

CustomList 의 add 및 get 메서드는 Object 타입을 추가 또는 반환 때문에 Integer, String 모두 컴파일시에는 문제가 없다.
하지만 실제 런타임에서는IntegerString ClassCastException 이 발생하게 된다.

따라서 클래스 선언시 제네릭 타입으로 선언하여 이러한 문제들을 해결 할 수 있다.

public class GenericCustomList<E> {
    private List<E> list = new ArrayList<E>();

    public void add(E element) {
        list.add(element);
    }

    public E get(int index){
        return list.get(index);
    }
}

제네릭 타입 클래스

제네릭 타입 클래스 란 일반적으로 타입 파라미터< 타입 파라미터 >를 가진 클래스 또는 인터페이스를 의미한다.

public class ClassName<T> { ... }
public class ClassName<T> extends SuperClass<T> { ... }
public interface InterfaceName<T> { ... }

제네릭 타입 파라미터 규칙

  • 제네릭 표현식 안의 문자는 자바 변수 선언 규칙과 동일
    - 숫자 시작 불가, 특수문자 시작 불가 등...
  • 일반적으로 대문자 영문 한 글자로 작성
  • E : Elements
  • K : Key
  • V : Value
  • T : Type
  • N : Number
  • R : Return

멀티 타입 파라미터

<> 안에 , 를 구분으로 제네릭 타입을 여러개 선언 할 수 있으며, 이렇게 두 개 이상의 제네릭 타입을 선언하는 것을 멀티 타입 파라미터 라고 한다.

public class GenericCustomMap<K, V> {

    private Map<K, V> map = new HashMap<K, V>();

    public void put(K key, V value) {
        map.put(key, value);
    }

    public V get(K key) {
        return map.get(key);
    }

}

제네릭 메소드

매개 타입과 리턴 타입을 타입 파라미터로 가지는 메소드로서 리턴 타입 앞에 타입 파라미터를 선언하는 방식으로 정의한다.

    public static Map sorting(Map map){
        // ... do something
        return map;
    }
    
    // 제네릭 적용시
    public static <K, V> Map<K, V> sorting(Map<K, V> map){
        // ... do something
        return map;
    }

호출시에는 메서드의 이름 앞에 명시적으로 제네릭 타입을 선언해 주거나 생략하여 컴파일러가 매개 값의 타입을 보고 구체적인 타입을 추정하게 할 수도 있다.

    // 제네릭 타입 명시
    Map<String, String> sortedPorp = GenericUtil.<String, String>sorting(prop);
    // 매개값을 통하여 타입 추정
    Map<String, String> sortedPorp = GenericUtil.sorting(prop);

3. JVM에서 제네릭 처리

제네릭 타입은 람다와 몇 가지 유사한 점이 있다. 그 중 하나는 코딩 및 컴파일 시에만 유효하다는 것이다. 따라서 실제로 코드가 컴파일을 통하여 바이트 코드로 변환될 때 제네릭은 Object 형으로 변환 한다.

    public static void main(String[] args) {

        List<String> stringList = new ArrayList<String>();
        stringList.add("hello");
        stringList.add("world");

        String hello = stringList.get(0);
        String world = stringList.get(1);

    }
    // 디컴파일시 코드
    public static void main(String[] args) {
        List stringList = new ArrayList();
        stringList.add("hello");
        stringList.add("world");
        String hello = (String)stringList.get(0);
        String world = (String)stringList.get(1);
    }

디컴파일 된 코드를 확인 해 보면 변수명을 동일하지만 타입 파라미터가 제거 되어 있다.

  • 제네릭 타입은 컴파일 시에만 고려되며 JVM 이 실행될때는 제네릭 타입이 제거된 기본 클래스형으로 처리된다.
  • 메서드에서 리턴 받을 때는 컴파일러에 의해 형변환 코드가 자동으로 추가된다.

실제로 컴파일 될 때 제네릭 코드를 제거하는 이유는 해석을 위한 추가적인 자원 소모를 없애고 명확하게 동작하기 위해서 이다.


4. 와일드카드와 타입 제한

와일드 카드

💡 와일드 카드란?
컴퓨터 용어에서 주로 패턴을 정의할 때 많이 사용되며 전체의 의미로 쓰이거나 특정한 문자에 따라 조건이 지정된다는 의미.

제네릭에서는 ?와일드카드 로서 사용된다.

제네릭에서 와일드카드가 필요한 이유는 다음을 통해 확인 할 수 있다.

java 1.5 forEach

    void printCollection(Collection<Object> c) {
       for(Object o : c){
           System.out.println(o);
       }
    }
    List<Object> objectList = new ArrayList<Object>();
    List<String> stringList = new ArrayList<String>();
    
    printCollection(objectList);
    printCollection(stringList); // 컴파일 에러 발생!

Object 를 상속한 String 역시 메서드의 파라미터로 지정 할 수 있을거 같지만 컴파일 에러 가 발생한다.
그 이유는 제네릭 타입 파라미터의 목적은 클래스 타입을 명확히 함에 있기 때문에 컴파일러는 상속 관계에 있다고 하더라도 다른 객체로서 인지한다.

    void printCollection(Collection<?> c) {
       for(Object o : c){
           System.out.println(o);
       }
    }

따라서 와일드카드를 사용하여 코드를 수정하면 위와 같이 수정할 수 있다. 와일드카드 는 제네릭에서 모든 클래스를 허용하고 싶을 때 유용하다.

제네릭 타입 제한

위에서 설명한 내용과 같이 제네릭 타입은 상속 관계에 있다고 하더라도 전혀 다른 타입으로 인지한다. 따라서 상속 관계에 있는 제네릭 타입을 선언 하고자 할 때 아래 두가지 방법을 사용한다.

  • extends
  • super

extends를 이용한 제한

    // 클래스의 타입 제한
    public class GenericBoundExample<T extends Vehicle> {
    	private T vehicleType;
    
    	// ...

    }

위와 같이 extends 를 이용하여 특정 클래스 상속 또는 구현한 클래스를 지정할 수 있다.

   // 메서드의 클레스 제한
   void printCollection(Collection<? extends Vehicle> c) {
       for(Vehicle v : c){
           System.out.println(v);
       }
    }

메서드의 경우 와일드카드extends 를 이용하여 타입의 범위를 제한 할 수 있다.

super 를 이용한 제한

super 를 이용한 제한은 자신 과 상위 클래스 또는 인터페이스를 허용하겠다는 뜻이다.

    void printCollection(Collection<? super Truck> c) {
       for(Vehicle v : c){
           System.out.println(v);
       }
    }

위 예시에서 파라미터로 전달 할 수 있는 객체는 Trunk 와 Vehicle이 된다.

따라서 superextends 제한은 각각 다음과 같이 정의 할 수 있다.

  • extends : 상위 한정적 와일드 카드, 최대로 허용하는 상속 계층
  • super : 하위 한정적 와일드 카드, 최소로 허용하는 상속 계층

superextends 중 어느 것을 상용하는지는 다양한 환경에 따라 다르지만 일반적으로 다음과 같은 기준을 권장한다.

  • 입력 파라미터의 경우 extends 와일드 카드를 권장
  • 출력 파라미터의 경우 super 와일드 카드를 권장

5. 제네릭 제약 조건

제네릭의 사용시 추가적으로 고려해야할 조건들은 다음과 같다.

  • 제네릭 타입은 자바 기본형을 사용할 수 없다.
    - 필요할 경우 래퍼 클래스 이용.
  • 제네릭 코딩은 컴파일 시에만 적용
  • 제네릭 타입으로 정의한 배열을 사용 불가능.
    - 배열 대신 컬렉션 프레임워크 사용
  • 제네릭의 타입 변수로 객체를 생성 할 수 없다.
  • 제네릭의 타입 변수는 정적 필드 또는 메서드에서 사용 할 수 없다.
  • 제네릭 클래스를 catch 하거나 throw 할 수 없다.

6. 다이아몬드 연산자

제네릭의 도입이 자바의 안전성에 도움이 되었지만. 오히려 코드를 길고 복잡하게 만드는 단접이 있다. 따라서 이러한 부분을 해결하기 위하여 자바 7 부터 다이아몬드 연산자가 도입 되었다.

Map<String, List<Integer>> newMap = new HashMap<String, List<Integer>>();

위 코드 예시와 같이 제네릭 타입 선언을 두번 해야 함에 따라 코드가 길어지며 선언을 다르게 할 경우 컴파일에서 문제가 발생한다.

다이아몬드 연산자 적용 예시

Map<String, List<Integer>> newMap = new HashMap<>();

객체의 선언 부에만 제네릭 타입을 선언하는 방식으로 사용하면 된다.


💬 정리

서적에 나와있는 내용으로는 와일드카드와 제네릭 타입의 차이점 등 심화적인 내용에 대한 파악이 어려웠다.
추가적인 공부가 필요할 것으로 생각된다.

profile
개인 공부용

0개의 댓글