본문 바로가기

Java

제네릭 특징

오랜만에 왔다... 

 

생각은 하는데 쓰는게 쉽지가 않네 머쓱..

 

오늘은 제네릭에 대해 알아보자.

public class Test<T> {} 

public class MyMap<K, V> {}

 

이런거 본 적 있을거다.

ArrayList<String> list = new ArrayList<>();

 

맞다. 리스트할 때 봤던 이녀석도 제네릭이다! 

 

제네릭은 쉽게 말해서 <> 부호 사이에 타입 파라미터가 위치한다.

 

제네릭이 뭐냐면 클래스, 인터페이스, 메서드에서 사용할 타입을 파라미터로 지정할 수 있게 해주는 기능이다. 제네릭을 사용하면 컴파일 타임에 타입을 체크할 수 있어 코드의 안전성과 가독성을 높일 수 있다. 

 

1. 캐스팅 제거

// 제네릭 사용 전
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0);

// 제네릭 사용 후
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0);  // 캐스팅 불필요

 

제네릭을 사용함으로써 런타임 오류를 줄일 수 있고 불필요한 타입 캐스팅을 제거하여 가독성과 유지보수를 높일 수 있다.

 

2. 코드 재사용성 증가

public class Box<T> {
    private T content;

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

    public T getContent() {
        return content;
    }
}

Box<String> stringBox = new Box<>();
Box<Integer> intBox = new Box<>();

 

타입을 지정하지 않았기 때문에 다양한 타입에 대해 재사용 가능한 범용 코드를 작성할 수 있다. 이는 코드 중복을 줄이고 유지보수성을 향상

 

3. 컴파일 타임 타입 체크

public <T> void add(T a, T b) {
    // 코드 내부에서 T 타입의 변수 사용
}

 

이게 무슨 소린가 싶은데 메서드가 호출될 떄 컴파일러가 전달된 인수의 타입을 보고 'T'의 실제 타입을 추론한다는 소리다.

public class GenericExample {

    public <T> void add(T a, T b) {
        // 여기서는 T 타입의 변수 a와 b를 사용할 수 있다.
    }

    public static void main(String[] args) {
        GenericExample example = new GenericExample();
        
        example.add(1, 2);  // 컴파일 타임에 T가 Integer로 치환됨
        example.add("Hello", "World");  // 컴파일 타임에 T가 String으로 치환됨
        // example.add(1, "World");  // 컴파일 오류: 서로 다른 타입은 허용되지 않음
    }
}

 

예를 들어 example.add(1, 2); 가 호출될 때 T는 Integer로 추론되고, example.add("Hello", "World");는 String으로 추론된다는 소리다.

 

4. 의도 명확성

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

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
}

 

제네릭을 사용하는 경우 타입 매개변수를 명시함으로써 클래스나 메서드가 다루는 데이터 타입을 구체적으로 정의할 수 있다. 이를 통해 코드의 가독성이 향상 된다.

 

이게 무슨 소리냐면 제네릭을 사용하면 객체를 선언할 때

Pair<String, Integer> pair = new Pair<>("Age", 30);

 

이런식으로 선언하기 때문에 String타입의 키와 Integer타입의 값을 가진다는 것을 명시적으로 나타낼 수 있다는 것이다.

 

여기서 또 봐야되는데 타입 매개변수는 T 뿐만 아니라 V , K 등 여러가지가 있는데 자주 사용되는 매개변수를 알아보자.

T Type - 주로 클래스, 메서드에서 임의의 타입을 나타낼 때 사용
E Element - 주로 컬렉션에서 사용되며 요소를 나타냄
K Key - 주로 Map에서 키를 나타냄
V Value - 주로 Map에서 값을 나타냄
N Number - 숫자를 나타냄
Result - 결과를 나타냄

 

마지막으로 제네릭 와일드 카드에 대해 알아보자.

 

코드를 보다보면 <?>라고 되어있는걸 볼 수 있다. 제네릭 타입을 좀 더 유연하게 사용하기 위해 도입된 기능인데 특정 타입을 대신에 사용할 수 있다.

 

1. 무제한 와일드 카드 

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

 

주로 인수로 사용할 때 어떤 타입이든 받아들이고자 사용된다.

 

2. 상한 경계 와일드 카드 

public void processNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num.doubleValue());
    }
}

 

특정 타입의 하위 타입만을 받을 수 있다. 주로 읽기 전용 메서드에 사용된다. Number 타입이나 그 하위 타입인 Integer,  Double, Float등을 받아들일 수 있다.

 

3. 하한 경계 와일드 카드

public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

 

특정 타입의 상위 타입만을 받아들일 수 있다. 주로 쓰기 전용 메서드에 쓰인다. 예시에서는 Integer 타입이나 그 상위 타입인 Number, Object타입을 받아들일 수 있다.

 

근데 나만 그냥 타입 매개변수와 와일드 카드의 차이를 모르겠는가..? 타입 매개변수도 여러 타입을 받아들일 수 있잖아..?

 

제네릭 타입 매개변수는 클래스나 메서드를 정의할 때 사용되고, 와일드카드는 클래스나 메서드를 사용할 때, 매개변수로 사용되며 구체적인 타입을 알 필요가 없거나 제한할 때 사용된다고 한다.

 

내가 이해하기론 와일드 카드는 이미 정의된 제네릭 타입을 다룰 때 사용된다. (아니면 말해주세요)

 

마지막 예시 

public class Box<T> {
    private T content;

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

    public T getContent() {
        return content;
    }
}

 

이렇게 정의된 제네릭 타입 클래스가 있으면

public void printBox(Box<?> box) {
    System.out.println(box.getContent());
}

 

이런 식으로 어떤 타입의 Box라도 받아들일 수 있지만 내부 타입을 알 필요가 없다는 것이다.

'Java' 카테고리의 다른 글

(spring) Exception 이녀석........  (0) 2024.06.18
Logger 딱 대  (2) 2024.06.17
Set / Map / Iterator  (0) 2024.06.14
익명 클래스 / 람다식 / 메소드 참조  (1) 2024.06.08
형 변환 / valueOf() / stream  (0) 2024.06.07