[백기선님_자바_스터디] 14주차 - 제네릭

시나브로·2021년 7월 29일
0

자바 라이브 스터디

목록 보기
14/15
post-thumbnail

1. 제네릭


제네릭은 '클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법'이다

제네릭을 사용하는 코드는 제네릭을 사용하지 않은 코드에 비해 많은 이점이 있다.

  • 컴파일 타임에 더 강력한 타입 검사. (type-safed)
  • Java 컴파일러는 강력한 타입 검사를 코드에 적용하고 코드가 type-safety를 위반하면 오류를 발생시킨다. 컴파일 타임 오류를 수정하는 것은 찾기 어려울 수 있는 런타임 오류를 수정하는 것보다 쉽다.
  • 캐스트 제거.

ex)

List list = new ArrayList();
list.add("안녕하세요");
//타입 캐스팅
String s = (String) list.get(0);
List<String> list = new ArrayList<String>();
list.add("안녕하세요");
// 캐스팅 없음
String s = list.get(0);

1.1 타입 변수


  • 아무런 이름이나 지정해도 컴파일하는데 전혀 상관이 없다.
  • 임의의 참조형 타입을 의미한다.
  • 어떠한 문자를 사용해도되지만 네이밍을 지켜주는 것이 좋다.
  • 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다.
  • 타입 변수는 클래스에서뿐만 아니라 메소드의 매개변수나 반환값으로도 사용할 수 있다.

제네릭 타입의 이름 정하기

  • E : 요소 (Element, 자바 컬렉션에서 주로 사용됨)
  • K : 키
  • N : 숫자
  • T : 타입
  • V : 값
  • S,U,V : 두번 째, 세 번째, 네 번째에 선언된 타입

1.2 사용법


public class Box {
    private Object object;

    public void set(Object object) {this.object = object;}
    public Object get() {return object;}
}

메서드가 Object를 받거나 반환하게 되면 primitive 타입이 아니라면 원하는대로 자유롭게 전달할 수 있다. 그러나 컴파일 타임에 클래스가 어떻게 사용되는지 확인할 방법이 없다.

사용하는 곳에서 잘못된 타입 캐스팅이 발생할 가능성이 있어 컴파일 단계에서 에러를 찾을 수 없다.

public class Box<T> {
    // T는 "타입"을 나타냄
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}
}

이 클래스로 생성되는 Box 객체는 특정한 타입을 매개변수로 받아 클래스 내부에서 사용하게 된다.


1.2.1 선언하기


코드 내에서 제네릭 클래스를 참조하려면 T 를 Integer 와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야한다.

GenericClass<Type args> var;

인스턴스 생성 부분에서도 선언처럼 <Type args>를 기입해야 한다.

GenericClass<Type args> var = new GenericClass<Type args>();

1.2.2 다이아몬드


Java SE 7 부터 컴파일러가 선언을 살펴본 후 타입을 추론 할 수 있다면 일반 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 인자 <>로 바꿀 수 있다.

public class WitchPot<T> {
    private T meterial;

    public static void main(String[] args) {
        //선언부에 Integer로 명시되어 있기 때문에 타입 추론을 통해 다이아몬드로도 객체 생성 가능.
        WitchPot<Integer> pot; = new WitchPot<>();
    }
}

1.2.3 raw 타입


raw 타입은 제네릭을 사용하지 않았던 과거 자바 버전과의 호환성을 위해서 존재하는 타입 매개변수가 없는 제네릭 타입이다.

raw 타입에 매개변수화된 제네릭 타입을 할당할 수 있다.

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {
        //raw 타입 생성
        Box rawBox = new Box();

        Box<String> pBox = new Box<>();

        //raw type에 parameterized type 대입
        rawBox = pBox;
    }
}

또는

public class Box<T> {
    private T t;

    public void set(T t) {this.t = t;}
    public T get() {return t;}

    public static void main(String[] args) {

        Box rawBox = new Box();

        //parameterized type에 raw type 대입
        Box<String> pBox = rawBox;
        System.out.println("OK");
        
        rawBox.set(3);
        System.out.println(rawBox.get());
        System.out.println("OK");
    }
}

위 경우에는 컴파일 단계에서 경고메시지를 받는다. 다만 경고일 뿐 컴파일은 진행된다.


1.2.4 제네릭 메소드


