본문 바로가기
자바/스터디

[자바 스터디] 14주차 과제 - 제네릭(Generic)

by jeonghaemin 2021. 2. 26.
728x90

본 게시글은 백기선 님의 live-study 과제를 수행하면서 작성한 글입니다.

목표

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

학습할 것 (필수)

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

제네릭이란?

JDK1.5에 도입된 제네릭은 다양한 타입의 메서드나 컬렉션 클래스에 컴파일 시에 타입을 체크해주는 기능이다. 객체의 타입을 컴파일 시에 체크해줌으로써 의도치 않은 타입의 객체가 저장되는 것을 막아 타입 안정성을 높이고, 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.

간단히 이야기하면 사용할 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

제네릭의 장점

  1. 타입 안정성을 제공한다.
  2. 타입 체크와 형변환을 생략할 수 있다.

제네릭 사용법

제네릭은 클래스와 메서드에 선언할 수 있다.

제네릭 클래스의 선언

public class Box {
    Object item;

    public Object getItem() {
        return item;
    }

    public void setItem(Object item) {
        this.tem = item;
    }
}

위와 같은 클래스를 제네릭 클래스로 선언하고 싶다면, 클래스 옆에를 붙이고, Object를 T로 치환하면 된다.

public class Box<T> {
    T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

참고로 Box의 T는 타입 변수라고 하는 것인데, T 뿐만 아니라 상황에 맞게 다른 문자를 사용해도 된다.

예를 들어 ArrayList의 E는 Element의 첫 글자를 딴 것이고, Map<K, V>의 K, V는 각각 Key와 Value를 의미한다. 문자의 종류만 다를 뿐 이들 모두 '임의의 참조형 타입'을 의미한다.

이렇게 하여 Box 클래스는 객체를 생성할 때, T 대신에 사용될 특정 타입을 지정하여 사용할 수 있다.

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

위 코드에서 T타입을 String으로 지정하였기 때문에, Box 클래스는 다음과 같이 정의된 것과 같다.

public class Box{
    String item;

    public String getItem() {
        return item;
    }

    public void setItem(String item) {
        this.item = item;
    }

다음과 같은 방법으로도 객체를 생성할 수 있다.

//JDK1.7부터 타입 추론이 가능한 경우 생략 가능.
Box<String> stringBox2 = new Box<>(); 

//타입을 지정해주지 않아서 안전하지 않다는 경고 발생. 
//하지만 제네릭이 도입되기 이전의 코드와 호환을 위해, 타입을 적어주지 않고 객체를 생성하는 것이 허용된다.
Box stringBox3 = new Box();

참고로 제네릭 클래스에서 static 멤버에는 타입 변수를 사용할 수 없다. 제네릭은 각 인스턴스마다 다른 타입을 지정할 수 있도록 하는 것인데, static 멤버는 모든 인스턴스에서 동일하게 동작해야 되기 때문이다.

public class Box<T> {
        //사용 불가
    static T item;
    //사용 불가
    static void method(T item)
}

또한 제네릭 클래스에서는 제네릭 배열 타입의 변수를 선언은 할 수 있지만, 생성은 할 수 없다.

public class Box<T> {

    T[] itemArr1; //선언 가능.

    T[] itemArr2 = new T[5]; //생성은 불가능.
}

그 이유는 new 연산자 때문인데, 아래 바이트 코드에서 볼 수 있듯이 컴파일 시점에는 T에 어떤 타입이 들어올지 모르기 때문이다. 같은 이유로 instanceof 연산자에도 타입 변수를 사용할 수 없다.

Compiled from "Box.java"
public class com.company.Box<T> {
  T Item;

  public com.company.Box();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public T getItem();
    Code:
       0: aload_0
       1: getfield      #2                  // Field Item:Ljava/lang/Object;
       4: areturn

  public void setItem(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field Item:Ljava/lang/Object;
       5: return
}

꼭 제네릭 배열을 생성해야 한다면, new 연산자 대신 Reflection API를 사용해서 생성할 수 있다.
참고: ReflectionAPI에 대한 강의를 찾고있다면 백기선님의 더 자바, 코드를 조작하는 다양한 방법 강의를 추천한다.

public class Box<T> {

