제네릭 (Generics) 이란?
제네릭 (Generics)
자바에서 클래스나 메소드를 정의할 때, 데이터 타입을 일반화(Generalization)할 수 있게 하는 것이다.
이로 인해 사용할 데이터의 타입을 외부에서 설정할 수 있다.
1. 타입 파라미터
제네릭을 이용해 클래스나 인터페이스, 메소드를 정의할 때 사용하며, 타입 매개변수라고도 한다.
제네릭에서는 참조(Reference) 타입만 할당 받을 수 있다.
즉, int나 double과 같은 원시(Primitive) 타입은 타입 매개변수로 줄 수 없고, 대신에 Wrapper 클래스를 사용해야 한다.
<>
(다이아몬드 연산자) 사이에 식별자 기호를 입력해 파라미터화 해서 사용한다.
1-1. 타입 파라미터 기호 컨벤션
다이아몬드 연산자 사이에 입력하는 식별자 기호는 문접적으로 정해진 것이 없다.
하지만, 통상적인 네이밍 방식은 아래와 같다.
타입 | 설명 |
---|---|
<T> | 타입(Type) |
<E> | 요소(Element) |
<K> | 키(Key) |
<V> | 값(Value) |
<N> | 숫자(Number) |
<S, U, V> | 2, 3, 4번째 타입 |
2. 제네릭 주의사항
2-1. static 멤버의 타입으로 타입 매개변수 사용 불가
static 필드의 타입이나 static 메소드의 반환 타입 및 매개변수 타입으로 타입 매개변수를 사용할 수 없다.
static 멤버는 인스턴스가 생성되기 전에 이미 타입이 정해져 있어야 하기 때문에 인스턴스를 생성할 때 타입이 정해지는 타입 매개변수를 사용할 수 없다.
1
2
3
4
5
6
7
8
9
public class Box<T> {
private static T item; // static 필드에 사용 불가
// static 메소드의 매개변수에 사용 불가
public static void push(T info) {}
// // static 메소드의 반환 타입에 사용 불가
public static T pop() {}
}
static 메소드의 반환 타입 및 매개변수 타입으로 타입 매개변수를 사용할 수 있는 제네릭 메소드
는 이후 설명한다!
2-2. 제네릭 타입의 객체 생성 불가
타입 매개변수로로 어떤 것이 올지 모르기 때문에 제네릭 타입으로 객체를 생성할 수 없다.
1
2
3
4
5
6
public class Box<T> {
public void generate() {
T t = new T(); // 객체 생성 불가
}
}
다만, 제네릭 타입으로 객체를 생성하지 못하면 죽을 수도 있는 상황일 때는 java의 Reflection을 이용해 Class 객체
를 통해 생성할 수는 있다.
3. 제네릭 클래스
클래스 정의 시 타입 매개변수를 사용해 다양한 데이터 타입을 처리할 수 있는 클래스이다.
1
2
3
4
5
6
7
8
9
10
11
class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
1
2
3
4
5
public static main(String[] args) {
Box<String> stringBox = new Box<>(); // 생성자의 타입 파라미터는 생략 가능
stringBox.setItem("Hello Generics");
System.out.println(stringBox.getItem());
}
4. 제네릭 인터페이스
타입 매개변수를 사용해 정의한 인터페이스이다.
1
2
3
4
interface portable<T> {
T move();
void stop(T t);
}
5. 제네릭 메소드
위에 나온 제네릭 주의사항에 다음과 같은 주의 사항이 있었다.
static 메소드의 반환 타입 및 매개변수 타입으로 타입 매개변수를 사용할 수 없다.
하지만, 클래스의 제네릭 타입을 받아와 사용하는 일반적인 메소드와 다르게, 제네릭 메소드는 메소드가 호출될 때 제네릭을 설정하고 클래스와 독립적이기 때문에, static으로 정의 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Box<T> {
// 제네릭 메소드는 독립적이기 때문에 클래스의 타입 파라미터와 다른 타입 파라미터 기호 사용 가능
public static <K, V> K print(K key, V value) {
System.out.println(value.toString());
return key;
}
}
class Main {
public static void main(String[] args) {
Box.<Integer, Double>print(0, 1.2);
Box.<Integer, String>print(1, "Hello generics");
// 아래와 같이 인자를 통해 제네릭 타입 매개변수를 추정할 수 있기 때문에, 생략 가능
Box.print(0, 1.2);
Box.print(1, "Hello generics");
}
}
6. 제네릭 타입 제한
제네릭은 클래스나 메소드가 특정 조건을 만족하는 타입만을 처리할 수 있게 제한된 타입 매개변수만 허용하는 타입 제한 기능이 있다.
자바에서는 타입 제한을 위해 extends
키워드를 사용한다.
6-1. 단일 타입 제한
기본적인 사용법은 <T extends [제한타입]>
이다.
이것의 의미는 제한타입
과 그 하위 타입들만 받도록 타입 파라미터 범위를 제한한다는 뜻이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Calculator<T extends Number> {
T add(T a, T b) { ... }
T max(T a, T b) { ... }
}
public class Main {
public static void main(String[] args) {
// Number과 그의 하위 타입만 가능
Calculator<Number> calculator = new Calculator<>();
Calculator<Integer> integerCalculator = new Calculator<>();
// Calculator<String> stringCalculator = new Calculator<>(); Number과 상관 없는 타입 불가
// Calculator<Object> objectCalculator = new Calculator<>(); Number의 상위 타입도 불가
}
}
제한 타입으로 올 수 있는 타입은 일반 클래스 뿐만 아니라 추상 클래스와 인터페이스도 올 수 있다.
자바에서 인터페이스에는 extends
키워드가 아닌 implements
키워드를 사용하지만, 제네릭 타입 제한에 있어서는 extends
키워드를 그대로 사용한다.
1
2
3
4
interface Portable {}
// 인터페이스도 extends 키워드를 사용한다.
class Box<T extends Portable> {}
6-2. 다중 타입 제한
두 개 이상의 타입들을 동시에 상속 및 구현한 타입으로 제한하려고 한다면 &
연산자를 사용해 다중 타입 제한을 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Tank {}
interface Installable {}
class SiegeTank extends Tank implements Installable {}
class Garage<T extends Tank & Installable> {}
public class Main {
public static void main(String[] args) {
// Tank와 Installable을 동시에 상속 및 구현한 클래스만 가능
Garage<SiegeTank> garage = new Garage<>();
}
}
7. 제네릭 와일드 카드
자바의 일반적인 타입들과는 다르게 제네릭은 기본적으로 서브 타입 간 형변환이 불가하다.
이를 제네릭의 무공변성
이라고 한다.
자바에서는 무공변성을 해결하기 위해 와일드 카드 ?
를 제공한다.
1
2
3
4
5
6
// 자동 형변환 불가
ArrayList<Object> list = new ArrayList<Integer>();
// 강제 형변환 불가
ArrayList<String> stringList = new ArrayList<>();
ArrayList<Object> objectList = (ArrayList<Object>) stringList;
7-1. 와일드 카드 경계
이 부분은 이해하기 어려울 수 있는 부분이니 집중하고 읽어야 할 부분이다.
경계에 대해 살펴보기 전에 다음과 같은 구조의 클래스들이 있다고 가정해보자.
7-1-1. 상한 경계 (공변)
<? extends 타입>
와 같은 형식으로 사용하며, 타입 매개변수의 범위가 입력한 타입과 그의 하위 타입만 허용한다.
1
2
3
4
5
6
7
8
9
10
public void method(List<? extends Animal> animals) {
Animal a = animals.get(0); // 가능!
Creature c = animals.get(0); // 가능!
Dog d = animals.get(0); // 불가능!
animals.add(new Animal()); // 불가능!
animals.add(new Dog()); // 불가능!
animals.add(null); // 가능!
}
위 예시를 보면 매개변수 animals의 타입은 List<Animal>
일 수도 List<Dog>
일 수도 List<Cat>
일 수도 있다.
animals에 Animal 객체나, Dog 객체를 넣게 된다면, 타입 안전성을 보장할 수 없기 때문에 불가능하다.
그래서 보통 상한 경계는 다음과 같이 Collection으로부터 객체를 꺼내 변수를 만들고 할당하는 역할(Producer)을 할 때 사용한다.
1
2
3
4
5
void print(List<? extends Animal> animals) {
for (Animal a : animals) {
System.out.println(a);
}
}
7-1-2. 하한 경계 (반공변)
<? super 타입>
와 같은 형식으로 사용하며, 타입 매개변수의 범위가 입력한 타입과 그의 상위 타입만 허용한다.
1
2
3
4
5
6
7
8
9
10
11
public void method(List<? super Animal> animals) {
animals.add(new Animal()); // 가능!
animals.add(new Dog()); // 가능!
animals.add(null); // 가능!
animals.add(new Creature()) // 불가능!
Dog d = animals.get(0); // 불가능!
Animal a = animals.get(0); // 불가능!
Object o = animals.get(0); // 가능!
}
위 예시를 보면 매개변수 animals의 타입은 List<Animal>
일 수도 List<Creature>
일 수도 List<Object>
일 수도 있다.
때문에, 타입 안정성을 보장하기 위해 최고 조상 타입인 Object 타입으로만 객체를 꺼낼 수 있다.
그래서 보통 하한 경계는 다음과 같이 Collection에 객채를 넣어 소비하는 역할(Consumer)을 할 때 사용한다.
1
2
3
4
void add(List<? super Animal> animals) {
animals.add(new Dog());
animals.add(new Animal());
}
7-1-3. 비경계
<?>
형식으로 사용하며, 타입 매개변수로 모든 타입이 올 수 있다.
1
2
3
4
5
6
7
8
9
10
11
public void method(List<?> animals) {
animals.add(new Creature()) // 불가능!
animals.add(new Animal()); // 불가능!
animals.add(new Dog()); // 불가능!
animals.add(null); // 가능!
Dog d = animals.get(0); // 불가능!
Animal a = animals.get(0); // 불가능!
Object o = animals.get(0); // 가능!
}
위 예시를 보면 매개변수 animals의 타입으로 모든 타입이 될 수 있다.
때문에, 타입 안정성을 보장하기 위해 null만 넣을 수 있고 최고 조상 타입인 Object 타입으로만 객체를 꺼낼 수 있다.
7-1-4. PECS (Producer-Extends, Consumer-Super)
Effective java에서 소개한 원칙이다.
위의 상한 경계와 하한 경계를 이해했다면, 자연스럽게 이해되는 내용일 것이다.
Collection의 입장에서 생각하면 이해하기 쉽다.
- Collection에 있는 데이터로 변수를 만들어 할당하는 경우, <? extends> 사용
- 기존에 있는 객체를 Collection이 먹어버리는 (추가하는) 경우, <? super> 사용
7-2. 와일드 카드의 사용 시기
와일드 카드는 제네릭 클래스나 제네릭 메소드 등의 제네릭 타입을 선언할 때는 사용할 수 없고, 제네릭을 활용하여 메소드의 파라미터나 반환 타입, 변수의 타입 등을 설정할 때 사용할 수 있다.
글로만 보면 이해가 잘 가지 않을 수 있으니 아래 예시를 참고하자!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 제네릭 클래스의 타입 선언 시 사용 불가
class Box<? extends T> {
// 제네릭 메소드의 타입 선언 시 사용 불가
public <? extends T> void close() { ... }
// 메소드의 반환 타입이나 파라미터를 설정할 때 사용 가능
public List<? extends T> open(List<? extends T> input) { ... }
}
public class Main {
public static void main(String[] args) {
// 변수 타입 설정 시 사용 가능
Box<?> stringBox = new Box<String>();
Box<? extends Number> intBox = new Box<Integer>();
// 와일드 카드를 활용해 구현한 메소드의 사용
Box<Number> numberBox = new Box<>();
List<Double> doubleList = new ArrayList<>();
numberBox.open(doubleList);
}
}