ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java 항해일지 - 14. 제네릭
    공부일기/자바 스터디 2021. 3. 1. 18:52

    목표

    자바의 제네릭에 대해 학습하세요.

    학습할 것 (필수)

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

    제네릭이란?

    클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법. 제네릭은 중복 코드의 제거를 위해 자바 5부터 나왔다. 객체의 타입을 컴파일 시에 체크해 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

    예를 들어 ArrayList의 경우 다양한 종류의 객체를 담을 수 있긴 하지만 보통 한 종류의 객체를 담는 경우가 많다. 저장할 객체의 타입을 지정해줌으로써 지정한 타입 외에 다른 타입의 객체가 저장되면 에러를 발생시킨다.

    ArrayList<Tv> tvList = new ArrayList<Tv>();
    
    tvList.add(new Tv());
    tvList.add(new Audio()); // 컴파일 에러. Tv외의 다른 타입 저장 불가

     

    - 제네릭의 장점

    1. 타입 안정성을 제공한다.

    2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.

     

    타입변수

    ArrayList클래스의 선언에서 클래스 이름 옆의 <>안에 있는 T를 타입 변수라고 한다. 반드시 T를 사용해야 하는 것은 아니고, 다른 것을 사용해도 된다. 예를들어 ArrayList<E>의 경우 Element의 첫 글자를 따서 타입 변수의 이름으로 사용할 수 있다.

    public class ArrayList<E> extends AbstractList<E> {
        private transient E[] elementData;
        public boolean add(E o) { }
        public E get(int index) { }
    }

     

    타입 변수가 여러 개인 경우 Map<K, V>와 같이 콤마','를 구분자로 나열하면 된다. K는 Key(키)를 의미하고, V는 Value(값)을 의미한다.

    ArrayList와 같은 제네릭 클래스를 생성할 때는 참조변수와 생성자에 타입 변수 E 대신에 Tv와 같은 실제 타입을 지정해주어야 한다.

    아래와 같이 제네릭 클래스가 생성되는 경우 위의 코드가 변하는 모습도 살펴보면 다음과 같다.

    ArrayList<Tv> tvList = new ArrayList<Tv>();
    
    public class ArrayList extends AbstractList<E> {
        private transient Tv[] elementData;
        public boolean add(Tv o) { }
        public Tv get(int index) { }
    }

     

     

    제네릭 타입과 다형성

    제네릭 클래스의 객체를 생성할 때, 참조변수에 지정해준 지네릭 타입과 생성자에 지정해준 지네릭 타입은 일치해야한다. 클래스 Tv와 Product가 서로 상속관계에 있어도 일치해야 한다.

    ArrayList<Tv> list = new ArrayList<Tv>();
    ArrayList<Product> list = new ArrayList<Tv>(); // 에러, 불일치
    
    class Product { }
    class Tv extends Product { }
    class Audio extends Prdouct { }

     

     

    제네릭 타입이 아닌 클래스 타입 간의 다형석을 적용하는 것은 가능한데, 다만 제네릭 타입은 일치해야 한다.

    List<Tv> list = new ArrayList<Tv>();
    List<Tv> list = new LinkedList<TV>();

     

     

    그렇다면 Product의 자손 객체를 저장할 수는 없을까? 답은 가능하다. 제네릭 타입이 Product인 ArrayList를 생성하고, 이 ArrayList에 Product의 자손인 Tv와 Audio의 객체를 저장하면 된다. 그러나 ArrayList에 저장된 객체를 꺼낼 땐 반드시 형변환이 필요하다.

    ArrayList<Product> list = new ArrayList<Product>();
    list.add(new Product());
    list.add(new Tv()); // 가능
    list.add(new Audio()); // 가능
    
    
    Product p = list.get(0);
    Tv t = (Tv) list.get(1); // 형변환 반드시 필요

     

    제한된 제네릭 클래스

    아래와 같은 코드를 보자. 과일 상자에 장난감을 담을 수가 있는데, 이런 문제를 해결하기 위해 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법도 있다. 

    FruitBox<Toy> fruitBox = new FruitBox<Toy>();
    fruitBox.add(new Toy());

     

     

    이 때, 제네릭 타입에 exntends를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

    class FruitBox<T extends Fruit> {
        ArrayList<T> list = new ArrayList<T>();
        
        ...
        
    }

     

     

    여전히 한 종류의 타입만 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다. 클래스가 아닌 인터페이스를 구현해야 한다는 제약의 경우에도 extends를 사용한다. 클래스의 자손이면서 인터페이스도 구현해야 한다면 '&' 기호로 연결한다.

    FruitBox<Apple> appleBox = new FruitBox<Apple>();
    FruitVox<Toy> toyBox = new FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아니다.

     

     

    와일드 카드

    제네릭 클래스를 생성할 때, 참조변수에 지정된 제네릭 타입과 생성자에 지정된 제네릭 타입은 일치해야 한다. 만약 일치하지 않는 경우 컴파일 에러가 발생하는데, Product와 Tv가 서로 상속관계인 경우라도 마찬가지이다.

    그렇다면 제네릭 타입에 다형성을 적용할 방법은 없는 걸까? 제네릭 타입으로 '와일드 카드'를 사용해 가능하다. 와일드 카드 기호는 '?'를 사용하는데, 다음과 같이 extendssuper로 상한과 하한을 제한할 수 있다.

     

    <? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능
    <? super T> 와일드 카드의 하한 제한. T와 그 조상들만 가능

     

     

    와일드 카드를 이용하면 아래처럼 하나의 참조변수로 다른 제네릭 타입이 지정된 객체를 다룰 수 있게 된다.

    ArrayList<? extends Product> list = new ArrayList<Tv>();
    ArrayList<? extends Product> list = new ArrayList<Audio>();

     

     

    제네릭 메서드

    메서드의 선언부에 제네릭 타입이 선언된 메서드제네릭 메서드라고 한다. 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다. 제네릭 메서드는 파라미터와 반환타입으로 제네릭 타입을 사용하는 것이다.

    제네릭 클래스에서는 해당 클래스 내부에서 사용할 타입 파라미터가 무엇인지 알려주기 위해 class를 선언할 때 알려줬다면, 제네릭 메서드는 메서드를 정의할 때, 해당 메서드 내부에서 사용할 타입 파라미터가 무엇인지 알려주기 위해 메서드를 정의할 때 먼저 나열 후 사용해야 한다(리턴 타입을 명시하기 전 작성).

     

    제네릭을 사용해 컴파일 타임에 요소의 타입 검사를 하고, 런타임 오류를 방지하며 타입 캐스팅 없이 보다 범용적인 메서드를 작성할 수 있다.

    import java.util.ArrayList;
    import java.util.List;
     
    public class GenericStudy {
        
        public static <T extends String> List<Integer> printTokenSizeList(List<T> list) {
            List<Integer> result = new ArrayList<>();
            for(T t : list) {
                result.add(t.split(" ").length);
            }
            return result;
        }
        
        public static void main(String[] args) {
            List<String> myList = new ArrayList<>();
            myList.add("동해물과 백두산이 마르고 닳도록");
            myList.add("하느님이 보우하사 우리나라 만세");
            myList.add("무궁화 삼천리 화려 강산");
            myList.add("대한사람 대한으로 길이 보전하세");
            
            List<Integer> result = printTokenSizeList(myList);
        
            for(int elem : result) {
                System.out.println(elem);
            }
        }
    }
    

     

     

    Erasure

    자바는 하위 호환성을 지키기 위해 타입 소거를 만들었다. 제네릭이 나오기 전 버전의 동작을 위해서 타입 소거를 만들었는데, 타입 소거는 제네릭 타입이 특정 타입으로 제한되어 있을 경우 해당 타입에 맞춰 컴파일시 타입 변경이 발생하고, 타입 제한이 없는 경우 Object 타입으로 변경된다.

    제네릭의 타입 파라미터에 primitive 타입을 사용하지 못하는 이유도 primitive 타입은 Object 클래스를 상속받고 있지 않기 때문이다.

    그래서 기본 타입 자료형을 위해서는 Wrapper 클래스를 사용해야 한다.

    Wrapper 클래스를 사용하는 경우 박싱과 언박싱을 명시적으로 사용할 수도 있지만, 암묵적으로도 사용할 수 있으니 구현에 신경쓸 필요는 없다.

     

     

     

    참고.

    자바의 정석 기초편. 남궁성

     

    https://blog.naver.com/hsm622/222251602836

Designed by Tistory.