제네릭 메소드는 타입 매개변수를 사용하는 메소드이다. 제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메소드에서 타입 매개변수의 scope는 선언 된 메소드로 제한된다.

제네릭 메소드의 구문에는 메소드의 리턴 타입 전에 나타나는 괄호 안에 타입 매개변수 목록이 포함된다. static 제네릭 메소드의 경우 타입 매개변수 섹션이 메소드의 리턴 타입 전에 나타나야한다.

public <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }
public static <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }

public class Util {
    public static <T> WitchPot<T> put(T t) {
        return new WitchPot<>(t);
    }
}
public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        WitchPot<String> pot = Util.<String>put(frog);

        System.out.println(pot.meterial); //개구리

    }

컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파라미터는 생략이 가능하다.

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }
}

Java SE 8부터는 컴파일러의 타입 추론 개념이 확장되어 메소드 인자에 포함된 매개변수화된 타입까지 검사한다.

public class WitchPot<T> {
    private T meterial;

    public WitchPot(T meterial) {
        this.meterial = meterial;
    }

    public static void main(String[] args) {
        String frog = "개구리";
        //반환 대상이 WitchPot<String> 인 것을 확인하고 String 으로 추론한다.
        WitchPot<String> pot = Util.put(frog);

        System.out.println(pot.meterial); //개구리

    }
}



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



제네릭 타입 코드 에서 와일드 카드 라고하는 물음표 ( ? ) 는 알 수 없는 유형을 나타낸다. 와일드 카드는 파라미터 변수, 필드 또는 지역변수의 타입 등 다양한 상황(때때로 리턴 타입에도 사용할 수 있음.)에서 사용할 수 있다. 와일드 카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 수퍼 타입의 타입 인자로는 사용될 수 없다.


public class Exam_008 {
    public static <T> List<Integer> printTokenSizeList(List<T> list) {
        List<Integer> result = new ArrayList<>();
        for(T t : list) {
            result.add(((String)t).split(" ").length);
        }
        return result;
    }
    public static void main(String[] args) {
        List<String> myList = new ArrayList<>();
        myList.add("테스트");
        List<Integer> result = printTokenSizeList(myList);
        for(int elem : result) {
            System.out.println(elem);
        }
    }
}

위와 같은 예제에서 printTokenSizeList 안을 보면 String 타입으로 캐스팅 하는 코드가 있다.

만일 매개변수로 List<Integer>가 전달된다면 런타임 에러가 발생하게 된다


바운디드 타입은 타입 파라미터의 타입을 제한할 수 있다.

바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends키워드, 상위 바운드를 나열한다.

사용하는 방법은 extends 키워드를 사용해서 다음과 같이 작성해주면 된다.

public class Exam_008 {
    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("테스트");
        List<Integer> result = printTokenSizeList(myList);
        for(int elem : result) {
            System.out.println(elem);
        }
    }
}

타입 파라미터의 타입을 String으로 제한한 부분과 T 타입의 변수 t 에 대해 String 타입이라는 것을 알고 있기 때문에 더 이상 타입 캐스팅이 의미가 없어 삭제한 부분이다.

이렇게 타입을 제한하는 방법중에 와일드 카드를 사용하는 방법이 있다.

와일드 카드는 보통 '모든 것' 을 뜻하는데 * (별표, asterisk) 또는 ? (물음표) 를 사용하는데, 자바 제네릭 에서는 ? 를 사용한다.

크게 세가지 형태가 존재한다.

  • <?>

    • 모든 종류의 클래스나 인터페이스 타입 사용 가능
  • <? extends 상위타입>

    • 상위타입 타입 또는 이 타입의 하위타입만 사용 가능
  • <? super 하위타입>
    • 하위타입 타입 또는 이 타입의 상위타입만 사용 하능

ex)

위와 같을 때, 다음과 같이 사용 가능하다.

