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) 제네릭으로 해결
제네릭을 사용하면 컴파일 시 타입을 체크하여 런타임 오류를 방지할 수 있습니다.
코드 재사용성과 타입 안정성을 동시에 보장합니다.
제네릭을 사용한 해결 코드
// 제네릭 클래스를 사용
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);
}
}
실행 결과
Value: Hello, Generics!
Value: 123
Value: 3.14
제네릭의 기본 특징
(1) 기본형은 제네릭 타입으로 사용할 수 없다
제네릭은 참조형 타입만 사용 가능합니다.
기본형 데이터 타입(int, double 등)을 사용하려면 래퍼 클래스(Integer, Double)로 변환해야 합니다.
코드 예시
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 타입으로 처리됩니다.
문제점:
타입 안정성이 보장되지 않아, 잘못된 타입 사용 시 런타임 오류가 발생할 수 있습니다.
코드 예시
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 클래스 또는 그 하위 클래스만 허용됩니다.
코드 예시
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) 예제: 캐스팅 없이 타입 반환
코드 예시
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) 타입 제한이 있는 제네릭 메서드
코드 예시
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) 제네릭 메서드의 타입 추론
컴파일러는 전달된 인자를 기반으로 타입을 추론합니다.
코드 예시
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)
코드 예시
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. 정리
제네릭 클래스:
객체 생성 시점에 타입을 결정.
동일한 로직을 여러 타입에서 재사용 가능.
제네릭 메서드:
메서드 호출 시점에 타입을 결정.
캐스팅 없이 입력된 타입 그대로 반환 가능.
타입 제한:
extends 키워드를 사용해 허용된 타입 범위를 설정.
컴파일 단계에서 타입 오류를 방지.
와일드카드 (Wildcard)
1. 와일드카드란?
정의: 와일드카드는 제네릭의 유연성을 높이기 위해 사용되며, **?**로 표현됩니다.
목적:
특정 타입에 한정되지 않고, 여러 타입을 처리할 수 있도록 제네릭을 확장.
이미 만들어진 제네릭 클래스나 메서드를 활용할 때 사용.
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);
}
}
코드 설명:
Box<T> 클래스:
제네릭 클래스로, 타입에 따라 item을 저장합니다.
타입 안정성을 제공하며, 타입 캐스팅 없이 사용할 수 있습니다.
printBoxContents 메서드:
와일드카드 <? extends Number>를 사용하여 Number 타입의 하위 클래스만 허용합니다. 즉, Integer, Double, Float 등이 가능합니다.
리스트 안의 값을 출력합니다.
와일드카드의 유연성:
printBoxContents는 타입을 명시적으로 지정하지 않고 Integer와 Double 두 타입 모두를 처리할 수 있습니다.
특정 타입만 허용하려는 경우, 와일드카드로 제한을 설정 (extends 또는 super)할 수 있습니다.
실행 결과:
Integer List:
1
2
3
4
5
Double List:
1.1
2.2
3.3
2. 와일드카드와 제네릭의 차이점
구분
제네릭
와일드카드
사용 대상
새로 정의하는 클래스나 메서드
이미 만들어진 제네릭 클래스나 메서드 활용
표현 방법
타입 매개변수(T, E 등) 사용
? 사용
역할
타입 안정성을 유지하면서 동적인 타입 처리
타입 제한 없이 더 유연한 처리 가능
3. 와일드카드의 타입 제한
(1) 제한 없는 와일드카드 (<?>)
어떤 타입이든 허용.
코드 예시
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 클래스 또는 하위 클래스만 허용.
코드 예시
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 클래스 또는 상위 클래스만 허용.
코드 예시
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. 타입 이레이저의 동작
컴파일 전:
class Box<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
컴파일 후:
class Box {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
3. 타입 이레이저의 주의점
타입 정보 소실:
제네릭의 타입 매개변수는 컴파일 시점에만 사용되고, 컴파일 이후에는 제거됩니다.
T를 사용한 인스턴스 생성 불가:
컴파일 이후 타입 정보가 사라지므로 new T()와 같은 인스턴스 생성은 불가능합니다.
코드 예시: 컴파일 오류
class GenericClass<T> {
// T value = new T(); // 컴파일 오류: 타입 이레이저로 인해 불가능
}
4. 예제: 타입 이레이저 동작
(1) 제네릭 클래스 컴파일 후 동작
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를 활용한 타입 제한
코드 예시
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로 변환
}
}