[Java] Generics

제네릭스 Generics

컴파일시 타입 체크(Compile-Time Type Check)해주는 기능.

다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스는 

어떤 자료를 담을 지 알 수 없기 때문에 최상위 객체인 Object 타입으로 저장, 관리한다. 

이 경우 의도치 않은 자료형이 담겨 실행 시에 오류가 발생할 수 있다.

해당 오류는 컴파일 시에 알 수 없는 것으로, 제네릭 타입을 지정하면 컴파일 시 오류를 확인할 수 있게 된다.

인스턴스 별로 다르게 동작할 수 있도록 만들어졌다.

다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다.

제네릭스를 모르면 Java API 문서를 제대로 볼 수 없을 만큼 중요하다.

  • 장점

    타입 안전성 제공

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

선언

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

  • 일반 클래스

Java

1

2

3

4

5

6

7

8

9

10

11

public class Test {
    Object item;

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

    Object  getItem() {
        return item;
    }
}
  • 제네릭 클래스

Java

1

2

3

4

5

6

7

8

9

10

11

public class Test<T> {
    T item;

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

    T getItem() {
        return item;
    }
}

Test<T>의 T를 타입 변수라고 한다.

용어

Java

1

  class Test<T>

     class 제네릭 클래스

     class 원시 타입<T>

     class Test<타입 변수>

  Test<T>  제네릭 클래스. ‘T의 Test’ 또는 ‘T Test’라고 읽는다.
  T  Test<T>의 타입 변수 또는 타입 매개변수. (T는 타입 문자)
  Test  원시 타입 (raw type)

타입 변수 Type Variable

상황에 맞게 의미 있는 문자를 선택해서 사용한다.

기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.

여러 개인 경우 콤마(,)를 구분자로 나열한다.

  • 자주 쓰이는 타입 문자

      T :  Object

      E :  Element

      K :  Key

      V :  Valeu

제한

  • static 멤버

     제네릭스는 객체 별로 다르게 동작하기 위해 만들어졌다.

     때문에 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입 변수를 사용할 수 없다.

     타입 변수는 인스턴스 변수로 간주 된다.

  • 제네릭 타입의 배열

      제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 배열을 생성하는 것은 안 된다.

      new 연산자는 컴파일 시점에 타입 변수가 뭔지 정확히 알아야 하는데

      제네릭 클래스는 컴파일하는 시점에 타입 변수가 어떤 타입이 될 지 전혀 알 수 없기 때문이다.

Java

1

2

3

public class Test<T> {
    T[] arr;
    T[] arr1 = new T[10];    // error

      instanceof 연산자도 같은 이유로 타입 변수를 피연산자로 사용할 수 없다.

  • 제네릭 배열을 생성해야 할 경우

     'Reflection API'의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,

     Object 배열을 생성해서 복사한 후 'T[]'로 형변환하는 방법 등을 사용한다.

와일드 카드 Wildcards

기호 ?

타입 변수는 보통 단 하나의 타입만 지정하지만 와일드 카드를 이용하면 하나 이상의 타입을 지정할 수 있다.

어떠한 타입도 될 수 있다. (타입 변수의 다형성)

  <? extends T>  와일드 카드의 상한 제한(Upper Bound). T와 그 자손들만 가능
  <? super T>  와일드 카드의 하한 제한(Lower Bound). T와 그 조상들만 가능
  <?>  제한 없음. 모든 타입 가능. <? extends Object>와 동일
  • 고안된 이유

     static 메서드에 제네릭스를 적용할 경우, 타입 매개변수는 사용하지 못하므로 특정 타입을 지정해야 한다.

     그렇게 되면 해당 메서드는 특정 타입의 객체만을 사용할 수 있게 되어

     다른 타입의 객체를 매개변수로 오게 하려면 타입 변수만 다른 똑같은 메서드를 만들어야 한다.

     이 때 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문에 메서드 중복 정의가 된다.

