자바 기본 14. 제네릭

장난·2021년 6월 22일
0

자바 기본

목록 보기
14/15
post-thumbnail

14주차 과제: 제네릭


📌 목표

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


📌 학습할 것

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

📜 시작에 앞서

  • 백기선 님의 라이브 스터디(2020년 11월부터 2021년 3월까지) 커리큘럼을 따라 진행한 학습입니다
  • 뒤늦게 알게 되어 스터디 참여는 못했지만 남아있는 스터디 깃허브 주소유튜브 영상을 참고했습니다

📑 제네릭 사용법


용어 정리


제네릭

  • 클래스나 메서드에서 사용할 내부 데이터 타입을 외부에서 지정하는 방법

제네릭 타입

  • 제네릭 클래스 or 제네릭 인터페이스 : 클래스와 인터페이스 선언에 타입 매개변수(type parameter)를 사용한 것
public class ArrayList<E> extends AbstractList<E> {
    private transient E[] elementData;
    public boolean add(E o){...}
    public E get(int index){...}
    ...
}

이외 관련 용어

한글 용어영문 용어
매개변수화 타입parameterized typeList<String>
실제 타입 매개변수actual type parameterString
제네릭 타입generic typeList<E>
정규 타입 매개변수formal type parameterE
비한정적 와일드카드 타입unbounded wildcard typeList<?>
로 타입raw typeList
한정적 타입 매개변수bounded type parameter<E extends Number>
재귀적 타입 한정recursive type bound<T extends Comparable<T>>
한정적 와일드카드 타입bounded wildcard typeList<? extends Number>
제네릭 메서드generic methodstatic <E> List<E> asList(E[] a)
타입 토큰type tokenString.class

출저: 이펙티프자바 3/E


사용 이유


제네릭 사용 전 코드

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

ArrayList squareList = new ArrayList();
squareList.add(new Square());
squareList.add(new Triangle()); //Object로 리스트에 들어간다
Square square = (Square) squareList.get(0); //꺼낼때 형변환 필요
Square square1 = (Square) squareList.get(1); //ClassCastException

제네릭 사용 후 코드

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

ArrayList<Square> squareList = new ArrayList<>();
squareList.add(new Square());
//squareList.add(new Triangle()); //컴파일 에러
Square square = squareList.get(0); // 꺼낼때 형변환 불필요

  • 컴파일 타임에 타입체크를 할 수 있게 해주어, 타입 안정성 제공
  • 타입체크형변환을 제네릭 타입을 통해 컴파일러가 대신 해주기때문에 코드 간결해짐

사용법


제네릭 클래스 객체 생성

ArrayList<Fruit> fruitList = new ArrayList<Fruit>();
ArrayList<Fruit> fruitList1 = new ArrayList<>(); // 생략 간능 부분
//ArrayList<Fruit> fruitList2 = new ArrayList<Apple>(); //컴파일 에러
  • 타입 매개변수에 외부에서 지정할 타입인 매개변수화 타입(parameterized type)을 지정

    • 생성자의 제네릭 타입은 컴파일러가 추정 가능할 경우 생략 가능

    • 참조변수와 생성자에 지정하는 제네릭 타입은 일치해야 한다(어길시 컴파일 에러)

  • 로 타입과 매개변수화 타입<Object> 지정의 차이

    • 매개변수화 타입<Object> 지정은 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달하는 반면, 로 타입은 제네릭 타입 정보가 전부 지워진 것처럼 동작(제네릭 이전 버전과 호환성 위해)
    • 이는 제네릭이 안겨주는 안전성과 표현력을 모두 읽는 것

제네릭 클래스 선언

class MyGenericClass <T1, T2, ..., Tn> {
    ... //T1, T2 ... Tn 사용 가능
}

class MyGenericInterface <T1, T2, ..., Tn> {
    ... //T1, T2 ... Tn 사용 가능
}
  • 클래스명 뒤에 \<타입 매개변수> 작성을 통해 선언
  • 여러 타입 매개변수 사용시 , 로 구분

제네릭의 제한 사항

oracle java tutorial

타입 소거 관련 내용이 많으므로 타입 소거 모른다면 이후 장에서 읽고 보길 추천


  1. 프리미티브 타입으로 제네릭 타입 인스턴스화 불가
