# 제네릭 보충자료

### 제네릭이 필요한 이유

#### (1) 동일한 기능, 다른 타입

* 동일한 로직을 처리하지만, 타입이 다를 경우 매번 새로운 메서드를 생성해야 합니다.

**문제점**

```java
class Printer {
    void printString(String str) {
        System.out.println("String: " + str);
    }

    void printInteger(Integer num) {
        System.out.println("Integer: " + num);
    }
}
```

* 위 코드처럼 각 타입별로 메서드를 정의하면 **중복 코드**가 발생합니다.

#### (2) 다형성으로 해결

* **Object 타입**을 사용하면 다형성을 이용하여 타입을 처리할 수 있습니다.

**문제점**

```java
class Printer {
    void printObject(Object obj) {
        System.out.println("Object: " + obj);
    }
}

public class GenericExample {
    public static void main(String[] args) {
        Printer printer = new Printer();
        printer.printObject("Hello"); // 정상 실행
        printer.printObject(123);    // 정상 실행

        // 잘못된 타입으로 캐스팅
        String result = (String) printer.printObject(123); // 런타임 오류 발생 가능
    }
}
```

* **타입 안정성 부족**: 잘못된 타입을 캐스팅할 경우 **런타임 오류**가 발생할 수 있습니다.
* 컴파일 단계에서 이러한 오류를 잡아낼 수 없습니다.

#### (3) 제네릭으로 해결

* 제네릭을 사용하면 **컴파일 시 타입을 체크**하여 런타임 오류를 방지할 수 있습니다.
* 코드 재사용성과 **타입 안정성**을 동시에 보장합니다.

#### 제네릭을 사용한 해결 코드

```java
// 제네릭 클래스를 사용
class Printer<T> { // T는 타입 매개변수
    void print(T obj) {
        System.out.println("Value: " + obj);
    }
}

public class GenericSolutionExample {
    public static void main(String[] args) {
        // String 타입 처리
        Printer<String> stringPrinter = new Printer<>();
        stringPrinter.print("Hello, Generics!");

        // Integer 타입 처리
        Printer<Integer> integerPrinter = new Printer<>();
        integerPrinter.print(123);

        // Double 타입 처리
        Printer<Double> doublePrinter = new Printer<>();
        doublePrinter.print(3.14);
    }
}
```

***

#### 실행 결과

```makefile
Value: Hello, Generics!
Value: 123
Value: 3.14
```

### 제네릭의 기본 특징

#### (1) 기본형은 제네릭 타입으로 사용할 수 없다

* 제네릭은 **참조형 타입**만 사용 가능합니다.
* 기본형 데이터 타입(`int`, `double` 등)을 사용하려면 **래퍼 클래스**(`Integer`, `Double`)로 변환해야 합니다.

**코드 예시**

```java
class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class GenericPrimitiveExample {
    public static void main(String[] args) {
        // 기본형 사용 불가: Box<int> box = new Box<>(); // 컴파일 오류
        Box<Integer> intBox = new Box<>(); // 래퍼 클래스 사용
        intBox.setValue(123);
        System.out.println(intBox.getValue()); // 123
    }
}
```

***

#### (2) <> 없이 사용 가능 (원시 타입, Raw Type)

* 제네릭 타입을 선언하지 않으면 **Object 타입**으로 처리됩니다.
* **문제점**:
  * 타입 안정성이 보장되지 않아, 잘못된 타입 사용 시 런타임 오류가 발생할 수 있습니다.

**코드 예시**

```java
class RawTypeExample {
    public static void main(String[] args) {
        Box rawBox = new Box(); // 원시 타입 사용
        rawBox.setValue("Hello");
        System.out.println(rawBox.getValue()); // "Hello"

        rawBox.setValue(123); // 다른 타입도 저장 가능
        System.out.println(rawBox.getValue()); // 123 (Object 타입 반환)
    }
}
```

***

### 2. 타입 매개변수 제한

#### (1) `extends`로 제한

* 타입 매개변수를 특정 타입 또는 특정 타입의 하위 클래스만 허용하도록 제한할 수 있습니다.
* **`T extends Type`**:
  * `T`는 `Type` 클래스 또는 그 하위 클래스만 허용됩니다.

**코드 예시**

