개요
제네릭은 자바에서 정말 밥먹듯이 사용되는 기술이다.
오늘은 이 제네릭에 대해 정리해 보자.
제네릭
클래스나 인터페이스 선언에 타입 매개변수가 필요하다면 제네릭 클래스나 제네릭 인터페이스라 한다.
주요 기술은 데이터 타입을 내부에서 미리 지정하지 않고, 객체를 생성할 때 외부에서 직접 타입을 결정할 수 있도록 해주는 기술이다.
즉, 제네릭을 사용하면 객체 별로 다른 타입을 저장할 수 있다.
말로는 이해가 어려우니 아래 예시 코드를 보도록 하자.
ArrayList<Integer> intList = new ArrayList<Integer>();
ArrayList<String> strList = new ArrayList<String>();
ArrayList<Double> doubleList = new ArrayList<Double>();
이처럼 ArrayList 객체에선 하나의 데이터 타입에 묶여있는 것이 아니다.
ArrayList 바로 옆에는 <데이터 타입>가 있는데 이 괄호를 제네릭이라고 한다.
ArrayList는 제네릭에 저장하고 싶은 데이터 타입을 명시하면 Integer를 관리할 수도 있고, String을 관리할 수도 있고, Double을 관리할 수도 있다.
타입 매개변수
제네릭 클래스를 사용해 타입을 지정할 때, 사용되는 것이 타입 매개변수이다.
제네릭이 타입을 지정하는 건 알겠는데, 매개변수가 명칭에 따로 붙은 이유는 마치 메서드의 매개변수로 입력하는 것처럼 사용되기 때문이다.
참고로 타입 매개변수에는 오직 참조형 타입만 사용 가능하다.
아래 코드는 ArrayList에 작성되어 있는 코드이다.
ArrayList는 제네릭 클래스이고, 타입 매개변수로 E를 사용한다. E의 의미는 Element로 타입 매개변수를 의미한다. (타입 매개변수로 E를 사용하는 건 관례적인 것임! 다른 알파벳일 수도 있음)
객체가 생성되기 전까지 실제 타입이 결정되지 않은 상태이지만, 객체가 생성될 때, E의 타입은 결정되고, 해당 객체에선 선언된 타입 파라미터만 사용 가능하다.
그 이유는 클래스 내부에서 제네릭 전파가 진행되기 때문이다.
제네릭 전파란 클래스 내부에 작성된 타입 파라미터에 타입이 할당되는 것을 의미한다.
제네릭 전파 : E → String
public class ArrayList<E -> String> ...
public boolean add(E -> String ..) {
...
}
public class GenericExample {
public static void main(String[] args) {
ArrayList<String> strList = new ArrayList<>(); // E에서 String으로 제네릭 전파
strList.add("Hello");
strList.add("World");
ArrayList<Integer> intList = new ArrayList<>(); // E에서 Integer으로 제네릭 전파
intList.add(100);
intList.add(200);
}
}
제네릭 장점
제네릭의 가장 큰 장점은 유연함과 강제성이다.
유연함
만약 제네릭이 없을 경우, String을 저장하는 List, Integer를 저장하는 List, Double을 저장하는 List를 어떻게 사용할까?
아래의 코드와 같이 일일이 클래스를 만들어야 할 것이다.
// String만 저장할 수 있는 클래스
class StringList {
private List<String> list = new ArrayList<>();
public void add(String value) {
list.add(value);
}
public String get(int index) {
return list.get(index);
}
...
}
// Integer만 저장할 수 있는 클래스
class IntegerList {
private List<Integer> list = new ArrayList<>();
public void add(Integer value) {
list.add(value);
}
public Integer get(int index) {
return list.get(index);
}
...
}
// Double만 저장할 수 있는 클래스
class DoubleList {
private List<Double> list = new ArrayList<>();
public void add(Double value) {
list.add(value);
}
public Double get(int index) {
return list.get(index);
}
...
}
// 사용 예시
public class NoGenericsExample {
public static void main(String[] args) {
StringList stringList = new StringList();
stringList.add("Hello");
stringList.add("World");
System.out.println(stringList.get(0));
IntegerList integerList = new IntegerList();
integerList.add(100);
integerList.add(200);
System.out.println(integerList.get(1));
DoubleList doubleList = new DoubleList();
doubleList.add(10.5);
doubleList.add(20.5);
System.out.println(doubleList.get(0));
}
}
개발자 입장에서 봤을 땐, 코드 길이가 길어지고, 유지보수 측면에서도 최악이 될 것이다.
하지만 제네릭을 사용한다면 아래와 같이 코드가 훨씬 간결해진다.
// 제네릭을 사용한 List 클래스
class GenericList<T> {
private List<T> list = new ArrayList<>();
public void add(T value) {
list.add(value);
}
public T get(int index) {
return list.get(index);
}
...
}
// 사용 예시
public class GenericExample {
public static void main(String[] args) {
GenericList<String> stringList = new GenericList<>();
stringList.add("Hello");
stringList.add("World");
System.out.println(stringList.get(0));
GenericList<Integer> integerList = new GenericList<>();
integerList.add(100);
integerList.add(200);
System.out.println(integerList.get(1));
GenericList<Double> doubleList = new GenericList<>();
doubleList.add(10.5);
doubleList.add(20.5);
System.out.println(doubleList.get(0));
}
}
강제성
개발자에게 가장 무서운 건 런타임 시, 예상치 못한 예외가 발생하는 것이다.
대표적인 예시가 ClassCastException 가 발생하는 경우이다.
다음은 List에 제네릭을 사용하지 않은 예시 코드이다.
public class NoGenericTest {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add("Hello");
list.add(1);
for (Object obj : list) {
String str = (String) obj;
System.out.println(str);
}
}
}
컴파일은 정상적으로 통과된다.
하지만 코드를 실행시킬 경우 ClassCastException 예외가 발생한다.
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
List list에 있는 객체를 꺼낼 때, 캐스팅을 하는데, 이 과정에서 ClassCastException이 발생하는 것이다.
이러한 모든 상황이 우리가 제네릭을 사용하는 이유를 말해준다!
다음은 제네릭을 사용한 경우이다.
public class GenericTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Hello");
list.add(1);
list.add(true);
list.add(1.1);
}
}
타입 매개변수로 String을 선언했기 때문에 보다시피 String을 제외한 타입을 삽입하고자 할 때 컴파일 시점에서 예외가 잡힌다.
그렇기 때문에 프로그램 입장에선 ClassCastException 가 발생할 가능성을 현저하게 줄여, 더 안전해지는 것이다.
'개발 일기' 카테고리의 다른 글
[개발 일기] 2025.02.04 - 추상 클래스 vs 인터페이스 (1) | 2025.02.04 |
---|---|
[개발 일기] 2025.02.03 - bit, byte (Feat : byte의 혼란) (0) | 2025.02.03 |
[개발 일기] 2025.02.01 - 객체 지향 생활 체조 9가지 규칙 (2) (1) | 2025.02.01 |
[개발 일기] 2025.01.31 - LSTM (0) | 2025.01.31 |
[개발 일기] 2025.01.30 - 객체 지향 생활 체조 9가지 규칙 (1) (0) | 2025.01.30 |