Pair<int, char> p = new Pair<>(8, 'a');  // compile-time error
Pair<Integer, Character> p = new Pair<>(8, 'a');
  • 제네릭 타입은 타입 소거로 제네릭 타입의 타입 매개변수가 지정한 한정적 타입 매개변수 혹은 Object로 변경되는데, 프리미티브 타입은 Object 타입으로 형변환할 수 없기 때문에 Wrapper 클래스를 사용해야 한다

  1. 타입 파라미터로 인스턴스 생성 불가
public static <E> void append(List<E> list) {
    E elem = new E();  // compile-time error
    list.add(elem);
}

  1. 타입 매개변수 정적 필드로 사용 불가
public class MobileDevice<T> {
    private static T os; // compile-time error
}
  • 모든 객체에 대해 동일하게 동작하는 static 필드에 코드 작성시 정적이지 않은 제네릭 사용시 혼란

  1. 매개변수화된 타입에 캐스팅과 instanceof 연산자 사용 불가
//타입 소거로 매개변수 타입 정보 알 수 없기 때문에 불가
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // compile-time error
        // ...
    }
}

//비한정적 와일드 카드 타입 사용시 가능(매개변수 타입은 상관 없으므로)
public static void rtti(List<?> list) {
    if (list instanceof ArrayList<?>) {  // OK; instanceof requires a reifiable type
        // ...
    }
}

  1. 매개변수화 타입으로 배열 생성 불가
List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다. 즉, 코드를 new List<E>[], new List<String>[], new E[] 식으로 작성하면 컴파일할 때 제네릭 배열 생성 오류를 일으킨다
제네릭 배열을 만들지 못하게 막은 이유는 무엇일까? 타입 안전하지 않기 때문이다. 이를 허용한다면 컴파일러가 자동으로 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다. 런타임에 ClassCastException이 발생하는 일을 막아주겠다는 제네릭 타입 시스템의 취지에 어긋나는 것이다.

List<String>[] stringList = new List<String>[1]; //허용한다고 가정
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; //타입 소거로 인해 가능
String s = stringList[0].get(0); //타입 안정성 깨짐

출저: 이펙티브 자바 3/E


  1. 제네릭 타입의 예외에 관한 제약
//A generic class cannot extend the Throwable class directly or indirectly.
//For example, the following classes will not compile:

// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // compile-time error

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error

//A method cannot catch an instance of a type parameter:
public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}
    
//You can, however, use a type parameter in a throws clause:
class Parser<T extends Exception> {
    public void parse(File file) throws T {     // OK
        // ...
    }
}

  1. 타입 소거 후 메서드 시그니처가 같아지는 메서드 오버로딩 불가
public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}
  • 위 두 메서드는 타입 소거 후에 메서드 시그니처가 완전히 같으므로 다른 메서드라 확인할 수 없다

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


바운디드 타입

  • 타입 매개변수에 지정할 타입을 제한하고 싶을 떄 사용

class MyGenericClass <T1 extends X> {
    ...
}
  • 타입 매개변수에 extends 키워드 사용해 상위 바운드 설정
    • extends X로 상위 바운드 제한할 경우, 해당 제네릭 객체 생성시 매개변수화 타입으로 X 클래스나 인터페이스의 자손만 가능
    • 여러 extends 조건 부여시 &로 구분
  • 상위 바운드에 해당하는 클래스나 인터페이스의 메서드를 제네릭 클래스에서 사용 가능
    • 타입 매개변수가 <T>일 경우 해당 T의 역할(메서드)에 대해 알 수 없다
    • 타입 매개변수가 <T extends Comparable> 같은 경우 해당 TcompareTo()를 구혔했음을 알기 때문에 compareTo() 같은 메서드 사용 가능

제네릭의 상속과 하위 타입

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList; //매개변수화 타입에 따른 상속관계 성립한다면?

numberList.add(12.34);
Double d = intgerList.get(0); //???
  • 만약 매개변수화 타입의 상속관계에 따라 생성한 제네릭 객체의 상속관계가 성립한다면?
    • 위 코드에서 알 수 있듯이 마지막에 타입 안정성이 깨지며 제네릭 사용 이유가 없어진다
    • 매개변수화 타입의 상속관계가 성립한다고 해서 제네릭 객체간의 상속관계가 성립하지는 않는다
  • 반면 코드의 첫 번째 줄처럼 로 타입 간의 상속관계는 성립 한다



출저: oracle java8 tutorial


와일드 카드

  • 위의 "제네릭의 상속과 하위 타입"에서 알 수 있듯이 제네릭에서 로 타입간 상속관계는 성립하지만 제네릭의 매개변수화 타입에 따른 상속은 성립하지 않는다
  • 이때 제네릭의 매개변수화 타입에 따른 다형성을 활용하기 위해 와일드 카드 사용

