Monday

5. Java 고급 문법 (1) Generics 본문

언어/Java

5. Java 고급 문법 (1) Generics

뉴비2 2024. 11. 10. 19:22

제네릭(Generics) 깊이 있게 파헤치기

제네릭을 왜 배워야 할까?

  1. 타입 안전성 보장
  2. 코드 재사용성 증가
  3. 컴파일 시점 오류 검출
  4. 명시적 타입 캐스팅 불필요

제네릭의 본질: 타입의 유연성과 안전성

제네릭이란 무엇인가?

제네릭은 "타입을 파라미터화하여 컴파일 시점에 타입 안전성을 보장하는 Java의 강력한 기능"입니다.

제네릭 탄생 배경

Java 5 이전:

// 제네릭 이전 코드
List list = new ArrayList();
list.add("문자열");
list.add(123);  // 어떤 타입이든 추가 가능

// 사용 시 강제 형변환 필요
String str = (String) list.get(0);  // 런타임 시 오류 위험

제네릭 도입 후:

// 제네릭 적용
List<String> list = new ArrayList<>();
list.add("문자열");
// list.add(123);  // 컴파일 에러 발생!

String str = list.get(0);  // 안전한 타입 사용

제네릭의 심화 개념

1. 타입 매개변수의 다양한 표기법

제네릭에서 타입 파라미터를 선언할 때는 일반적으로 다음과 같은 순서와 관례를 따릅니다:

타입 파라미터 선언 순서

  1. 단일 타입 파라미터
    • <T>: 가장 일반적으로 사용되는 타입 파라미터
    • 예: class Box<T> { ... }
  2. 두 개 이상의 타입 파라미터
    • <K, V>: 키-값 쌍에서 주로 사용
    • 예: class Map<K, V> { ... }
  3. 제네릭 메서드에서 타입 파라미터는 메서드의 반환 타입 앞에 꺾쇠 괄호(<>)로 선언합니다
  4. public static <E> void methodName(E[] parameter) { // 메서드 본문 }

일반적인 타입 파라미터 약속

기호 의미 주로 사용되는 곳
T Type 일반적인 타입
E Element 컬렉션 요소
K Key 맵의 키
V Value 맵의 값
N Number 숫자 타입
S, U, V 2번째, 3번째, 4번째 타입 복수 타입 사용 시

예시:

class Pair<K, V> {
    private K key;
    private V value;
}

class MultiType<T, U, V> {
    private T first;
    private U second;
    private V third;
}

2. 제네릭의 고급 사용법

바운디드 타입 파라미터

// Number의 하위 클래스만 허용
public class NumberBox<T extends Number> {
    private T number;

    public double sqrt() {
        return Math.sqrt(number.doubleValue());
    }
}

// 사용 예시
NumberBox<Integer> intBox = new NumberBox<>();  // 가능
NumberBox<String> strBox = new NumberBox<>();   // 컴파일 에러

와일드카드 활용

// 와일드카드의 3가지 형태
public class WildcardExample {
    // 모든 타입 허용
    public void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }

    // 특정 타입의 하위 클래스만 허용
    public void processNumbers(List<? extends Number> numbers) {
        // Number의 하위 클래스(Integer, Double 등)만 처리 가능
    }

    // 특정 타입의 상위 클래스만 허용
    public void addNumbers(List<? super Integer> list) {
        // Integer의 상위 클래스(Number, Object)만 처리 가능
    }
}

3. T와 ?의 기본적인 차이

T (Type Parameter)

  • 정의: 구체적인 타입을 지정할 수 있는 타입 파라미터
  • 특징:
    • 클래스나 메서드를 정의할 때 사용
    • 실제 타입으로 대체 가능
    • 사용자 정의 클래스도 타입으로 사용 가능

? (Wildcard)

  • 정의: 알 수 없는 타입을 나타내는 와일드카드
  • 특징:
    • 타입이 불분명할 때 사용
    • 제한된 유연성 제공
    • 읽기 전용으로 주로 사용

예제로 이해하기

T 사용 예제

// 사용자 정의 클래스를 T로 사용 가능
class Person {}
class Student extends Person {}

class GenericBox<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

// 사용
GenericBox<Person> personBox = new GenericBox<>();
GenericBox<Student> studentBox = new GenericBox<>();

? 사용 예제

class WildcardExample {
    // 모든 타입의 리스트를 출력
    public void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }

    // 특정 타입의 부모 클래스만 허용
    public void processPersonList(List<? extends Person> list) {
        // Person이나 Person의 자식 클래스 리스트만 처리 가능
        for (Person person : list) {
            // 처리 로직
        }
    }
}

주요 차이점

  1. 타입 지정

    • T: 구체적인 타입 지정 가능
    • ?: 타입 불분명, 제한된 접근
  2. 유연성

    • T: 타입 안전성 높음
    • ?: 더 유연한 타입 처리
  3. 메서드 사용

    • T: 해당 타입의 메서드 호출 가능
    • ?: Object 메서드만 호출 가능

4. 제네릭 메서드의 고급 패턴

public class GenericMethodExample {
    // 제네릭 메서드: 타입 추론
    public static <T> T getFirstElement(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // 복수 타입 파라미터
    public static <K, V> void printKeyValue(K key, V value) {
        System.out.println("Key: " + key + ", Value: " + value);
    }

    // 제한된 타입 파라미터
    public static <T extends Comparable<T>> T findMax(T a, T b) {
        return (a.compareTo(b) > 0) ? a : b;
    }
}

5. 제네릭의 제약사항

public class GenericLimitations<T> {
    // 불가능한 연산들
    // 1. 제네릭 타입으로 인스턴스 생성 불가
    // T instance = new T();  // 컴파일 에러

    // 2. 기본형 타입 사용 불가 (래퍼 클래스 사용)
    // GenericBox<int> box = new GenericBox<>();  // 컴파일 에러
    GenericBox<Integer> box = new GenericBox<>();  // 가능

    // 3. 정적 멤버에 제네릭 타입 사용 불가
    // private static T staticField;  // 컴파일 에러
}

제네릭은 단순한 문법이 아니라 타입 시스템을 더욱 강력하고 유연하게 만드는 프로그래밍 패러다임입니다.

복잡해 보이지만, 결국 "타입에 대한 안전한 추상화"를 제공하는 Java의 혁신적인 기능입니다.

Comments