    T[] array;

    public Box(Class<T> type, int length) {
        array = (T[])Array.newInstance(type, length);
    }
}

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

바운디드 타입

제네릭 타입에 'extends' 키워드를 사용하여 특정 클래스의 자식 또는 특정 인터페이스를 구현한 타입들만 사용할 수 있도록 제한할 수 있다.

인터페이스를 구현한 타입으로 제한할 때도 implements가 아닌 extends를 사용하는 것을 주의하자.

class Fruit {}
interface Eatable {}

//Fruit의 자식 타입만 사용 가능.
class FruitBox<T extends Fruit> {} 

//Eatable 인터페이스를 구현한 타입만 사용 가능.
class FruitBox<T extends Eatable> {} 

//Fruit의 자식이면서 Eatable 인터페이스를 구현한 타입만 가능
class FruitBox<T extends Fruit & Eatable> {}

와일드 카드

와일드 카드란 제네릭 클래스 객체를 메서드의 매개변수로 받을 때 그 객체의 타입을 제한하는 것을 말한다.

class Fruit { }

class Apple extends Fruit { }

class Grape extends Fruit { }

public class Juice {
    static void makeJuice(List<Fruit> fruits) { }
}

이 경우에 makeJuice메서드는 매개변수로 Fruit 타입의 List밖에 받지 못한다. 즉, List과 같이 다른 타입의 List를 받을 수 없다. 이를 해결하기 위해 나온 것이 와일드 카드이다.

  • <? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능.
  • <? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능.
  • <?> : 제한 없음. 모든 타입이 가능. <? extends Object>와 동일하다.

예를 들어 makeJuice 메서드를 다음과 같이 변경하면 Fruit를 상속받는 Apple, Grape 타입의 리스트도 매개변수로 사용할 수 있게 된다.

static void makeJuice(List<? extends Fruit> fruits) { }

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고한다. 제네릭 타입의 선언 위치는 리턴 타입 바로 앞이다.

앞선 예제의 makeJuice 메서드를 제네릭 메서드로 변경하면 다음과 같다.

static <T extends Fruit> void makeJuice(List<T> fruits) { }

또한 다음과 같이 클래스에 선언되어 있는 타입 매개변수와 메서드에 선언된 타입 매개변수가 같은 문자를 사용하더라도 이 둘은 서로 다른 것이다.

메서드에 사용된 제네릭 타입은 지역 변수와 같이 지역적으로만 사용된다고 생각하면 이해가 쉬울 것이다.

public class Juice<T> {
    static <T> void makeJuice(List<T> fruits) { }
}

제네릭 메서드를 호출할 때는 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 일반적인 메서드를 호출하는 것과 같이 생략이 가능하다.

List<Apple> apples = new ArrayList<>();
makeJuice(apples);

하지만 타입을 생략할 수 없는 경우에는 같은 클래스 내에 있더라도 참조 변수나 클래스 이름을 생략할 수 없다.

/* 타입을 생략할 수 없는 경우 */

<Fruit>makeJuice(fruit); //에러 발생. 클래스 이름 생략 불가
this.<Fruit>makeJuice(fruit); //호출 가능
Juice.<Fruit>makeJuice(fruit); //호출 가능

타입 소거(Type Erasure)

자바에서는 제네릭 클래스를 인스턴스화 할 때 해당 타입을 지워버린다. 즉, 타입 변수는 컴파일 직전까지만 존재하고 컴파일된 바이트코드에는 존재하지 않는다.

ArrayList<Integer> genericList = new ArrayList<>();

ArrayList list = new ArrayList();

위 코드가 컴파일된 후의 바이트 코드는 다음과 같다.

0: new           #2                  // class java/util/ArrayList
3: dup
4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
7: astore_1
8: new           #2                  // class java/util/ArrayList
11: dup
12: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
15: astore_2

제네릭을 사용한 것과 사용하지 않는 ArrayList 모두 같은 바이트 코드가 생성되며, 이들 모두 단지 new ArrayList()로 생성한 것과 동일한 바이트 코드가 생성된다.

참고

댓글