주의

  • 본 게시글은 Claude로 작성되었습니다. 잘못된 정보가 있을 수 있습니다.

개요

자바 제네릭스(Generics)는 Java 5부터 도입된 강력한 기능으로, 컬렉션 등의 클래스에서 다양한 타입을 안전하게 사용할 수 있게 해줍니다. 이 문서에서는 제네릭스의 기본 개념부터 실제 사용법까지 초보자도 쉽게 이해할 수 있도록 상세히 설명합니다.

제네릭스란?

제네릭스는 클래스나 메서드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법입니다. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:

  1. 타입 안정성 향상
  2. 코드 재사용성 증가
  3. 타입 캐스팅 감소

제네릭스 사용 전과 후 비교

제네릭스를 사용하기 전:

ArrayList list = new ArrayList();
list.add("문자열");
list.add(100);  // 정수도 추가 가능
 
String str = (String) list.get(0);  // 타입 캐스팅 필요
Integer num = (Integer) list.get(1);  // 타입 캐스팅 필요

제네릭스 사용 후:

ArrayList<String> list = new ArrayList<>();
list.add("문자열");
// list.add(100);  // 컴파일 에러 발생
 
String str = list.get(0);  // 타입 캐스팅 불필요

제네릭스 상세 설명

제네릭 클래스 정의

제네릭 클래스는 다음과 같이 정의합니다:

public class Box<T> {
    private T content;
 
    public void set(T content) {
        this.content = content;
    }
 
    public T get() {
        return content;
    }
}

여기서 T는 타입 파라미터로, 실제 사용 시 구체적인 타입으로 대체됩니다.

제네릭 클래스 사용

정의한 제네릭 클래스는 다음과 같이 사용합니다:

Box<String> stringBox = new Box<>();
stringBox.set("안녕하세요");
String greeting = stringBox.get();  // 타입 캐스팅 불필요
 
Box<Integer> integerBox = new Box<>();
integerBox.set(100);
int number = integerBox.get();  // 타입 캐스팅 불필요

제네릭 메서드

제네릭은 메서드 레벨에서도 사용할 수 있습니다:

public static <E> void printArray(E[] array) {
    for (E element : array) {
        System.out.print(element + " ");
    }
    System.out.println();
}

사용 예:

String[] stringArray = {"Hello", "World"};
Integer[] integerArray = {1, 2, 3, 4, 5};
 
printArray(stringArray);  // 출력: Hello World
printArray(integerArray);  // 출력: 1 2 3 4 5

와일드카드

와일드카드(?)를 사용하면 알 수 없는 타입을 표현할 수 있습니다:

public static void printList(List<?> list) {
    for (Object elem : list) {
        System.out.print(elem + " ");
    }
    System.out.println();
}

사용 예:

List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("Hello", "World");
 
printList(intList);  // 출력: 1 2 3
printList(strList);  // 출력: Hello World

사용 예제

제네릭을 활용한 간단한 스택 구현

public class Stack<E> {
    private ArrayList<E> elements;
 
    public Stack() {
        this.elements = new ArrayList<>();
    }
 
    public void push(E item) {
        elements.add(item);
    }
 
    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }
 
    public boolean isEmpty() {
        return elements.isEmpty();
    }
}

사용 예:

Stack<String> stringStack = new Stack<>();
stringStack.push("첫 번째");
stringStack.push("두 번째");
System.out.println(stringStack.pop());  // 출력: 두 번째
System.out.println(stringStack.pop());  // 출력: 첫 번째
 
Stack<Integer> intStack = new Stack<>();
intStack.push(1);
intStack.push(2);
System.out.println(intStack.pop());  // 출력: 2
System.out.println(intStack.pop());  // 출력: 1

참고 자료

FAQ

Q: 제네릭스를 사용하는 주된 이유는 무엇인가요?

  • 제네릭스를 사용하면 컴파일 시점에 타입 안정성을 보장할 수 있습니다. 이는 런타임 에러를 줄이고, 코드의 가독성과 재사용성을 높여줍니다.

Q: 제네릭스의 타입 파라미터로 기본 타입(int, double 등)을 사용할 수 있나요?

  • 아니요, 제네릭스의 타입 파라미터로는 참조 타입만 사용할 수 있습니다. 기본 타입을 사용하려면 해당 타입의 래퍼 클래스(Integer, Double 등)를 사용해야 합니다.

Q: 제네릭 메서드와 제네릭 클래스의 차이점은 무엇인가요?

  • 제네릭 클래스는 클래스 전체에 대해 타입 파라미터를 적용하지만, 제네릭 메서드는 해당 메서드에 대해서만 타입 파라미터를 적용합니다. 제네릭 메서드는 non-generic 클래스 내에서도 사용할 수 있습니다.

관련 질문 및 추가 정보

  • 제네릭스와 상속: 제네릭 타입의 상속 관계에 대해 더 알아보세요.
  • 타입 경계(Bounded Type Parameters): 특정 타입의 서브클래스로 타입 파라미터를 제한하는 방법을 학습하세요.
  • 타입 추론: Java 7 이후 도입된 다이아몬드 연산자 <>를 통한 타입 추론에 대해 알아보세요.
  • 제네릭스의 타입 소거(Type Erasure): 컴파일 후 제네릭 정보가 어떻게 처리되는지 이해하세요.
  • 제네릭스와 컬렉션 프레임워크: Java의 컬렉션 프레임워크에서 제네릭스가 어떻게 활용되는지 살펴보세요.