     와일드 카드는 이런 상황에 사용하기 위해 고안되었다.

Java

1

2

3

static void method(Box<TypeA> b){}    // Compile error
static void method(Box<TypeB> b){}    // Compile error
static void method(Box<TypeC> b){}    // Compile error

제네릭 타입의 형변환

객체 안에 담을 수 있는 자료형을 지정하기 때문에 해당 자료형에 대한 형변환이 필요 없다.

Java

1

2

3

4

ArrayList<String> list = new ArrayList<>();
list.add("dico");

String str = list.get(0);

ArrayList<String> : ArrayList에 담을 수 있는 자료형은 String 뿐이다.

  • 제네릭 타입과 넌제네릭(Non-Generic) 타입간의 형변환

     가능하지만 경고가 발생한다.

Java

1

2

3

4

5

Test test = null;
Test<Object> objTest = null;

test = (Test) objTest;                  // Generic Type → Primitive Type. Warning
objTest = (Test<Object>) test;          // Primitive Type → Generic Type. Warning
  • 대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능하다.

Java

1

2

3

4

5

Test<String> strTest = null;
Test<Object> objTest = null;

strTest = (Test<String>) objTest;      // error. Test<Object> → Test<String>
objTest = (Test<Object>) strTest;      // error. Test<String> → Test<Object>

제네릭 타입의 제거

컴파일러는 제네릭 타입을 이용해서 소스 파일을 체크하고, 

필요한 곳에 형변환을 넣어준 후 제네릭 타입을 제거한다.

∵   제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해

∴   컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없다. (제네릭 타입은 대부분 Non-Reifiable Types)

  • 과정 1.  제네릭 타입의 경계 제거

Java

1

2

3

4

5

6

7

8

9

// before 
class Box<T extends Fruit> {
    void add(T t){}
}

// after
class Box { 
    void add(Fruit t){}
}
  • 과정 2.  제네릭 타입을 제거한 후에 타입이 일치하지 않으면 형변환을 추가한다.

Java

1

2

3

4

5

6

7

8

9

// before 
T get(int i){
    return list.get(i);
}

// after
Fruit get(int i){
    return (Fruit) list.get(i);
}
  • 과정 3.  와일드 카드가 포함되어 있으면 적절한 타입으로 형변환한다.

Java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

// before 
static Juice makeJuice(FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f : box.getList()) {
        tmp += f + " ";
    }
    return new Juice(tmp);
}