public class Exam_011 {
    public static void main(String[] args) {
// List 의 요소 타입으로 제한을 두지 않음
        List<?> wildcard_test = Arrays.asList(
                new Root(),
                new Sub_01(),
                new Sub_02(),
                new Sub_02_Sub(),
                new Exam_011()
        );
// List 의 요소 타입으로 Sub_02 또는 Sub_02 하위 타입으로 제한
        List<? extends Sub_02> wildcard_extends_test = Arrays.asList(
                new Sub_02(),
                new Sub_02_Sub()
        );
// List 의 요소 타입으로 Sub_01 또는 Sub_01 상위 타입으로 제한
        List<? super Sub_01> wildcard_super_test = Arrays.asList(
                new Root(),
                new Sub_01()
        );
        wildcard_test.forEach(System.out::println);
        System.out.println();
        wildcard_extends_test.forEach(System.out::println);
        System.out.println();
        wildcard_super_test.forEach(System.out::println);
    }
}
class Root {}
class Sub_01 extends Root {}
class Sub_02 extends Root {}
class Sub_02_Sub extends Sub_02 {}



3. Erasure


제네릭의 타입 소거(Generics Type Erasure)

erasure란 원소 타입을 컴파일 타임에서만 검사를하고 런타임에는 해당 타입 정보를 알기 힘들다. 컴파일 상태에만 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 프로세스이다.

List<Object> list = new ArrayList<Integer>(); //compile error
list.add("thewing"); // type 이 일치하지 않아 add가 안된다

이와 같은 상황에서 컴파일 오류를 확인이 가능하다

Java 컴파일러는 타입 소거를 아래와 같이 적용을 한다.

  • 제네릭 타입(Example) 에서는 해당 타입 파라미터(T) 나 Object로 변경해준다. Object로 변경하는 경우 unbounded 된 경우를 뜻하며, 이는 <E extends Comparable>와 같이 bound를 해주지 않은 경우를 의미한다. 이 소거 규칙에 대한 바이트 코드는 제네릭을 적용할 수 있는 일반 클래스, 인터페이스, 메서드에 적용이 가능하다.
  • 타입 안정성 보존을 위해 필요시 type casting을 넣어준다
  • 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성한다
public static  <E> boolean containsElement(E [] elements, E element){
    for (E e : elements){
        if(e.equals(element)){
            return true;
        }
    }
    return false;
}

실제로 이렇게 선언되어 있는 제네릭 메서드의 경우 선언 방식에 따라 컴파일러가 타입 파라미터 E를 실제 유형의 Object로 변경한다

public static  boolean containsElement(Object [] elements, Object element){
    for (Object e : elements){
        if(e.equals(element)){
            return true;
        }
    }
    return false;
}

따라서 컴파일러는 코드의 형식 안정성을 보장하고 런타임 오류를 방지한다.


3.1 Type Erasure의 유형


클래스 수준에서 컴파일러는 클래스의 Type Parameter를 버리고 첫 번째 바인딩으로 대체하거나 Type Parameter가 바인딩 되지 않은 경우 Object로 변환한다

Stack 구현의 예시를 보자

public class Stack<E> {
    private E[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}

컴파일시 컴파일러는 바인딩되지 않은 형식 매개변수 E를 Object로 바꾸게된다

public class Stack {
    private Object[] stackContent;

    public Stack(int capacity) {
        this.stackContent = (Object[]) new Object[capacity];
    }

    public void pushpublic class BoundStack<E extends Comparable<E>> {
    private E[] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}
    }
}

Type Parameter E가 바인딩 된 경우

public class BoundStack<E extends Comparable<E>> {
    private E[] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (E[]) new Object[capacity];
    }

    public void push(E data) {
        // ..
    }

    public E pop() {
        // ..
    }
}

컴파일러는 바인딩 된 형식 매개 변수 E를 첫 번째 바인딩 된 클래스인 Comparable로 대체한다

public class BoundStack {
    private Comparable [] stackContent;

    public BoundStack(int capacity) {
        this.stackContent = (Comparable[]) new Object[capacity];
    }

    public void push(Comparable data) {
        // ..
    }

    public Comparable pop() {
        // ..
    }
}

3.2 Method Type Erasure


Method Type Erasure의 경우 method-level type erasure 가 저장되지 않고 바인딩되지 않은 경우 부모 형식 Object로 변환되거나 바인딩 될 때 첫 번째 바인딩 된 클래스로 변환된다

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.printf("%s ", element);
    }
}

컴파일시 컴파일러는 Type parameter E를 Object로 바꾼다

public static void printArray(Object[] array) {
    for (Object element : array) {
        System.out.printf("%s ", element);
    }
}








참조


profile
Be More!

0개의 댓글