제네릭
자바에서 제네릭(Generic) 은 "박스" 라고 생각하면 된다.
박스를 만들었는데, 안에 어떤 물건을 넣을지 모를 경우, 박스를 미리 만들어두고, 나중에 어떤 물건이든 넣을 수 있게 하면 된다.
이처럼 제네릭 역시 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
이는 객체별로 다른 타입의 자료가 저장될 수 있도록 해준다.
제네릭이란
말로 들으면 뭔지 이해가 잘 안가기 때문에 코드로 알아보자.
ArrayList<String> list = new ArrayList<>();
위는 자바에서 자주 사용하는 자료형이 리스트다.
다음과 같이 클래스 선언 문법에 꺾쇠 괄호 <> 로 되어있는 코드 형태가 바로 제네릭이다.
즉, 제네릭(Generic)은 배열의 타입을 지정하는 것처럼 리스트(ArrayList), 맵(HashMap) 같은 컬렉션 클래스나 메소드에서 사용할 내부 데이터 타입을 외부에서 지정하는 기능이다.
즉, "타입을 변수처럼 다룬다" 라고 생각하면 된다.
타입 매개변수
그렇다면 제네릭에 들어가는 타입형은 무엇이 있을까?
기본적으로 들어갈 수 있는 타입 매개변수는 아래와 같다.
타입 매개변수 (에 들어가는 값) | 예제 | 설명 |
기본 타입의 래퍼 클래스 | FruitBox<Integer> |
int 대신 Integer 사용 |
문자열 타입 | FruitBox<Double> |
double 대신 Double 사용 |
문자열 타입 | FruitBox<String> |
String 타입 사용 |
사용자 정의 클래스 | FruitBox<Apple> |
Apple 클래스를 타입으로 사용 |
인터페이스 타입 | FruitBox<Fruit> |
Fruit 인터페이스 사용 가능 |
와일드카드 | FruitBox<?> |
어떤 타입이든 올 수 있음 |
타입 매개변수 할당 가능 타입
제네릭에서 할당 받을 수 있는 타입은 객체 뿐이다.
제네릭은 클래스만 사용할 수 있도록 설계되어 있기 때문에, int
형 이나 double
형 같은 자바 원시 타입을 제네릭 타입 파라미터로 넘길 수 없다.
때문에 이를 위해 Integer
, Double
, Character
와 같은 래퍼 클래스를 사용해야만 한다.
또한, 제네릭은 런타임에는 타입 정보를 지우는 방식을 사용하기 때문에, 컴파일 타임에만 존재하고 런타임에는 사라진다.
예를 들자면 아래와 같다.
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
위 코드는 컴파일 할 경우 아래 코드와 같이 타입이 지워지면서 T
가 Object
로 변환된다.
public class Box {
private Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
하지만, int
는 Objet
가 아니기 때문에 형 변환이 불가능하다.
이와 같은 이유 때문에 원시 타입은 제네릭에서 사용이 불가능하다.
제네릭의 중복제거
매개변수에는 반드시 변수형이 들어갈 필요는 없다.
class FruitBox<T> {
List<T> fruits = new ArrayList<>();
public void add(T fruit) {
fruits.add(fruit);
}
}
위 코드를 보면 매개변수에 T
가 들어간것을 볼 수 있다.
<T>
는 타입을 변수처럼 다룰 수 있도록 하는 제네릭 타입 매개변수라고 보면 된다.
역시 글로만 보면 이해가 안가니 코드로 자세하게 설명하겠다.
만약<T>
를 사용하지 않는다면 아래와 같이 코드를 작성해야 한다.
class AppleBox {
List<Apple> fruits = new ArrayList<>();
public void add(Apple fruit) {
fruits.add(fruit);
}
}
class BananaBox {
List<Banana> fruits = new ArrayList<>();
public void add(Banana fruit) {
fruits.add(fruit);
}
}
이는 가 어떤 타입을 저장할지 나중에 결정할 수 있도록 만드는 역할을 해준다는것을 알 수 있게 해준다.
즉, T
는 타입을 일반화(Generalization)하는 역할을 하고, FruitBox<T>
를 선언할 때 T
의 실제 타입을 지정할 수 있다는 소리다.
T
를 사용한다면 아래와 같이 코드를 작성할 수 있다.
FruitBox<Apple> appleBox = new FruitBox<>(); // Apple만 저장 가능
appleBox.add(new Apple());
FruitBox<Banana> bananaBox = new FruitBox<>(); // Banana만 저장 가능
bananaBox.add(new Banana());
이는 객체를 따로 선언할 필요 없이, 하나의 클래스만으로 여러 타입을 사용할 수 있다는 뜻이다.
복수 타입 파라미터
제네릭에서 타입 지정이 여러개가 필요할 경우 얼마든지 만들 수 있다.
제네릭 타입의 구분은 꺽쇠 괄호 안에서 쉽표(,)로 하며 <T, U>
와 같은 형식을 통해 복수 타입 매개변수를 지정할 수 있다.
또한, 클래스 초기화할때 제네릭 타입을 복수 타입 매개변수 만큼 넘겨주어야 한다.
import java.util.ArrayList;
import java.util.List;
class Apple {}
class Banana {}
class FruitBox<T, U> {
List<T> apples = new ArrayList<>();
List<U> bananas = new ArrayList<>();
public void add(T apple, U banana) {
apples.add(apple);
bananas.add(banana);
}
}
public class Main {
public static void main(String[] args) {
// 복수 제네릭 타입
FruitBox<Apple, Banana> box = new FruitBox<>();
box.add(new Apple(), new Banana());
box.add(new Apple(), new Banana());
}
}
중첩 타입 파라미터
제네릭 객체를 제네릭 타입 파라미터로 받는 형식도 표현할 수 있다.
ArrayList
자체도 하나의 타입으로써 제네릭 타입 파라미터가 될수 있기 때문에 이렇게 중첩 형식으로 사용할 수 있는 것이다.
public static void main(String[] args) {
// LinkedList<String>을 원소로서 저장하는 ArrayList
ArrayList<LinkedList<String>> list = new ArrayList<LinkedList<String>>();
LinkedList<String> node1 = new LinkedList<>();
node1.add("aa");
node1.add("bb");
LinkedList<String> node2 = new LinkedList<>();
node2.add("11");
node2.add("22");
list.add(node1);
list.add(node2);
System.out.println(list);
}
- 출력 결과
[[aa, bb]], [11, 22]
타입 매개변수 생략
jdk 1.7 버전 이후부터는, 아래처럼 new 생성자 부분의 제네릭 타입을 생략할 수 있게 되었다.
FruitBox<Apple> intBox = new FruitBox<>();
제네릭을 사용하면 얻는 이점
타입 안정성 보장
제네릭을 사용하면 컴파일 타임에 타입을 검사하기 때문에, 잘못된 타입이 들어가는것을 방지할 수 있다.
List list1 = new ArrayList(); // 제네릭을 사용하지 않음
list1.add("Hello");
list1.add(100); // 문자열 리스트에 정수를 추가 (컴파일 시 오류 없음)
List<String> list2 = new ArrayList<>(); // String만 저장 가능
list2.add("Hello");
list2.add(100); // 컴파일 오류 발생! (String 타입만 가능)
불필요한 형 변환 제거
자바에서 Object
는 모든 클래스의 부모 타입으로 여러 타입들을 저장할 수 있다.
하지만, 데이터를 꺼낼 때는 원래 타입으로 형 변환 해줘야 한다.
하지만 제네릭을 사용하면 컴파일러가 타입을 체크하고, 강제하기 때문에 데이터를 꺼낼 때 형 변환 없이 바로 사용 가능하다.
그 이유는 아래와 같다.
- 컴파일 타임에 타입을 체크
- 제네릭 사용시, 리스트에 저장 가능한 타입이 컴파일 타임에 결정
- 잘못된 타입이 들어가면 컴파일 오류 발생
List<Integer> numbers = new ArrayList<>();
numbers.add(100);
numbers.add(200);
// int num = (Integer) numbers.get(0); // 형 변환 필요 없음
int num = numbers.get(0); // 바로 사용 가능!
- 타입 안정성 보장
- 제네릭을 사용하면 다른 타입의 데이터를 저장하는 것 자체가 불가능하기 때문에 런타임 오류가 발생하지 않음
List<Integer> numbers = new ArrayList<>();
numbers.add(100);
numbers.add("Hello"); // 컴파일 오류! (Integer만 허용)
Integer num = numbers.get(0); // 형 변환 필요 없음!
System.out.println(num);
가독성 향상
제네릭을 사용하면 코드가 더 명확해지고, 타입을 직접 지정할 수 있어 유지보수가 쉬워진다.
- 제네릭을 사용하지 않은 코드
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);
- 제네릭을 사용한 코드
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0);
선언하는 부분만 봐도 list
가 String
타입의 데이터를 저장하는 리스트라는 것을 알 수 있다.
'자바' 카테고리의 다른 글
[JAVA] 람다식(Lambda) (0) | 2025.03.19 |
---|---|
[JAVA] 컬렉션 프레임워크(Collection Framework) (0) | 2025.03.18 |
[JAVA] 어노테이션 (Annotation) (0) | 2025.03.17 |
[JAVA] 문자열 처리와 관련된 클래스 (0) | 2025.03.14 |
[JAVA] 인터페이스(Interface) (0) | 2025.03.13 |