// after
static Juice makeJuice(FruitBox box){
    String tmp = "";
    Iterator it = box.getList().iterator();
    while(it.hasNext()) {
        tmp += (Fruit) it.next() + " ";
    }
    retrun new Juice(tmp);
}
  • 컴파일 후에

     제거되지 않는 타입 == Reifiable Types

     제거되는 타입 == Non-Reifiable Types

    Example

    Java

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    60

    61

    62

    63

    64

    65

    66

    67

    68

    69

    70

    71

    72

    73

    74

    75

    76

    77

    78

    79

    80

    81

    82

    83

    84

    85

    86

    87

    88

    89

    90

    91

    92

    93

    94

    95

    96

    97

    98

    99

    100

    101

    102

    103

    104

    105

    106

    107

    108

    109

    110

    111

    112

    113

    114

    115

    116

    117

    118

    119

    120

    121

    122

    123

    124

    125

    126

    127

    128

    129

    130

    131

    132

    133

    package blog;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    
    public class Main {
        public static void main(String[] args) {
            FruitBox<Fruit> fruitBox = new FruitBox<>();
            FruitBox<Apple> appleBox = new FruitBox<>();
            FruitBox<Grape> grapeBox = new FruitBox<>();
    
            fruitBox.add(new Fruit("fruit", 200)); // Upper Bounded Wildcards
            fruitBox.add(new Apple("apple", 150)); // Upper Bounded Wildcards
            fruitBox.add(new Grape("grape", 300)); // Upper Bounded Wildcards
    
            appleBox.add(new Apple("apple", 100));
            appleBox.add(new Apple("apple", 170));
            appleBox.add(new Apple("apple", 200));
    
            grapeBox.add(new Grape("grape", 300));
            grapeBox.add(new Grape("grape", 400));
            grapeBox.add(new Grape("grape", 200));
    
            System.out.println(Juicer.makeJuice(fruitBox)); // fruit 200 apple 150 grape 300 Juice
            System.out.println(Juicer.makeJuice(appleBox)); // apple 100 apple 170 apple 200 Juice
            System.out.println(Juicer.makeJuice(grapeBox)); // grape 300 grape 400 grape 200 Juice
    
            Collections.sort(fruitBox.getList(), new FruitComp()); // Lower Bounded Wildcards
            Collections.sort(appleBox.getList(), new FruitComp()); // Lower Bounded Wildcards
            Collections.sort(grapeBox.getList(), new FruitComp()); // Lower Bounded Wildcards
    
            System.out.println(fruitBox); // [apple 150, fruit 200, grape 300]
            System.out.println(appleBox); // [apple 100, apple 170, apple 200]
            System.out.println(grapeBox); // [grape 200, grape 300, grape 400]
        }
    }
    
    class Box<T> {
        ArrayList<T> list = new ArrayList<>();
    
        void add(T item) {
            list.add(item);
        }
    
        T get(int i) {
            return list.get(i);
        }
    
        ArrayList<T> getList() {
            return list;
        }
    
        int size() {
            return list.size();
        }
    
        public String toString() {
            return list.toString();
        }
    }
    
    class FruitBox<T extends Fruit> extends Box<T> {
    }
    
    class Fruit {
        String name;
        int weight;
    
        Fruit(String name, int weight) {
            this.name = name;
            this.weight = weight;
        }
    
        public String toString() {
            return name + " " + weight;
        }
    }
    
    class Apple extends Fruit {
        Apple(String name, int weight) {
            super(name, weight);
        }
    }
    
    class Grape extends Fruit {
        Grape(String name, int weight) {
            super(name, weight);
        }
    }
    
    class Juice {
        String name;
    
        Juice(String name) {
            this.name = name + "Juice";
        }
    
        public String toString() {
            return name;
        }
    }
    
    class Juicer {
        /*
         * static Juice makeJuice(FruitBox<? extends Object> box)로 할 경우 box의 요소가 Fruit의
         * 자손이라는 보장이 없다. 하지만 여기서는 제네릭 클래스 FruitBox를 <T extends Fruit>으로 제한했기 때문에 문제 없다.
         */
        static Juice makeJuice(FruitBox<? extends Fruit> box) { // FruitBox<Fruit/Apple/Grape>
            String tmp = "";
            for (Fruit f : box.getList()) {
                tmp += f + " ";
            }
            return new Juice(tmp);
        }
    }
    
    /*
     * public static <T> void sort(List<T> list, Comparator<? super T> c)
     * sort(List<Fruit>, list, Comparator<? super Fruit> c) :
     * Comparator<Object/Fruit> sort(List<Apple>, list, Comparator<? super Apple> c)
     * : Comparator<Object/Fruit/Apple> sort(List<Grape>, list, Comparator<? super
     * Grape> c) : Comparator<Object/Fruit/Grape> → List를 정렬하기 위해 Comparator를 구현할 경우
     * 동일한 조상(Fruit)으로 구현하여 Fruit의 자손이 생길 때마다 구현해야 하는 번거로움을 해소한다.
     */
    class FruitComp implements Comparator<Fruit> {
    
        @Override
        public int compare(Fruit o1, Fruit o2) {
            return o1.weight - o2.weight;
        }
    }
    

    참고 서적: 자바의 정석 3판 2

    민갤

    Back-End Developer

    백엔드 개발자입니다.

    [Java] Class Collections

    Java class Collections에 대한 글입니다.

    Java

    Canvas로 원형 그래프를 그려보자.

    Canvas로 원형 그래프를 그려보는 방법에 대해서 설명합니다.

    Java Script

    v5.0