Java 제네릭
13.1 제네릭이란?
1
2
3
| ArrayList<String> list = new ArrayList<String>();
list.add("안녕하세요"); // 문자열만 추가 가능
String text = list.get(0); // 형변환 필요 없음
|
- 제네릭은 클래스나 메소드에서 사용할 데이터 타입을 컴파일 시에 지정하는 방법입니다.
13.2 제네릭 타입
1
2
3
4
5
6
7
8
9
| class Box<T> {
private T item;
public void setItem(T item) { this.item = item; }
public T getItem() { return item; }
}
Box<Integer> intBox = new Box<>();
intBox.setItem(10);
|
- 제네릭 타입은 클래스나 인터페이스를 정의할 때 타입 파라미터를 사용하여 다양한 타입에 대응할 수 있게 합니다.
13.3 제네릭 메소드
1
2
3
4
5
6
7
8
| public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
Integer[] intArray = {1, 2, 3};
printArray(intArray); // Integer 배열 출력
|
- 제네릭 메소드는 메소드 내에서만 사용되는 타입 파라미터를 선언하여 다양한 타입의 매개변수를 처리할 수 있게 합니다.
13.4 제한된 타입 파라미터
1
2
3
4
5
6
7
8
9
10
| public static <T extends Number> double sum(T[] array) {
double sum = 0.0;
for (T element : array) {
sum += element.doubleValue();
}
return sum;
}
Integer[] numbers = {1, 2, 3};
double result = sum(numbers); // 6.0
|
- 제한된 타입 파라미터는 특정 타입이나 그 하위 타입만 받을 수 있도록 제한하는 기능입니다.
13.5 와일드카드 타입 파라미터
1
2
3
4
5
6
7
8
9
10
11
12
| // 상한 와일드카드
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
// 하한 와일드카드
public static void addIntegers(List<? super Integer> list) {
list.add(10);
list.add(20);
}
|
- 와일드카드는
?로 표시되며, extends와 super 키워드를 사용해 특정 타입의 상위 또는 하위 타입만 허용하도록 제한할 수 있습니다.
1. 제네릭 기본 이해하기
Java에서 제네릭은 클래스, 인터페이스, 메소드를 정의할 때 타입을 파라미터로 사용하는 기법입니다.
1
2
3
4
5
6
7
8
9
10
11
12
| // 기본적인 제네릭 클래스 정의
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
|
2. 타입 소거(Type Erasure) 이해하기
Java의 제네릭은 컴파일 시간에만 타입 체크를 하고, 런타임에는 타입 정보가 소거됩니다. 이를 타입 소거라고 합니다.
1
2
3
4
5
| List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 런타임에는 둘 다 그냥 List가 됨
System.out.println(stringList.getClass() == intList.getClass()); // true
|
타입 소거의 의미론(Semantics)
Swift 문서에서 언급된 “producing position”과 “consuming position” 개념을 Java에 적용해 보겠습니다.
생산 위치(Producing Position)
1
2
3
4
5
6
7
8
9
10
11
12
13
| interface Animal<CommodityType extends Food> {
CommodityType produce(); // 생산 위치에 있는 제네릭 타입
}
// 사용 예시
public <T extends Animal<? extends Food>> List<Food> collectFood(List<T> animals) {
List<Food> foods = new ArrayList<>();
for (T animal : animals) {
// animal.produce()는 항상 Food의 하위 타입을 반환하므로 안전함
foods.add(animal.produce());
}
return foods;
}
|
소비 위치(Consuming Position)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| interface Animal<FeedType extends AnimalFeed> {
void eat(FeedType feed); // 소비 위치에 있는 제네릭 타입
}
// 사용 시 문제점
public void feedAnimals(List<Animal<? extends AnimalFeed>> animals, AnimalFeed feed) {
for (Animal<? extends AnimalFeed> animal : animals) {
// 컴파일 에러! animal이 정확히 어떤 AnimalFeed를 필요로 하는지 알 수 없음
// animal.eat(feed);
}
}
// 올바른 해결책
public <T extends AnimalFeed> void feedAnimal(Animal<T> animal, T feed) {
// 구체적인 타입 T를 알고 있으므로 안전하게 호출 가능
animal.eat(feed);
}
|
3. 구현 세부사항 숨기기
Java에서는 Swift의 ‘opaque result type’과 비슷한 개념으로 와일드카드와 제한된 제네릭 타입을 사용할 수 있습니다.
1
2
3
4
5
6
7
| // 반환 타입의 구체적인 구현을 숨기면서 인터페이스만 노출
public <T extends Collection<? extends Animal>> T getAnimals() {
// 내부적으로는 ArrayList를 반환하지만 외부에는 Collection 인터페이스만 노출
ArrayList<Cow> cows = new ArrayList<>();
cows.add(new Cow());
return (T) cows;
}
|
4. 타입 관계 식별하기
Java에서는 Swift의 ‘same-type requirement’와 정확히 같은 기능은 없지만, 제네릭 타입 경계(bounds)와 와일드카드를 사용하여 유사한 제약을 표현할 수 있습니다.
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
| interface AnimalFeed<C extends Crop<? extends AnimalFeed<C>>> {
C grow();
}
interface Crop<F extends AnimalFeed<? extends Crop<F>>> {
F harvest();
}
// 구체적인 구현
class Corn implements Crop<CornFeed> {
@Override
public CornFeed harvest() {
return new CornFeed();
}
}
class CornFeed implements AnimalFeed<Corn> {
@Override
public Corn grow() {
return new Corn();
}
}
// 사용 예
public void demonstrateLifecycle() {
Corn corn = new Corn();
CornFeed feed = corn.harvest();
Corn newCorn = feed.grow();
// 타입 관계가 보장됨
}
|
5. 고급 제네릭 패턴
5.1 재귀적 타입 경계
1
2
3
4
5
6
7
8
9
10
11
12
| public class Node<T extends Comparable<T>> implements Comparable<Node<T>> {
private T data;
public Node(T data) {
this.data = data;
}
@Override
public int compareTo(Node<T> other) {
return this.data.compareTo(other.data);
}
}
|
5.2 타입 토큰
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public class TypeSafeRepository<T> {
private final Class<T> type;
public TypeSafeRepository(Class<T> type) {
this.type = type;
}
public T findById(long id) {
// type 정보를 사용하여 올바른 타입의 객체 검색
try {
// 간단한 예시로 기본 생성자를 통해 객체 생성
return type.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 사용 예
public static void main(String[] args) {
TypeSafeRepository<String> repo = new TypeSafeRepository<>(String.class);
String result = repo.findById(1);
}
}
|
6. 제네릭과 상속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| interface Producer<T> {
T produce();
}
class FoodProducer implements Producer<Food> {
@Override
public Food produce() {
return new Food();
}
}
class AppleProducer extends FoodProducer {
@Override
public Apple produce() { // 반환 타입을 하위 타입으로 공변 반환 가능
return new Apple();
}
}
class Food {}
class Apple extends Food {}
|