본문 바로가기

프로그래밍 공부/Java

Java - 제네릭(Generic)

제네릭(Generic)

Java에서 제네릭은 데이터의 타입을 일반화한다는 것을 말한다. 제네릭은 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다. 이렇게 컴파일 시에 미리 타입 검사를 수행하면 두 가지 장점이 있다.

1. 클래스나 메서드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 시간을 줄일 수 있다.

JDK 1.5 이전에서 여러 타입을 사용하는 대부분의 클래스나 메서드에서 인수나 반환값으로 Object 타입을 사용했다. 하지만 이 경우 반환된 Object 객체를 다시 원하는 타입으로 타입 변환을 해야하며, 이 때 오류가 발생할 수 있다. 하지만 JDK 1.5부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지게 된다. 따라서 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있게 된다.

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
// 제네릭(Generic)의 선언 및 생성
 
class MyArray<T> {
    T element;
    void setElement(T element){
        T getElement() {
            return element;
        }
    }
}
 
// MyArray 클래스에 사용된 타입 변수로 Integer 타입을 사용하는 코드
MyArray<Integer> myArr = new MyArray<Integer>();
 
// Java SE 7부터 사용 가능한 코드
MyArray<Integer> myArr = new MyArray<>();
 
import java.util.*;
 
class LandAnimal {
    public void crying(){
        System.out.println("육지동물");
    }
}
 
class Cat extends LandAnimal {
    public void crying(){
        System.out.println("야옹 야옹");
    }
}
class Dog extends LandAnimal {
    public void crying(){
        System.out.println("멍 멍");
    }
}
class Sparrow {
    public void crying(){
        System.out.println("짹 짹");
    }
}
 
class AnimalList<T>{
    ArrayList<T> al = new ArrayList<>();
 
    void add(T animal) {
        al.add(animal);
    }
    T get(int index){
        return al.get(index);
    }
    boolean remove(T animal){
        return al.remove(animal);
    }
    int size(){
        return al.size();
    }
}
 
public class Main{
    public static void main(String[] args){
        AnimalList<LandAnimal> landAnimal = new AnimalList<>();    // Java SE 7부터 생략 가능
 
        landAnimal.add(new LandAnimal());
        landAnimal.add(new Cat());
        landAnimal.add(new Dog());
        // landAnimal.add(new Sparrow());    // 오류가 발생함
 
        for(int i = 0; i < landAnimal.size(); i++){
            landAnimal.get(i).crying();
        }
    }
}
cs

위의 코드에서 제네릭의 선언 및 생성(1행)에 사용된 T를 타입 변수라고 한며, 임의의 참조형 타입을 의미한다. 꼭 T 뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 ,로 구분해 명시할 수 있다. 타입 변수는 클래스에서뿐만 아니라 메서드의 매개변수나 반환값으로도 사용할 수 있다.

제네릭 클래스를 생성할 때에는 타입 변수 자리에 사용할 실체 타입을 명시해야 한다.

12행에 있는 MyArray 클래스에 사용된 타입 변수로 Integer 타입을 사용하는 코드를 보면 제네릭 클래스를 생성할 때 사용할 실제 타입을 명시하면, 내부적으로는 정의된 타입 변수가 명시된 실제 타입으로 변환되어 처리된다.

자바에서 타입 변수 자리에 사용할 실테 타입을 명시할 때 기본 타입을 바로 사용할 수는 없다. 이때는 예시처럼 Integer와 같이 래퍼(Wrapper) 클래스를 사용해야한 한다.

또한 15행에 있는 예시 처럼 Java SE 7부터 인스턴스 생성 시 타입을 추정할 수 있는 경우에는 타입을 생략할 수 있다.

18행부터 시작하는 예시 코드의 결과는 육지동물, 야옹 야옹, 멍 멍이 차례대로 출력이 된다. 코드에 Cat과 Dog 클래스는 LandAnimal 클래스를 상속받는 자식 클래스이므로 AnimalList<LandAnimal>에 추가할 수 있다. 하지만 Sparrow 클래스는 타입이 다르기 때문에 추가할 수 없다.

Java 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사되어 타입 변환이 되고, 코드 내의 모든 제네릭 타입은 제거되어 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 된다. 이런 식으로 동작하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서 이다.

다양한 제네릭 표현

타입 변수의 제한

제네릭은 T와 같은 타입 변수를 사용해 타입을 제한한다. 이때 extends 키워드를 사용하면 타입 변수에 특정 타입만을 사용하도록 제한할 수 있다.

1
2
// 타입 변수의 제한
class AnimalList<extends LandAnimal> { ... }
cs

위와 같이 클래스의 타입 변수에 제한을 걸어 두면 클래스 내부에서 사용된 모든 타입 변수에 제한이 걸린다. 이때에는 클래스가 아닌 인터페이스를 구현할 경우에도 implements 키워드가 아닌 extends 키워드를 사용해야만 한다.

