Ian's Archive 🏃🏻

thumbnail
Generic 정리
JAVA
2024.12.19.

제네릭을 사용하면서 잘 모른다고 생각해 정리하기 위해 포스팅을 작성한다.

1. 제네릭(Generics) 이란

클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

예시를 하나 보자

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

<>부분이 제네릭이다. 꺾쇠 괄호 안에는 타입명을 기재한다.

위의 예시에선 리스트 클래스 자료형 타입이 String으로 지정이 되어 문자열 데이터만 리스트에 적재가 가능해진다.

타입을 변수화 한 기능이라고 이해하면 된다.


1.1 제네릭 타입 매개변수

1

<> 꺽쇠 괄호는 다이아몬드 연산자라고 한다. 이 괄호안에 식별자 기호를 지정함으로써 파라미터화 할 수 있다. 이것을 마치 메소드가 매개변수를 받아 사용하는 것과 비슷하다하여 타입 매개변수 / 타입 변수라고 부른다.

2

제네릭을 사용하면 위의 그림같이 제네릭 타입 전파가 행해진다. 타입이 구체적으로 설정되는 이 과정을 전문 용어로 구체화라고 한다.

copyButtonText
// 다음과 같이 new 생성자 부분의 제네릭의 타입 매개변수는 생략할 수 있다.
FruitBox<Apple> intBox = new FruitBox<>();

jdk 1.7 버전 이후부터, new 생성자 부분의 제네릭 타입을 생략할 수 있게 되었다

1.2 타입 파라미터 기호 네이밍

제네릭을 <T>와 같이 사용해 표현했지만, 사실 식별자 기호는 문법적으로 정해진 것이 없다.

다만 우리가 for문을 이용할때 루프 변수를 i 로 지정해서 사용하듯이, 제네릭의 표현 변수를 T 로 표현한다고 보면 된다.

만일 두번째, 세번째 제네릭이 필요하다고 보면 for문의 j나 k 같이 S, U 로 이어나간다.

명명하고 싶은대로 아무 단어나 넣어도 문제는 없지만, 대중적으로 통하는 통상적인 네이밍이 있으면 개발이 용이해 지기 때문에 아래 표화 같은 암묵적인 규칙(convention)이 존재한다. 예를들어 예제에서 사용된 T 를 타입 변수(type variable)라고 하며, 임의의 참조형 타입을 의미한다.

타입 설명
<T> 타입(Type)
<E> 요소(Element), 예를 들어 List
<K> 키(Key), 예를 들어 Map<k, v>
<V> 리턴 값 또는 매핑된 값(Variable)
<N> 숫자(Number)
<S, U, V> 2번째, 3번째, 4번째에 선언된 타입

2. 제네릭의 장점

  1. 컴파일 타임에 타입 검사를 통해 예외 방지
  • 제네릭을 사용하면 파라미터 타입이나 리턴 타입에 대한 정의를 외부로 미룬다.
  • 타입에 대해 유연성과 안정성을 확보한다.
  • 런타임 환경에 아무런 영향이 없는 컴파일 시점의 전처리 기술이다.

=> 타입을 유연하게 처리하며, 런타임에 발생할 수 있는 타입에러를 컴파일 전에 검출한다.

copyButtonText
class Apple {}
class Banana {}

class FruitBox <T> {
    private T[] fruit;

    public FruitBox(T[] fruit) {
        this.fruit = fruit;
    }

    public T getFruit(int index) {
        return fruit[index];
    }
}

class FruitBox2 {
    // 모든 클래스 타입을 받기 위해 최고 조상인 Object 타입으로 설정
    private Object[] fruit;

    public FruitBox2(Object[] fruit) {
        this.fruit = fruit;
    }

    public Object getFruit(int index) {
        return fruit[index];
    }
}

public class Main {
    public static void main(String[] args) {
        Apple[] arr = {
                new Apple(),
                new Apple()
        };
        FruitBox2 box = new FruitBox2(arr);

        Apple apple = (Apple) box.getFruit(0);
        Banana banana = (Banana) box.getFruit(1);

//        FruitBox<Apple> box = new FruitBox<>(arr);
//
//        Apple apple = (Apple) box.getFruit(0);
//        Banana banana = (Banana) box.getFruit(1);
    }
}

제네릭을 사용하지 않고 Object를 통해 코드를 작성하면 코드가 실행되고 런타임 오류가 발생한다.

제네릭을 사용하면 (Banana) box.getFruit(1); 부분에서 에러가 노출되어 미리 에러를 찾아 알려준다.

  1. 불필요한 캐스팅을 없애 성능 향상
copyButtonText
Apple[] arr = { new Apple(), new Apple() };
FruitBox box = new FruitBox(arr);

// 가져온 타입이 Object 타입이기 때문에 일일히 다운캐스팅을 해야함 - 쓸데없는 성능 낭비
Apple apple1 = (Apple) box.getFruit(0);
Apple apple2 = (Apple) box.getFruit(1);
copyButtonText
FruitBox<Apple> box = new FruitBox<>(arr);

// 미리 제네릭 타입 파라미터를 통해 형(type)을 지정해놓았기 때문에 별도의 형변환은 필요없다.
Apple apple = box.getFruit(0);
Apple apple = box.getFruit(1);