```java
class NumberBox<T extends Number> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

public class TypeBoundExample {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>(); // Integer는 Number의 하위 클래스
        intBox.setValue(100);
        System.out.println(intBox.getValue()); // 100

        NumberBox<Double> doubleBox = new NumberBox<>(); // Double도 허용
        doubleBox.setValue(3.14);
        System.out.println(doubleBox.getValue()); // 3.14

        // NumberBox<String> stringBox = new NumberBox<>(); // 컴파일 오류: String은 Number의 하위 클래스가 아님
    }
}
```

***

### 3. 제네릭 메서드

#### (1) 정의와 특징

* **정의**:
  * 반환 타입 앞에 \*\*`<T>`\*\*와 같은 타입 매개변수를 선언하여 제네릭 메서드를 정의합니다.
  * 메서드 호출 시점에 타입이 결정됩니다.
* **특징**:
  * **인스턴스 메서드**와 **static 메서드** 모두 적용 가능합니다.
  * 메서드 내부에서만 타입 매개변수가 적용됩니다.

***

#### (2) 예제: 캐스팅 없이 타입 반환

**코드 예시**

```java
class Util {
    public static <T> T getValue(T value) { // 제네릭 메서드
        return value; // 입력받은 타입 그대로 반환
    }
}

public class GenericMethodExample {
    public static void main(String[] args) {
        String result = Util.getValue("Hello, Generics!"); // 타입 추론
        System.out.println(result); // Hello, Generics!

        Integer number = Util.getValue(123); // 타입 추론
        System.out.println(number); // 123
    }
}
```

***

#### (3) 타입 제한이 있는 제네릭 메서드

**코드 예시**

```java
class MathUtil {
    public static <T extends Number> double add(T a, T b) { // Number 타입 제한
        return a.doubleValue() + b.doubleValue();
    }
}

public class BoundedGenericMethodExample {
    public static void main(String[] args) {
        System.out.println(MathUtil.add(10, 20)); // 30.0
        System.out.println(MathUtil.add(3.5, 2.5)); // 6.0

        // MathUtil.add("10", "20"); // 컴파일 오류: String은 Number의 하위 클래스가 아님
    }
}
```

***

#### (4) 제네릭 메서드의 타입 추론

* 컴파일러는 전달된 인자를 기반으로 타입을 추론합니다.

**코드 예시**

```java
class TypeInference {
    public static <T> void printType(T value) {
        System.out.println("Type: " + value.getClass().getName());
    }
}

public class TypeInferenceExample {
    public static void main(String[] args) {
        TypeInference.printType("Hello"); // String 타입 추론
        TypeInference.printType(123);    // Integer 타입 추론
        TypeInference.printType(3.14);   // Double 타입 추론
    }
}
```

***

### 4. 제네릭 클래스와 제네릭 메서드의 차이

| **구분**       | **제네릭 클래스**            | **제네릭 메서드**                              |
| ------------ | ---------------------- | ---------------------------------------- |
| **타입 결정 시점** | 객체를 생성하는 시점            | 메서드를 호출하는 시점                             |
| **범위**       | 클래스 전체에서 타입 매개변수 사용 가능 | 메서드 내부에서만 타입 매개변수 사용 가능                  |
| **적용 방식**    | `class Box<T>`         | `public static <T> void method(T value)` |

**코드 예시**

```java
class GenericClass<T> { // 제네릭 클래스
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

class GenericMethods {
    public static <T> T echo(T value) { // 제네릭 메서드
        return value;
    }
}

public class GenericExample {
    public static void main(String[] args) {
        // 제네릭 클래스
        GenericClass<String> stringBox = new GenericClass<>();
        stringBox.setValue("Hello");
        System.out.println(stringBox.getValue());

        // 제네릭 메서드
        System.out.println(GenericMethods.echo(123)); // 123
        System.out.println(GenericMethods.echo("Generics!")); // Generics!
    }
}
```

***

### 5. 정리

1. **제네릭 클래스**:
   * 객체 생성 시점에 타입을 결정.
   * 동일한 로직을 여러 타입에서 재사용 가능.
2. **제네릭 메서드**:
   * 메서드 호출 시점에 타입을 결정.
   * 캐스팅 없이 입력된 타입 그대로 반환 가능.
3. **타입 제한**:
   * `extends` 키워드를 사용해 허용된 타입 범위를 설정.
   * 컴파일 단계에서 타입 오류를 방지.

