Post

13_제네릭

13_제네릭

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);
}
  • 와일드카드는 ?로 표시되며, extendssuper 키워드를 사용해 특정 타입의 상위 또는 하위 타입만 허용하도록 제한할 수 있습니다.

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 {}
This post is licensed under CC BY 4.0 by the author.