3. 제네릭 사용 주의 사항

  • 제네릭 타입의 객체는 생성 불가
  • static 멤버에 제네릭 타입이 올 수 없음
    • static은 제네릭을 생성하기 전에 타입이 정해져 있어야함 (논리적 오류)
  • 제네릭 클래스 자체를 배열로 만들 순 없다.
    • 제네릭 타입의 배열 선언은 허용된다.
copyButtonText
// 불가능
Sample<Integer>[] arr1 = new Sample<>[10];

// 가능
Sample<Integer>[] arr2 = new Sample[10];
arr2[0] = new Sample<Integer>();
arr2[1] = new Sample<>();

4. 제네릭 사용 법

4.1 재네릭 클래스

클래스 선언문 옆에 제네릭 타입 매개변수가 쓰이면, 이를 제네릭 클래스라 한다.

copyButtonText
class FruitBox <T> {
    private T[] fruit;

    public FruitBox(T[] fruit) {
        this.fruit = fruit;
    }

    public T getFruit(int index) {
        return fruit[index];
    }
}

4.2 제네릭 인터페이스

인터페이스에도 제네릭을 적용 가능, 단 인터페이스를 implements한 클래스에서도 오버라이딩 한 메서드를 제네릭 타입에 맞춰서 똑같이 구현해주어야 한다.

copyButtonText
interface SampleInterface<T> {
    public void addElement(T t, int index);
    public T getElement(int index);
}

public class Sample<T> implements SampleInterface<T> {
    private T[] array;

    public Sample() {
        array = (T[]) new Object[10];
    }

    @Override
    public void addElement(T t, int index) {
        array[index] = t;
    }

    @Override
    public T getElement(int index) {
        return array[index];
    }
}

public class Main {
    public static void main(String[] args) {
        Sample<String> sample = new Sample<>();
        sample.addElement("This is string", 5);
        System.out.println(sample.getElement(5));
    }
}

아래는 제네릭 함수형 인터페이스

람다에서 많이 사용

copyButtonText
interface AddInterface<T> {
    public T add(T x, T y);
}

public class Main {
    public static void main(String[] args) {
        AddInterface<Integer> o = (x, y) -> x + y;
        System.out.println(o.add(10, 20));
    }
}

4.3 제네릭 메소드

3

위의 addBox는 그냥 타입 파라미터로 타입을 지정한 메서드

제네릭 메소드란 메소드 선언부에 <T>가 선언된 메소드

직접 메소드에 <T>를 설정함으로서 동적으로 타입을 받아와 사용할 수 있는 독립적으로 운용 가능한 제네릭 메소드

여기서 의문점이 들 수 있는데 굳이 제네릭 메소드를 사용하는 이유는

클래스의 제네릭 타입과 별도로 메소드에서만 특정 타입을 처리할 때 필요하다

처음 제네릭 클래스를 인스턴스화하면, 클래스 타입 매개변수에 전달한 타입에 따라 제네릭 메소드도 타입이 정해지게 된다.

그런데 만일 제네릭 메서드를 호출할때 직접 타입 파라미터를 다르게 지정해주거나, 다른 타입의 데이터를 매개변수에 넘기면 독립적인 타입을 가진 제네릭 메서드로 운용되게 된다.

아래와 같이 메소드에서만 제네릭 타입을 사용해 처리가 가능하다

4

요런 느낌이다.

이때 컴파일러가 제네릭 타입에 들어갈 데이터 타입을 메소드의 매개변수를 통해 추정할 수 있기 때문에, 대부분의 경우 제네릭 메서드의 타입 파라미터를 생략하고 호출할 수 있다.

copyButtonText
public class Main {
    // 제네릭 메소드: 메소드 선언부에 <T>로 타입 정의
    public static <T> void print(T item) {
        System.out.println("Item: " + item);
    }

    public static void main(String[] args) {
        Main.<Integer>print(10);
        print(20);
        print("Hello");
    }
}

5. 제네릭 타입 범위 한정

제네릭에 타입을 지정해서 클래스 타입을 컴파일 타임에서 정하여 타입 예외에 대한 안정성 확보는 좋지만 문제는 너무 자유롭다

그래서 나온 것이 제한된 타입 매개변수이다

기본적인 용법은 <T extends [제한 타입]>

상속 키워드와 혼동할 수 있는데 꺽쇠 괄호안에 extends가 있으면 제한이고, 괄호 바깥에 있으면 상속이다.

copyButtonText
class Calculator<T extends Number> {
    void add(T a, T b) {}
    void multiply(T a, T b) {}
    void divide(T a, T b) {}
    void minus(T a, T b) {}
}
class Box<T extends Readable & Closeable> {
    List<T> list = new ArrayList<>();

    public void add(T item) {
        list.add(item);
    }
}
public class Main {
    public static void main(String[] args) {
        Calculator<Integer> calculator1 = new Calculator<Integer>();

        // 오류
        Calculator<String> calculator2 = new Calculator<String>();
    }
}

다중 타입을 한정시키고 싶을 땐 &연산자 이용한다.

다중 extends를 사용할 땐 오로지 인터페이스만 사용 가능하다.

6. 제네릭 형 변환

제네릭은 서브 타입간에 형변환이 불가능하다.

형변환을 하기 위해서는 제네릭에서 제공하는 와일드 카드 ?문법을 이용해야 한다.

제네릭의 와일드 카드에 대해선 내용이 많아서 추후에 새로운 글로 정리한다. (하하…)

Reference

[JAVA] 제네릭(Generic)이란
자바 제네릭(Generics) 개념 & 문법 정복하기

Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.