명칭형식매개변수화 타입 범위
Unbounded Wildcards<?>제한 없음
Upper Bounded Wildcards<? extends UpperBound>UpperBound와 그 서브클래스만 가능
Lower Bounded Wildcards<? super LowerBound>LowerBound와 그 슈퍼클래스만 가능

//와일드 카드 사용 전
static void printListWithObject(List<Object> list) {
    for (Number e : list) {
        System.out.println(e);
    }
}

//와일드 카드 사용 후
static void printListWithWildcard(List<? extends Number> list) {
    for (Number e : list) {
        System.out.println(e);
    }
}

//
public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    list.add(1);
    //printListWithObject(list); //컴파일 에러
    printListWithWildcard(list);
}
  • 매개변수화 타입의 상속관계가 성립한다고 해서 제네릭 객체간의 상속관계가 성립하지는 않기 때문에 printListWithObject(List<Object> list) 메서드의 파라미터로 List<Integer>를 넣을 수 없다
  • 와일드 카드 사용시 List<? extends Number>Number와 그 서브클래스로 매개변수화 타입을 허용했으므로 List<Integer>을 파라미터로 해당 메서드 사용 가능
List<? extends Integer> intList = new ArrayList<>(); //아래와 같이 자동 형변환
List<? extends Integer> intList = (List<? extends Integer>) new ArrayList<>();
  • 와일드 카드 사용으로 상속관계가 아래 이미지 처럼 변하는데, 따라서 위와 같이 다형성 활용 가능


출저: oracle java8 tutorial


📑 제네릭 메소드

  • 타입 매개변수를 사용하는 메서드

제네릭 메서드 만들기

class GenericClass<T> {
    ...
    public <T> boolean genericMethod(T param){
        ...
    }
}

class MyClass {
    ...
    public <T> boolean genericMethod(T param){
        ...
    }
    static <T> void genericMethod(List<T> list){
        ...
    }
}
  • 메서드의 반환 타입 앞에 타입 매개변수 작성을 통해 제네릭 메서드로 만든다
  • 타입 매개변수의 scope는 해당 메서드 내로 한정
  • 제네릭 클래스 안에 제네릭 메서드가 타입 매개변수를 같은 문자로 사용해도 이 둘은 별개로, 제네릭 메서드는 자신에게 가까운 타입 매개변수를 우선
  • static 메서드도 제네릭 메서드로 만들 수 있다

제네릭 메서드 사용

boolean result = MyClass.<String>genericMethod("string");

boolean result = MyClass.genericMethod("string");
  • 호출시 명시적으로 타입 파라미터를 지정
    • 컴파일러가 추정 가능할 경우 생략 가능

📑 Erasure

  • 컴파일 타임에는 타입을 검사하는 것과 달리 런타임에는 타입 정보를 지워 알 수 없다
    • 컴파일된 파일에는 지네릭 타입에 대한 정보가 없다는 것

제네릭을 구현하기 위해 자바 컴파일러는 다음 유형 삭제를 적용

  • 제네릭 타입의 타입 매개변수를 지정한 한정적 타입 매개변수 혹은 Object로 변경
    • 타입 매개변수를 제한하지 않을 경우(unbounded) Object로 변경
    • 따라서 생성된 바이트코드에는 일반 클래스, 인터페이스, 메서드만 포함
  • 타입 안전성을 유지하기 위해 필요한 경우 형변환 삽입
  • 확장 된 제네릭 타입에서 다형성을 보존하는 브리지 메서드를 생성
  • oracle java tutorial

//Consider the following generic class that represents a node in a singly linked list:

public class Node<T> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

//Because the type parameter T is unbounded, the Java compiler replaces it with Object:

public class Node {

    private Object data;
    private Node next;

    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Object getData() { return data; }
    // ...
}

//In the following example, the generic Node class uses a bounded type parameter:

public class Node<T extends Comparable<T>> {

    private T data;
    private Node<T> next;

    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }

    public T getData() { return data; }
    // ...
}

//The Java compiler replaces the bounded type parameter T with the first bound class, Comparable:

public class Node {

    private Comparable data;
    private Node next;

    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }

    public Comparable getData() { return data; }
    // ...
}

출저: oracle java tutorial


관련 추가 링크


📑📌📜✏️

0개의 댓글