[Java] Generic의 원리와 활용법

2025. 1. 6. 14:59·TIL (Today I Learned)

Generic

Java의 제네릭(Generic)은 데이터 타입을 일반화하여 코드 재사용성을 높이고 타입 안정성을 보장하는 기능이다.

Generic은 일반적(generalized)이라는 뜻으로, 다양한 데이터 타입에서 동작하도록 설계된 클래스나 메서드를 작성할 수 있다.

 

Generic의 기본 원리

Java 1.5 이전에는 타입을 지정하지 않고 Object를 사용하여 모든 데이터를 처리했다.

List list = new ArrayList();
list.add("Hello");
list.add(123); // 서로 다른 타입 추가 가능

String str = (String) list.get(0); // 강제 캐스팅 필요

 

💥문제점

  1. 강제 캐스팅 필요 : 반환 값을 원하는 타입으로 변환
  2. 타입 안정성 부족 : 잘못된 타입이 추가되더라도 컴파일러가 검출하지 못함

 

위와 같은 문제점을  Generic으로 해결했다.

Java의 제네릭은 컴파일 시점에 타입을 검사한다.

List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // 컴파일 오류 발생

String str = list.get(0); // 강제 캐스팅 불필요

Generic의 주요 개념

1. Generic 클래스

데이터 타입에 의존하지 않는 클래스 설계가 가능

// T는 Type의 약자
public class Box<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println(stringBox.getItem()); // 출력: Hello

Box<Integer> intBox = new Box<>();
intBox.setItem(123);
System.out.println(intBox.getItem()); // 출력: 123

 

2. Generic 메서드

메서드에만 제네릭을 적용할 수도 있다.

public class Utils {
    // T는 메서드에만 적용됨
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.println(element);
        }
    }
}
String[] strings = {"A", "B", "C"};
Utils.printArray(strings); // 출력: A, B, C

Integer[] numbers = {1, 2, 3};
Utils.printArray(numbers); // 출력: 1, 2, 3

 

3. Bounded Type Parameters

제네릭 타입에 제약 조건을 설정할 수 있다.

public class NumberBox<T extends Number> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }
}
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setItem(123); // OK

// NumberBox<String> strBox = new NumberBox<>(); // 컴파일 오류
  • <T extends Number> : T는 Number 또는 그 하위 클래스(Integer, Double 등)만 가능
  • <T extends Enum<T>> : T는 Enum 또는 그 하위 타입만 가능

 

4. Wildcards: ?

제네릭을 더 유연하게 사용하기 위해 와일드카드를 제공.

와일드카드는 제네릭 타입을 제한 없이 나타내기 위해 사용하는 기호이다.

?는 "어떤 타입인지 모름"을 의미하며, 다음과 같은 경우에 유용하다.

1. 특정 타입을 정확히 알 필요가 없을 때
2. 여러 제네릭 타입을 다룰 때

 

  • ? (Unbounded Wildcard) : 어떤 타입이든 받을 수 있다. (제한 없음)
public void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}
List<String> stringList = List.of("A", "B", "C");
List<Integer> intList = List.of(1, 2, 3);

printList(stringList); // 출력: A, B, C
printList(intList);    // 출력: 1, 2, 3
  •  장점
    • String, Integer 등 다양한 타입의 리스트를 한 메서드에서 처리할 수 있다.
  • 제약
    • 리스트 내부의 타입을 알 수 없으므로, 요소를 추가하거나 수정할 수 없다.
    • ex) list.add()는 사용 불가

 

  • ? extends T (Upper Bounded Wildcard) : T 또는 그 하위 타입만 허용, 읽기 전용으로 활용됨
public void printNumbers(List<? extends Number> list) {
    for (Number number : list) {
        System.out.println(number);
    }
}
List<Integer> intList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);

printNumbers(intList);    // 출력: 1, 2, 3
printNumbers(doubleList); // 출력: 1.1, 2.2, 3.3
  •  장점
    • Number의 하위 타입을 모두 처리할 수 있다.
  • 제약
    • 리스트에 새로운 요소를 추가할 수 없다.
    • ex) list.add(5)

 

  • ? super T (Lower Bounded Wildcard) : T 또는 그 상위 타입만 허용, 쓰기 전용으로 활용됨
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);

System.out.println(numberList); // 출력: [1, 2, 3]
  • 장점
    • Integer 뿐만 아니라 Number, Object 같은 상위 타입 리스트에도 요소를 추가할 수 있다.
  • 제약
    • 리스트에서 요소를 읽으면 타입이 Object로 반환되므로, 구체적인 타입을 알 수 없다.

 

✏️ Wildcard 비교 요약

Wildcard 설명 읽기 쓰기
? 모든 타입 허용 ✅ ❌
? extends T T와 그 하위 타입만 허용 ✅ ❌
? super T T와 그 상위 타입만 허용 ❌ ✅

<T extends Number>와 <? extends Number>

내가 헷갈려서 추가 정리하는 <T extends Number>와 <? extends Number> 차이점

 

1. <T extends Number>

T라는 고유한 타입 변수를 선언하여, 제네릭 클래스나 메서드 내에서 일관되게 사용할 수 있다.

메서드나 필드에서 T 타입으로 데이터를 다룰 수 있다.

 

2. <? extends Number>

제네릭 타입을 제한하지만, 구체적인 타입 변수를 선언하지는 않는다.

?는 불특정 타입을 나타내며, 그 타입이 Number 또는 그 하위 클래스임을 보장

