제네릭(Generics) 은 자바 프로그래밍 언어에서 코드의 재사용성과 타입 안전성을 높이는 데 중요한 역할을 한다. 제네릭 을 사용하면 클래스, 인터페이스, 메서드를 다양한 타입으로 동작하게 할 수 있으며, 컴파일 시 타입 검사를 통해 오류를 사전에 방지할 수 있다. 이번 글에서는 제네릭 의 개념과 필요성, 기본적인 사용법, 그리고 주의사항에 대해 자세히 알아보려고 한다.
제네릭이 도입되기 전에는 컬렉션 프레임워크와 같은 데이터 구조를 사용할 때 Object 타입을 이용하여 모든 종류의 객체를 저장하고 관리하였다. 예를 들어, ArrayList 는 다양한 타입의 객체를 저장할 수 있었지만, 이를 꺼내 사용할 때마다 명시적 타입 캐스팅이 필요했다. 이는 다음과 같은 문제점을 야기한다.
이러한 문제를 해결하기 위해 Java 5 에서 제네릭이 도입되었다. 제네릭을 사용하면 컴파일 시 타입 검사를 통해 잘못된 타입 사용을 방지할 수 있으며, 명시적 타입 캐스팅이 필요 없어 코드가 간결해진다.
제네릭 은 클래스, 인터페이스, 메서드에 타입 매개변수를 사용하는 것이다. 이를 통해 다양한 타입에 대해 유연하게 동작하는 코드를 작성할 수 있다. 제네릭의 기본적인 문법은 다음과 같다.
다음은 제네릭 클래스의 예제이다.
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
위 예제에서 Box 클래스는 타입 매개변수 T 를 사용하여 정의되었다. 이를 통해 Box 클래스는 다양한 타입으로 사용할 수 있다.
다음은 제네릭 메서드의 예제이다.
public class Util {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
위 예제에서 printArray 메서드는 타입 매개변수 T 를 사용하여 정의되었다. 이를 통해 다양한 타입의 배열을 출력할 수 있다.
제네릭 을 사용하는 클래스와 메서드는 여러 가지 형태로 구현될 수 있다. 다음은 제네릭 클래스와 메서드를 더 자세히 살펴보자.
제네릭 클래스를 사용하면 다양한 타입을 처리할 수 있는 클래스를 작성할 수 있다. 예를 들어, Pair 클래스를 정의하여 두 개의 객체를 저장할 수 있다.
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 클래스는 두 개의 타입 매개변수 K 와 V 를 사용하여 키와 값을 저장할 수 있다.
제네릭 메서드는 메서드에 타입 매개변수를 사용하여 정의한다. 예를 들어, 두 개의 값을 교환하는 메서드를 정의할 수 있다.
public class SwapUtil {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
위 예제에서 swap 메서드는 타입 매개변수 T 를 사용하여 배열의 두 요소를 교환한다.
제네릭은 강력한 기능이지만 몇 가지 한계와 주의사항이 있다. 이를 이해하고 사용하는 것이 중요하다.
Java 에서 제네릭은 타입 소거(Type Erasure)를 사용하여 구현된다. 컴파일 시 제네릭 타입은 Object 로 변환되고, 타입 검사와 변환이 제거된다. 이로 인해 런타임에는 제네릭 타입 정보가 사라지게 된다. 따라서 다음과 같은 한계가 있다:
instanceof 연산자나 getClass 메서드를 사용할 수 없다.T[] array = new T[10]; 와 같은 코드는 컴파일 오류가 발생한다.제네릭 을 사용할 때 상속 관계에 주의해야 한다. 예를 들어, List<String> 은 List<Object> 의 하위타입이 아니다. 이는 제네릭 의 타입 안전성을 보장하기 위함이다. 다음과 같은 코드는 컴파일 오류가 발생한다.
List<String> stringList = new ArrayList<>();
List<Object> objectList = stringList; // 컴파일 오류
제네릭을 사용할 때 와일드카드(?) 를 사용하여 다양한 타입을 유연하게 처리할 수 있다. 와일드카드 는 세 가지 종류가 있다:
<?> 는 어떤 타입이든 허용한다.<? extends T> 는 T 와 그 하위 타입을 허용한다.<? super T> 는 T 와 그 상위 타입을 허용한다.예를 들어, 다음과 같이 와일드카드 를 사용할 수 있다.
public void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
위 예제에서 printList 메서드는 어떤 타입의 리스트든 인자로 받을 수 있다.
제네릭은 자바 프로그래밍 언어에서 코드의 재사용성과 타입 안전성을 높이는 데 중요한 역할을 한다. 제네릭을 사용하면 다양한 타입에 대해 유연하게 동작하는 코드를 작성할 수 있으며, 컴파일 시 타입 검사를 통해 오류를 사전에 방지할 수 있을 것이라 생각한다.