1
2
3
interface WarmBlood { ... }
...
class AnimalList<extends WarmBlood> { ... } // implements 키워드를 사용하면 안된다.
cs

또 클래스와 인터페이스를 동시에 상속받고 구현하고 싶다면 & 기호를 사용한다.

1
class AnimalList<extends LandAnimal & WarmBlood> { ... }
cs

아래는 예시 코드이다.

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
import java.util.*;
 
class LandAnimal {
    public void crying(){
        System.out.println("육지동물");
    }
}
 
class Cat extends LandAnimal {
    public void crying(){
        System.out.println("야옹 야옹");
    }
}
class Dog extends LandAnimal {
    public void crying(){
        System.out.println("멍 멍");
    }
}
class Sparrow {
    public void crying(){
        System.out.println("짹 짹");
    }
}
 
class AnimalList<extends LandAnimal>{
    ArrayList<T> al = new ArrayList<>();
 
    void add(T animal) {
        al.add(animal);
    }
    T get(int index){
        return al.get(index);
    }
    boolean remove(T animal){
        return al.remove(animal);
    }
    int size(){
        return al.size();
    }
}
 
public class Main{
    public static void main(String[] args){
        AnimalList<LandAnimal> landAnimal = new AnimalList<>();    // Java SE 7부터 생략 가능
 
        landAnimal.add(new LandAnimal());
        landAnimal.add(new Cat());
        landAnimal.add(new Dog());
        // landAnimal.add(new Sparrow());    // 오류가 발생함
 
        for(int i = 0; i < landAnimal.size(); i++){
            landAnimal.get(i).crying();
        }
    }
}
cs

실행 결과는 육지동물, 야옹 야옹, 멍 멍이 차례대로 출력이 되는데, 위의 코드에서 25행에 있는 extends LandAnimal 구문을 생략해도 제대로 작동을 하지만 코드의 명확성을 위해서 타입의 제한을 명시하는 것이 가독성에 도움이 된다.

제네릭 메서드(Generic method)

제네릭 메서드는 메서드의 선언부에 타입 변수를 사용한 메서드를 의미한다. 이때 타입변수의 선언은 메서드 선언부에서 반환 타입 바로 앞에 위치한다.

1
2
3
4
5
6
7
8
9
10
// 제네릭 메서드(Generic method)
public static <T> void sort( ... ) { ... }
 
class AnimalList<T> {
    ...
    public static <T> void sort(List<T> list, Comparator<super T> comp){
        ...
    }
    ...
}
cs

4행부터 시작하는 예시의 제네릭 클래스에서 정의된 타입 변수 T와 제네릭 메서드에서 사용된 타입 변수 T는 전혀 다른 별개의 것이다.

와일드 카드(Wild card)

와일드 카드(Wild card)는 이름에 제한을 두지 않음을 표현하는 데 사용되는 기호이다. Java의 제네릭에서는 ? 기호를 사용해 와일드 카드를 사용할 수 있다.

1
2
3
4
// 와일드 카드(Wild card)
<?>             // 타입 변수에 모든 타입을 사용할 수 있음
<extends T>    // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음
<super T>     // T 타입과 T 타입을 상속받은 조상 클래스 타입만을 사용할 수 있음
cs

아래의 예시는 클래스 메서드(Static method)인 cryingAnimalList() 메서드의 매개변수의 타입을 와일드 카드를 사용해 제한하는 코드이다.

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
import java.util.*;
 
class LandAnimal {
    public void crying(){
        System.out.println("육지동물");
    }
}
 
class Cat extends LandAnimal {
    public void crying(){
        System.out.println("야옹 야옹");
    }
}
class Dog extends LandAnimal {
    public void crying(){
        System.out.println("멍 멍");
    }
}
class Sparrow {
    public void crying(){
        System.out.println("짹 짹");
    }
}
 
class AnimalList<T>{
    ArrayList<T> al = new ArrayList<>();
 
    public static void cryAnimalList(AnimalList<extends LandAnimal> al){
        LandAnimal la = al.get(0);
        la.crying();
    }
 
    void add(T animal) {
        al.add(animal);
    }
    T get(int index){
        return al.get(index);
    }
    boolean remove(T animal){
        return al.remove(animal);
    }
    int size(){
        return al.size();
    }
}
 
public class Main{
    public static void main(String[] args){
        AnimalList<Cat> catList = new AnimalList();
        catList.add(new Cat());
        AnimalList<Dog> dogList = new AnimalList();
        dogList.add(new Dog());
 
        AnimalList.cryAnimalList(catList);
        AnimalList.cryAnimalList(dogList);
    }
}
cs

코드의 실행 결과는 야옹 야옹, 멍 멍이 차례대로 출력이 된다.