읽기 전용으로 데이터를 처리할 때 유용

 

3. 차이점 비교

특징 <T extends Number> <? extends Number>
타입 변수 사용 여부 T라는 타입 변수를 선언하여 내부에서 사용 가능 타입 변수 선언 없이 ?로 표현
적용 범위 제네릭 클래스 또는 메서드에 사용 가능 제네릭 메서드의 매개변수에 주로 사용
읽기/쓰기 읽기/쓰기 모두 가능(T를 통해 추가 가능) 읽기만 가능(리스트에 추가 불가)
데이터 추가 가능
ex: setItem(T item)
불가능
컴파일러가 타입을 알 수 없기 때문
유연성 특정 타입으로 제한된 작업 가능 (T 고정) 다양한 타입의 데이터를 처리할 때 유연함
사용 사례 클래스나 메서드 내에서 타입 변수를 일관되게 사용해야 할 때

데이터의 읽기와 쓰기 작업을 모두 처리해야할 때

제네릭 클래스나 메서드를 설계할 때
제네릭 컬렉션을 처리하면서 읽기 작업만 필요할 때

다양한 타입의 데이터를 처리해야 하지만, 특정 타입에 고정되지 않아야 할 때

타입 안정성을 유지하면서 여러 제네릭 타입을 받아야 할 

Generic 활용 예시

1. 데이터 변환을 위한 유틸리티 클래스

제네릭을 사용하여 범용적으로 사용할 수 있는 유틸리티 메서드를 작성할 수 있다.

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<String, Integer> pair = new Pair<>("Age", 30);
System.out.println(pair.getKey() + ": " + pair.getValue()); // 출력: Age: 30

 

2. 타입 제약을 통한 도메인 모델링

도메인에 맞는 타입 제약을 걸어 잘못된 데이터 사용을 방지할 수 있다.

public class Repository<T extends Entity> {
    private List<T> entities = new ArrayList<>();

    public void save(T entity) {
        entities.add(entity);
    }

    public List<T> findAll() {
        return entities;
    }
}

abstract class Entity {
    private Long id;
    // 공통 속성 및 메서드
}

class User extends Entity {
    private String name;
}

class Product extends Entity {
    private String title;
}
Repository<User> userRepo = new Repository<>();
userRepo.save(new User());
// userRepo.save(new Product()); // 컴파일 오류

Generic의 장점과 주의사항

장점

  • 타입 안정성
    • 잘못된 타입 사용을 컴파일 시점에 방지
  • 코드 재사용성
    • 데이터 타입에 상관없이 동일한 코드 구조를 사용할 수 있다.
  • 가독성 및 유지보수성 향상
    • 강제 캐스팅이 줄어들어 코드가 간결해진다.
  • 컴파일 시 타입 검사
    • 런타임 오류를 줄이고, 안전한 코드를 작성할 수 있다.

 

주의사항

  • Type Erasure
    • 제네릭은 컴파일 시 타입 정보가 제거된다.
    • 따라서 런타임에는 타입 정보를 확인할 수 없다.
  • 기본 타입 사용 불가
    • 제네릭은 객체 타입만 허용하므로 기본 타입(int, double 등)을 사용할 수 없다.
    • 대신 래퍼 클래스(Wrapper Class)인 Integer, Double 등을 사용해야 한다.
저작자표시 비영리 변경금지 (새창열림)

'TIL (Today I Learned)' 카테고리의 다른 글

[Java] 람다(Lambda)와 스트림(Stream) API 개념 및 예제  (0) 2025.01.07
[TIL] Java Enum과 Generic 활용하여 계산기 만들기 (25-01-06)  (1) 2025.01.06
[Java] Enum 사용법과 활용 예시  (0) 2025.01.06
[TIL] Java 계산기 만들기, 계산기 예외처리 과제 / Java Error와 Exception, Generic (25-01-03)  (2) 2025.01.03
[TIL] Java-객체지향 프로그래밍, 계산기 만들기 (25-01-02)  (0) 2025.01.02
'TIL (Today I Learned)' 카테고리의 다른 글
  • [Java] 람다(Lambda)와 스트림(Stream) API 개념 및 예제
  • [TIL] Java Enum과 Generic 활용하여 계산기 만들기 (25-01-06)
  • [Java] Enum 사용법과 활용 예시
  • [TIL] Java 계산기 만들기, 계산기 예외처리 과제 / Java Error와 Exception, Generic (25-01-03)
기만나🐸
기만나🐸
공부한 내용을 기록합시다 🔥🔥🔥
  • 기만나🐸
    기만나의 공부 기록 🤓
    기만나🐸
  • 전체
    오늘
    어제
    • ALL (147)
      • TIL (Today I Learned) (56)
      • Dev Projects (15)
      • Algorithm Solving (67)
        • Java (52)
        • SQL (15)
      • Certifications (8)
        • 정보처리기사 실기 (8)
  • 인기 글

  • 태그

    websocket
    greedy
    BFS
    자료구조
    GROUP BY
    java
    CSS
    bootstrap
    HTML
    백트래킹
    jQuery
    jpa
    mysql
    Google Fonts
    join
    javascript
    Firebase
    dp
    백준
    Subquery
    완전탐색
    프로그래머스
    BOJ
    sql
    jwt
    그리디
    다이나믹프로그래밍
    programmers
    시뮬레이션
    DFS
  • 최근 글

  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
기만나🐸
[Java] Generic의 원리와 활용법
상단으로

티스토리툴바