제네릭(Generic)
제네릭은 클래스 내부가 아닌 외부에서 사용자에 의해 지정되는 타입을 의미한다.
제네릭을 사용하는 이유는
- 클래스의 재사용성이 높아지며,
- 컴파일 과정에서 잘못된 타입이 사용되는 것을 방지할 수 있다.
제네릭을 쓰지 않는다면, Object 타입으로 클래스나 메소드를 선언하고
객체를 생성할 때 원하는 타입으로 캐스팅을 해야 한다.. (귀찮..😮💨)
제네릭 타입 파라미터
제네릭은 추후 지정하게 될 타입을 위해 <> 모양 안에 아래 타입을 선언해주어야 한다.
- <T>: Type, 일반적으로 쓰이는 타입 파라미터
- <E>: Element, List 내 요소에 주로 사용한다.
- <K>: Key, Map 형식의 키에 주로 사용한다.
- <V>: Value, Map 형식의 값에 주로 사용한다.
- <N>: Number, Integer / Double / Long 에 주로 사용한다.
제네릭의 특징
- 제네릭으로 선언한 후, 참조 타입(Reference Type)으로만 지정할 수 있다.
즉, 기본 자료형(Primitive Type)이 아닌 Integer, Double 같은 Wrapper 클래스를 지정해야 한다.
예시) ❌ List<int> list = new ArrayList<>();
⭕️ List<Integer> list = new ArrayList<>(); - static 변수는 제네릭을 사용할 수 없다.
static 변수는 클래스 변수로, 메소드 영역에 저장되어 모든 인스턴스가 값을 공유한다.
따라서 공유되는 변수의 자료형이 달라질 수 없기 때문에, 제네릭을 사용할 수 없다. - static 메소드는 제네릭을 사용할 수 있다.
static 제네릭 메소드는 호출 시점에 타입을 지정한 후, 클래스 전체에서 static 제네릭 메소드를 공유한다.
(마치 kotlin의 lazy와 유사하다!)
제네릭 사용 방법
이제 직접 제네릭을 사용하는 방법을 알아보자.
제네릭 클래스
다음은 클래스 A 내에 T를 Integer로 지정한 예제이다.
Integer 뿐만 아니라 모든 참조 타입을 넣을 수 있다.
class A<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class Generic {
public static void main (String[] args) {
A<Integer> a = new A<>();
a.setValue(3);
System.out.println(a.getValue());
}
}
제네릭 인터페이스
인터페이스 A를 implements한 AList는 A의 E 타입을 그대로 가져와 지정한다.
String 타입으로 지정된 aList 객체는 String 값을 추가·제거 및 조회할 수 있다.
interface A<E> {
void addE (E e);
void removeE (E e);
E getE();
}
class AList<T> implements A<E> {
private List<E> list = new ArrayList<>();
@Override
public void addE(E e) {
list.add(e);
}
@Override
public void removeE(E e) {
list.remove(e);
}
@Override
public E getE() {
if (list.isEmpty()) return null;
return list.get(0);
}
}
public class Generic {
public static void main (String[] args) {
AList<String> aList = new AList<>();
aList.addE("Apple");
aList.addE("Banana");
System.out.println(aList.getE()); // Apple
aList.removeE("Apple");
System.out.println(aList.getE()); // Banana
}
}
제네릭 메소드
메소드 자체에서 타입을 정의하므로, 클래스가 제네릭이 아니어도 사용이 가능하다.
다음은 "Hi"인 String과 42인 Integer로 타입을 지정해 조회한 예제이다.
class Util {
public static <T> T identity(T value) {
return value;
}
}
public class Generic {
public static void main (String[] args) {
String s = Util.identity("Hi");
Integer i = Util.identity(42);
System.out.println(s); // Hi
System.out.println(i); // 42
}
}
제네릭 와일드카드
제네릭 타입 파라미터는 지정할 수 있는 대상을 모든 참조 타입이 아닌 특정 대상으로 제한할 수 있다.
제한할 수 있는 방법은 총 3가지로, 상한 경계 / 하한 경계 / 와일드카드이다.
상한 경계(extends)
상한 경계는 제네릭 선언 시 <T extends E> 형식으로 표현한다.
이는 E와 E를 상속받는 자식 클래스 타입만 지정할 수 있다는 의미이다.
제네릭 클래스를 예로 들어보자.
class A <T extends Number> {}
public class Generic {
public static void main (String[] args) {
A<Integer> a = new A<>();
A<String> a2 = new A<>(); // error
}
}
T는 Number 또는 Number의 자식 클래스만 가능하다.
따라서 a2는 컴파일 에러가 발생한다!
하한 경계(super)
하한 경계는 제네릭 선언 시 <? super E> 형식으로 표현한다.
이는 E와 E의 부모 클래스 타입만 지정할 수 있다는 의미이다.
제네릭 클래스를 예로 들어보자.
class AParent {
public void printParent() {
System.out.println("부모 클래스");
}
}
class AChild extends AParent {
public void printChild() {
System.out.println("자식 클래스");
}
}
public class Generic {
public static void addChild(List<? super AChild> list) {
list.add(new AChild());
}
public static void main(String[] args) {
List<AParent> parentList = new ArrayList<>();
List<AChild> childList = new ArrayList<>();
addChild(parentList);
addChild(childList);
}
}
AParent를 상속받는 AChild를 super의 대상으로 선언하면, AChild와 그 부모 클래스에 한해 타입을 지정할 수 있다!
와일드카드(?)
와일드카드<?>는 <? extends Object>와 동일하다.
즉, 모든 타입이 가능하다!
이쯤되면, <?>와 <T>는 같은 것 아닌가요? 라고 할 수 있다.
하지만 둘은 다르다....
<T>는 클래스를 선언하며 타입을 정의할 때 쓰고, <?>는 타입을 지정할 때 쓴다.
따라서 <T>는 자료형이 결정되는 순간, 읽기/쓰기가 모두 가능하다.
<?>는 ?, extends, super를 이용해 읽기가 가능하다.
또한 ?에서는 null만 + super를 이용해 쓰기가 가능하다.
class A<T> { ... }
public class Generic {
public static void main (String[] args) {
A<?> a1 = new A<String>();
Object value = a1.getValue();
System.out.println(value);
}
}
위 예시는 Object로 getValue()를 받아 value를 출력하였다! (물론 value는 null이다.)
마무리
지금까지 제네릭(Generic)에 대해서 알아보았다.
개발하면서 제네릭을 직접 정의해본 적은 없지만,
모두들 List 같은 프레임워크를 쓰면서 이미 제네릭을 활용하고 있었을 것이다!
희미했던 개념을 들춰서 들여다본 기분 ..
👏
참고
'Java' 카테고리의 다른 글
| [Java] Reflection (0) | 2025.10.23 |
|---|---|
| [Java] 익명 클래스, 람다식 (0) | 2025.10.14 |
| [Java] 인터페이스 vs 추상 클래스 (0) | 2025.10.03 |
| [Java] final vs static vs static final (0) | 2025.09.22 |
| [Java] JVM(Java Virtual Machine) (1) | 2025.08.30 |