## 와일드카드 (Wildcard)

### 1. 와일드카드란?

* **정의**: 와일드카드는 제네릭의 **유연성**을 높이기 위해 사용되며, \*\*`?`\*\*로 표현됩니다.
* **목적**:
  * 특정 타입에 한정되지 않고, 여러 타입을 처리할 수 있도록 제네릭을 확장.
  * **이미 만들어진 제네릭 클래스나 메서드를 활용할 때 사용**.

***

```java
import java.util.ArrayList;
import java.util.List;

class Box<T> {
    private T item;

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

    public T getItem() {
        return item;
    }
}

public class WildcardExample {
    // 이미 만들어진 제네릭 클래스를 활용하는 메서드
    public static void printBoxContents(List<? extends Number> boxes) {
        for (Number num : boxes) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        // Box 클래스는 제네릭으로 만들어진 클래스
        Box<Integer> intBox = new Box<>();
        intBox.setItem(100);

        Box<Double> doubleBox = new Box<>();
        doubleBox.setItem(45.67);

        // 제네릭 클래스의 List를 생성
        List<Box<? extends Number>> boxList = new ArrayList<>();
        boxList.add(intBox);
        boxList.add(doubleBox);

        // 와일드카드를 활용한 리스트
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        // 이미 만들어진 제네릭 메서드를 활용
        System.out.println("Integer List:");
        printBoxContents(intList);

        System.out.println("Double List:");
        printBoxContents(doubleList);
    }
}
```

#### 코드 설명:

1. **`Box<T>` 클래스**:
   * 제네릭 클래스로, 타입에 따라 `item`을 저장합니다.
   * 타입 안정성을 제공하며, 타입 캐스팅 없이 사용할 수 있습니다.
2. **`printBoxContents` 메서드**:
   * 와일드카드 `<? extends Number>`를 사용하여 `Number` 타입의 하위 클래스만 허용합니다. 즉, `Integer`, `Double`, `Float` 등이 가능합니다.
   * 리스트 안의 값을 출력합니다.
3. **와일드카드의 유연성**:
   * `printBoxContents`는 타입을 명시적으로 지정하지 않고 `Integer`와 `Double` 두 타입 모두를 처리할 수 있습니다.
   * 특정 타입만 허용하려는 경우, 와일드카드로 제한을 설정 (`extends` 또는 `super`)할 수 있습니다.

#### 실행 결과:

```mathematica
Integer List:
1
2
3
4
5
Double List:
1.1
2.2
3.3
```

### 2. 와일드카드와 제네릭의 차이점

| **구분**    | **제네릭**                 | **와일드카드**               |
| --------- | ----------------------- | ----------------------- |
| **사용 대상** | 새로 정의하는 클래스나 메서드        | 이미 만들어진 제네릭 클래스나 메서드 활용 |
| **표현 방법** | 타입 매개변수(`T`, `E` 등) 사용  | `?` 사용                  |
| **역할**    | 타입 안정성을 유지하면서 동적인 타입 처리 | 타입 제한 없이 더 유연한 처리 가능    |

***

### 3. 와일드카드의 타입 제한

#### (1) 제한 없는 와일드카드 (`<?>`)

* 어떤 타입이든 허용.

**코드 예시**

```java
import java.util.List;

class WildcardExample {
    public static void printList(List<?> list) { // 제한 없는 와일드카드
        for (Object item : list) {
            System.out.println(item);
        }
    }

    public static void main(String[] args) {
        List<String> stringList = List.of("A", "B", "C");
        List<Integer> intList = List.of(1, 2, 3);

        printList(stringList); // 모든 타입 허용
        printList(intList);    // 모든 타입 허용
    }
}
```

***

#### (2) 상한 제한 와일드카드 (`<? extends Type>`)

* 와일드카드로 **`Type` 클래스 또는 하위 클래스**만 허용.

**코드 예시**

```java
import java.util.List;

class WildcardWithUpperBound {
    public static void printNumbers(List<? extends Number> list) { // 상한 제한
        for (Number num : list) {
            System.out.println(num);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3);
        List<Double> doubleList = List.of(1.1, 2.2, 3.3);

        printNumbers(intList);    // Integer는 Number의 하위 클래스
        printNumbers(doubleList); // Double도 Number의 하위 클래스
    }
}
```

