Generic
Java의 제네릭(Generic)은 데이터 타입을 일반화하여 코드 재사용성을 높이고 타입 안정성을 보장하는 기능이다.
Generic은 일반적(generalized)이라는 뜻으로, 다양한 데이터 타입에서 동작하도록 설계된 클래스나 메서드를 작성할 수 있다.
Generic의 기본 원리
Java 1.5 이전에는 타입을 지정하지 않고 Object를 사용하여 모든 데이터를 처리했다.
List list = new ArrayList();
list.add("Hello");
list.add(123); // 서로 다른 타입 추가 가능
String str = (String) list.get(0); // 강제 캐스팅 필요
💥문제점
- 강제 캐스팅 필요 : 반환 값을 원하는 타입으로 변환
- 타입 안정성 부족 : 잘못된 타입이 추가되더라도 컴파일러가 검출하지 못함
위와 같은 문제점을 Generic으로 해결했다.
Java의 제네릭은 컴파일 시점에 타입을 검사한다.
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 컴파일 오류 발생
String str = list.get(0); // 강제 캐스팅 불필요
Generic의 주요 개념
1. Generic 클래스
데이터 타입에 의존하지 않는 클래스 설계가 가능
// T는 Type의 약자
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println(stringBox.getItem()); // 출력: Hello
Box<Integer> intBox = new Box<>();
intBox.setItem(123);
System.out.println(intBox.getItem()); // 출력: 123
2. Generic 메서드
메서드에만 제네릭을 적용할 수도 있다.
public class Utils {
// T는 메서드에만 적용됨
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
String[] strings = {"A", "B", "C"};
Utils.printArray(strings); // 출력: A, B, C
Integer[] numbers = {1, 2, 3};
Utils.printArray(numbers); // 출력: 1, 2, 3
3. Bounded Type Parameters
제네릭 타입에 제약 조건을 설정할 수 있다.
public class NumberBox<T extends Number> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setItem(123); // OK
// NumberBox<String> strBox = new NumberBox<>(); // 컴파일 오류
- <T extends Number> : T는 Number 또는 그 하위 클래스(Integer, Double 등)만 가능
- <T extends Enum<T>> : T는 Enum 또는 그 하위 타입만 가능
4. Wildcards: ?
제네릭을 더 유연하게 사용하기 위해 와일드카드를 제공.
와일드카드는 제네릭 타입을 제한 없이 나타내기 위해 사용하는 기호이다.
?는 "어떤 타입인지 모름"을 의미하며, 다음과 같은 경우에 유용하다.
1. 특정 타입을 정확히 알 필요가 없을 때
2. 여러 제네릭 타입을 다룰 때
- ? (Unbounded Wildcard) : 어떤 타입이든 받을 수 있다. (제한 없음)
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
List<String> stringList = List.of("A", "B", "C");
List<Integer> intList = List.of(1, 2, 3);
printList(stringList); // 출력: A, B, C
printList(intList); // 출력: 1, 2, 3
- 장점
- String, Integer 등 다양한 타입의 리스트를 한 메서드에서 처리할 수 있다.
- 제약
- 리스트 내부의 타입을 알 수 없으므로, 요소를 추가하거나 수정할 수 없다.
- ex) list.add()는 사용 불가
- ? extends T (Upper Bounded Wildcard) : T 또는 그 하위 타입만 허용, 읽기 전용으로 활용됨
public void printNumbers(List<? extends Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
printNumbers(intList); // 출력: 1, 2, 3
printNumbers(doubleList); // 출력: 1.1, 2.2, 3.3
- 장점
- Number의 하위 타입을 모두 처리할 수 있다.
- 제약
- 리스트에 새로운 요소를 추가할 수 없다.
- ex) list.add(5)
- ? super T (Lower Bounded Wildcard) : T 또는 그 상위 타입만 허용, 쓰기 전용으로 활용됨
public void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // 출력: [1, 2, 3]
- 장점
- Integer 뿐만 아니라 Number, Object 같은 상위 타입 리스트에도 요소를 추가할 수 있다.
- 제약
- 리스트에서 요소를 읽으면 타입이 Object로 반환되므로, 구체적인 타입을 알 수 없다.
✏️ Wildcard 비교 요약
Wildcard | 설명 | 읽기 | 쓰기 |
? | 모든 타입 허용 | ✅ | ❌ |
? extends T | T와 그 하위 타입만 허용 | ✅ | ❌ |
? super T | T와 그 상위 타입만 허용 | ❌ | ✅ |
<T extends Number>와 <? extends Number>
내가 헷갈려서 추가 정리하는 <T extends Number>와 <? extends Number> 차이점
1. <T extends Number>
T라는 고유한 타입 변수를 선언하여, 제네릭 클래스나 메서드 내에서 일관되게 사용할 수 있다.
메서드나 필드에서 T 타입으로 데이터를 다룰 수 있다.
2. <? extends Number>
제네릭 타입을 제한하지만, 구체적인 타입 변수를 선언하지는 않는다.
?는 불특정 타입을 나타내며, 그 타입이 Number 또는 그 하위 클래스임을 보장
읽기 전용으로 데이터를 처리할 때 유용
3. 차이점 비교
특징 | <T extends Number> | <? extends Number> |
타입 변수 사용 여부 | T라는 타입 변수를 선언하여 내부에서 사용 가능 | 타입 변수 선언 없이 ?로 표현 |
적용 범위 | 제네릭 클래스 또는 메서드에 사용 가능 | 제네릭 메서드의 매개변수에 주로 사용 |
읽기/쓰기 | 읽기/쓰기 모두 가능(T를 통해 추가 가능) | 읽기만 가능(리스트에 추가 불가) |
데이터 추가 | 가능 ex: setItem(T item) |
불가능 컴파일러가 타입을 알 수 없기 때문 |
유연성 | 특정 타입으로 제한된 작업 가능 (T 고정) | 다양한 타입의 데이터를 처리할 때 유연함 |
사용 사례 | 클래스나 메서드 내에서 타입 변수를 일관되게 사용해야 할 때 데이터의 읽기와 쓰기 작업을 모두 처리해야할 때 제네릭 클래스나 메서드를 설계할 때 |
제네릭 컬렉션을 처리하면서 읽기 작업만 필요할 때 다양한 타입의 데이터를 처리해야 하지만, 특정 타입에 고정되지 않아야 할 때 타입 안정성을 유지하면서 여러 제네릭 타입을 받아야 할 |
Generic 활용 예시
1. 데이터 변환을 위한 유틸리티 클래스
제네릭을 사용하여 범용적으로 사용할 수 있는 유틸리티 메서드를 작성할 수 있다.
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
Pair<String, Integer> pair = new Pair<>("Age", 30);
System.out.println(pair.getKey() + ": " + pair.getValue()); // 출력: Age: 30
2. 타입 제약을 통한 도메인 모델링
도메인에 맞는 타입 제약을 걸어 잘못된 데이터 사용을 방지할 수 있다.
public class Repository<T extends Entity> {
private List<T> entities = new ArrayList<>();
public void save(T entity) {
entities.add(entity);
}
public List<T> findAll() {
return entities;
}
}
abstract class Entity {
private Long id;
// 공통 속성 및 메서드
}
class User extends Entity {
private String name;
}
class Product extends Entity {
private String title;
}
Repository<User> userRepo = new Repository<>();
userRepo.save(new User());
// userRepo.save(new Product()); // 컴파일 오류
Generic의 장점과 주의사항
장점
- 타입 안정성
- 잘못된 타입 사용을 컴파일 시점에 방지
- 코드 재사용성
- 데이터 타입에 상관없이 동일한 코드 구조를 사용할 수 있다.
- 가독성 및 유지보수성 향상
- 강제 캐스팅이 줄어들어 코드가 간결해진다.
- 컴파일 시 타입 검사
- 런타임 오류를 줄이고, 안전한 코드를 작성할 수 있다.
주의사항
- Type Erasure
- 제네릭은 컴파일 시 타입 정보가 제거된다.
- 따라서 런타임에는 타입 정보를 확인할 수 없다.
- 기본 타입 사용 불가
- 제네릭은 객체 타입만 허용하므로 기본 타입(int, double 등)을 사용할 수 없다.
- 대신 래퍼 클래스(Wrapper Class)인 Integer, Double 등을 사용해야 한다.
'TIL (Today I Learned)' 카테고리의 다른 글
[Java] 람다(Lambda)와 스트림(Stream) API 개념 및 예제 (0) | 2025.01.07 |
---|---|
[TIL] Java Enum과 Generic 활용하여 계산기 만들기 (25-01-06) (1) | 2025.01.06 |
[Java] Enum 사용법과 활용 예시 (0) | 2025.01.06 |
[TIL] Java 계산기 만들기, 계산기 예외처리 과제 / Java Error와 Exception, Generic (25-01-03) (2) | 2025.01.03 |
[TIL] Java-객체지향 프로그래밍, 계산기 만들기 (25-01-02) (0) | 2025.01.02 |