***

#### (3) 하한 제한 와일드카드 (`<? super Type>`)

* 와일드카드로 **`Type` 클래스 또는 상위 클래스**만 허용.

**코드 예시**

```java
import java.util.List;

class WildcardWithLowerBound {
    public static void addNumbers(List<? super Integer> list) { // 하한 제한
        list.add(10); // Integer 추가 가능
        list.add(20);
    }

    public static void main(String[] args) {
        List<Number> numList = new java.util.ArrayList<>();
        addNumbers(numList); // Number는 Integer의 상위 클래스
        System.out.println(numList);
    }
}
```

***

### 4. 와일드카드의 특징과 주의점

* **동적 타입 반환이 아님**:
  * 와일드카드는 반환 타입을 변경하거나 동적으로 처리하지 않습니다.
  * 반환되는 객체는 `Object`로 처리되거나 명시적 캐스팅이 필요합니다.
* **제네릭과 함께 사용**:
  * 새롭게 정의된 제네릭보다는 **이미 만들어진 제네릭 클래스/메서드 활용**에 적합.

***

## 타입 이레이저 (Type Erasure)

### 1. 타입 이레이저란?

* **정의**: 자바의 제네릭은 컴파일 시점에만 타입을 체크하며, **컴파일 후에는 모든 제네릭 정보가 제거**됩니다.
* **목적**:
  * 하위 호환성을 유지하면서도 타입 안정성을 제공.
* **특징**:
  * 컴파일 후 모든 타입 매개변수(`T`, `E`)는 \*\*`Object`\*\*로 변환.
  * 타입 매개변수가 \*\*`extends`\*\*로 제한되었을 경우, 제한된 타입으로 변환.

***

### 2. 타입 이레이저의 동작

* **컴파일 전**:

  ```java
  class Box<T> {
      private T value;
      public T getValue() {
          return value;
      }
      public void setValue(T value) {
          this.value = value;
      }
  }
  ```
* **컴파일 후**:

  ```java
  class Box {
      private Object value;
      public Object getValue() {
          return value;
      }
      public void setValue(Object value) {
          this.value = value;
      }
  }
  ```

***

### 3. 타입 이레이저의 주의점

* **타입 정보 소실**:
  * 제네릭의 타입 매개변수는 컴파일 시점에만 사용되고, 컴파일 이후에는 제거됩니다.
* **T를 사용한 인스턴스 생성 불가**:
  * 컴파일 이후 타입 정보가 사라지므로 `new T()`와 같은 인스턴스 생성은 불가능합니다.

**코드 예시: 컴파일 오류**

```java
class GenericClass<T> {
    // T value = new T(); // 컴파일 오류: 타입 이레이저로 인해 불가능
}
```

***

### 4. 예제: 타입 이레이저 동작

#### (1) 제네릭 클래스 컴파일 후 동작

```java
class GenericBox<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

public class TypeErasureExample {
    public static void main(String[] args) {
        GenericBox<String> stringBox = new GenericBox<>();
        stringBox.setValue("Hello");
        System.out.println(stringBox.getValue()); // "Hello"

        // 컴파일 이후
        // GenericBox의 모든 T가 Object로 변경됨
    }
}
```

#### (2) `extends`를 활용한 타입 제한

**코드 예시**

```java
class NumberBox<T extends Number> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

public class TypeErasureWithBoundsExample {
    public static void main(String[] args) {
        NumberBox<Integer> intBox = new NumberBox<>();
        intBox.setValue(123);
        System.out.println(intBox.getValue()); // 123

        // 컴파일 후, 모든 T는 Number로 변환
    }
}
```

***

### 5. 정리

#### 와일드카드

1. **와일드카드(`?`)**:
   * 제네릭보다 더 유연하게 이미 만들어진 클래스를 활용 가능.
2. **타입 제한**:
   * `<? extends Type>`: 상한 제한.
   * `<? super Type>`: 하한 제한.

#### 타입 이레이저

1. **컴파일 시점에서만 타입 체크**:
   * 컴파일 완료 후 제네릭 정보 제거.
   * 모든 타입 매개변수는 **Object**로 변환.
2. **주요 특징**:
   * `extends`로 타입 제한 시, 제한된 타입으로 변환.
   * 컴파일 이후 제네릭 타입 정보가 없으므로 `new T()` 불가.
