관리 메뉴

개발그래머

[자바스터디 14주차] Generic 본문

Java

[자바스터디 14주차] Generic

임요환 2023. 7. 18. 19:25

목표

  • 자바의 제네릭에 대해 학습하세요

학습할 것 (필수)

  • 제네릭 사용법
  • 제네릭 주요 개념 (바운디드 타입, 와일드카드)
  • 제네릭 메소드 만들기
  • Erasure

제네릭 사용법

제네릭의 탄생 이유

  • 제네릭은 자바 5 버전부터 도입되었음
  • 이전 컬렉션(List, Set, Map...)과 가은 데이터 구조를 사용할 때, Object 타입을 이용하여 모든 유형의 객체를 담을 수 있었음
public class GenericExample {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        list.add(10L);
        list.add(2.5);
        list.add("hello world");

        for (Object o : list) {
            System.out.println(o);
        }

    }
}
  • Object는 모든 객체를 담을 수 있지만 객체의 실제 타입 정보가 사라지게 됨 -> 컴파일 시점에는 타입 체크가 이루어지지 않고 런타임에야 객체의 실제 타입을 확인하고 타입 캐스팅을 해야 하는 번거로움이 발생하게 됨
for (Object o : list) {
    if(o instanceof Integer) {
        System.out.println((int) o);
    }
}

for (Object o : list) {
    System.out.println((int) o);// Exception in thread "main" java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap')
}
  • Integer 값만 원할 시 이런 식으로 타입체크를 하고 타입캐스팅을 해야 함
  • 또한, Object 타입으로 Integer 타입만 들어가지 않고 모든 타입이 들어가게 됨
public class GenericExample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
//        list.add(10L); // compile error
//        list.add(2.5); // compile error
//        list.add("hello world"); // compile error

        for (Integer integer : list) {
            System.out.println(integer);
        }
    }
}
  • 제네릭은 타입을 파라미터로 받아들여 컴파일 시점에 타입 체크를 수행하여 타입 안정성을 보장함
  • 런타임 에러를 방지하고 더욱 안전하고 효율적인 코드를 작성할 수 있게 됨

제네릭의 장점과 사용 이유

  • 타입안정성 = 컴파일 시점에서 타입 체크를 하기 때문에 런타임 에러를 방지하고 컴파일러가 타입 불일치를 잡아내기 때문에 프로그램의 안정성이 증가함
  • 재사용성 = 제네릭을 사용하여 다양한 유형의 객체를 처리하는 일반적인 메서드나 클래스를 작성할 수 있으며 코드 재사용성이 높아짐
  • 간결성 = 타입 캐스팅을 제거하고 일반적인 코드를 작성하기 때문에 코드가 더 간결하고 가독성이 좋아짐

제네릭이란 무엇인가?

  • 클래스 또는 메서드를 선언할 때 타입 매개변수를 사용하여 다양한 유형의 객체를 처리할 수 있도록 하는 기능
  • 타입 매개변수는 일반적으로 대문자 알파벳 한 글자로 표현되며, 꺽쇠 괄호(<>)안에 선언됨
    • E : 요소 (Element, 자바 컬렉션에서 주로 사용됨)
    • K : 키
    • N : 숫자
    • T : 타입
    • V : 값

제네릭 클래스

  • 클래스 정의 시 타입 매개변수를 사용하여 클래스 내에서 사용할 데이터 유형을 동적으로 결정함
public class Box<T> {
    private T product;

    public void putProduct(T product) {
        this.product = product;
    }

    public T getProduct() {
        return product;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.putProduct("hello");
        System.out.println(stringBox.getProduct());

        Box<Integer> integerBox = new Box<>();
//        integerBox.putProduct("hello"); // 컴파일 에러
        integerBox.putProduct(1);
        System.out.println(stringBox.getProduct());
    }
}
  • 자기가 원하는 타입의 박스로 만들 수 있으며 integerBox에 String이 들어가게 되면 컴파일 에러가 발생하게 됨

제네릭 주요 개념 (바운디드 타입, 와일드 카드)

바운디드 타입(Bounded Type)

  • 제네릭에서 사용되는 타입 매개변수의 범위를 제한하는 기능
  • 특정 타입의 상위 클래스나 하위 클래스만을 타입 매개변수로 받도록 설정할 수 있음
<T extends 상위클래스명>
<T super 하위클래스명>
  • T extends 상위클래스명 : 타입 매개변수 T는 지정한 상위 클래스나 상위 클래스의 하위 클래스만을 허용
  • T super 하위클래스명 : 타입 매개변수 T는 지정한 하위 클래스나 하위 클래스의 상위 클래스만을 허용
public class BoundedTypeExample<T extends Number> {
    private T value;

    public BoundedTypeExample(T value) {
        this.value = value;
    }

    public double square() {
        return value.doubleValue() * value.doubleValue();
    }

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

    public static void main(String[] args) {
        BoundedTypeExample<Integer> intExample = new BoundedTypeExample<>(10);
        System.out.println(intExample.square());

        // 컴파일 에러! String은 Number의 하위 클래스가 아님.
        // BoundedTypeExample<String> strExample = new BoundedTypeExample<>("Hello");

        List<Number> numberList = new ArrayList<>();
        numberList.add(10.5); // double 값 추가

        List<Object> objectList = new ArrayList<>();
        objectList.add("Hello"); // String 값 추가

        addNumbers(numberList); // Integer의 하위 클래스인 Number 리스트에 1, 2, 3 추가
        addNumbers(objectList); // Integer의 상위 클래스인 Object 리스트에 1, 2, 3 추가

        System.out.println("Number list: " + numberList); // Number list: [10.5, 1, 2, 3]
        System.out.println("Object list: " + objectList); // Object list: [Hello, 1, 2, 3]
    }
}
  • T super 하위클래스명은 제네릭 타입 매개변수 선언에서는 사용할 수 없음(클래스 레벨에서 사용 불가)
  • 주로 메서드에서 사용되며 메서드의 매개변수로 받는 컬렉션에 새로운 요소를 추가하는 용도로 사용됨
// 클래스 레벨에서 Lower Bounded Wildcards는 사용 불가능
public class GenericClass<T super Integer> {
    // 컴파일 에러!
    // 제네릭 클래스에서 Lower Bounded Wildcards 사용은 허용되지 않습니다.
}

와일드카드(Wildcard)

  • 제네릭 타입에서 불특정 한 유형을 나타내기 위해 사용
  • ?로 표시되며 세 가지 형태로 사용 가능
  • 다양한 유형의 객체를 처리할 수 있는 유연성을 가짐
  • 메서드 또는 클래스에서 다양한 제네릭 타입을 다룰 수 있음
  1. <?>: Unbounded Wildcards
  2. <? extends T>: Upper Bounded Wildcards
  3. <? super T>: Lower Bounded Wildcards
public class WildcardExample {
    // Unbounded Wildcards: 어떤 유형의 List든 처리 가능
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    // Upper Bounded Wildcards: Number의 하위 클래스들만 처리 가능
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0;
        for (Number number : list) {
            sum += number.doubleValue();
        }
        return sum;
    }

    // Lower Bounded Wildcards: Integer 또는 Integer의 상위 클래스들만 처리 가능
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<Double> doubleList = List.of(1.5, 2.5, 3.5);
        List<Object> objectList = List.of("Hello");

        printList(intList); // 1 2 3 4 5
        printList(doubleList); // 1.5 2.5 3.5
        printList(objectList); // Hello

        System.out.println(sumOfList(intList)); // 15.0
        System.out.println(sumOfList(doubleList)); // 7.5

        List<Number> numList = new ArrayList<>();
        for(int i = 1; i <=5; i++) {
            numList.add(i);
        }
        addNumbers(numList); // Integer 리스트에 1부터 5까지의 값을 추가
        System.out.println("Modified integer list: " + numList); // Output: Modified integer list: [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
    }
}

제네릭 메소드 만들기

  • 메서드 정의 시 타입 매개변수를 사용하여 메서드 내에서 사용할 데이터 유형을 동적으로 결정함
  • 메서드 선언부에 <T>를 꼭 붙여주어야 함
  • 제네릭 메서드를 사용하여 다양한 유형의 데이터를 처리하는 메서드를 작성할 수 있으며 코드의 재사용성과 유지보수성을 향상할 수 있음
public class GenericMethodExample {
    public <T> void print(T t) {
        System.out.println(t);
    }

    public static <T> void print(List<T> list) {
        for (T t : list) {
            System.out.println(t);
        }
    }

    public static void main(String[] args) {
        GenericMethodExample example = new GenericMethodExample();
        example.print(1);
        example.print("hello");

        example.print(List.of("one", "two", "three"));
    }
}

Erasure

  • 컴파일러가 제네릭 타입 정보를 제거하는 프로세스
  • 제네릭은 컴파일러 시점에 타입 체크를 수행하여 타입 안정성을 보장하지만 런타임 시점에는 타입 정보가 지워지고 제네릭 타입 매개변수들은 제거됨 -> 제네릭 코드를 JVM에서 실행하는 과정에서 발생
  • 런타임에 제네릭 타입 정보를 지우고 컴파일 시점에서만 제네릭 타입이 작동하도록 하는 과정
  • 자바의 제네릭은 런타임 오버헤드가 없어지고 타입 안정성을 보장하면서 호환성을 유지

Erasure 발생 이유

  • 제네릭 타입은 컴파일 시점에만 필요하며 실행 시점에는 모든 제네릭 타입이 동일하게 처리됨
  • 제네릭 타입 정보는 실행 시에는 사용되지 않으며 제네릭 타입 매개변수는 지워지고 Object 타입으로 변환됨
  • List<T> 에서 T는 런타임 시점에 더 이상 존재하지 않고 List로만 남게 되며 컴파일러는 List라는 단일 클래스로 간주하고 제네릭 타입에 대한 정보를 지움
The Java compiler applies type erasure to ensure that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.
Type erasure ensures that generics are implemented as they were originally specified by cleanly translating and checking the code and then removing all generic type parameters, replacing each with its first bound if the type parameter is bounded, or Object if the type parameter is unbounded.

Java 컴파일러는 매개변수화된 유형에 대해 새 클래스가 작성되지 않도록 유형 삭제를 적용합니다. 결과적으로 제네릭은 런타임 오버헤드를 발생시키지 않습니다.
유형 삭제는 코드를 깔끔하게 번역하고 확인한 다음 모든 제네릭 유형 매개변수를 제거하고, 유형 매개변수가 경계가 있는 경우 각 매개변수를 첫 번째 경계로 바꾸고, 유형 매개변수가 경계가 없는 경우 Object로 대체하여 원래 지정된 대로 제네릭이 구현되도록 합니다.
  • 제네릭은 런타임 오버헤드가 없도록 컴파일러가 타입 지워짐을 적용함
  • 제네릭이 원래 명세대로 구현되도록 하기 위해 코드를 깨끗하게 변환하고 확인한 다음 제네릭 타입 매개변수를 모두 지우고 각 타입 매개변수를 그 첫 번째 바운드로 대체하거나 타입 매개변수가 바운드가 없는 경우에는 Object로 대체함
public class Box<T> {
    private T product;

    public void putProduct(T product) {
        this.product = product;
    }

    public T getProduct() {
        return product;
    }
}

public class Box<Object> {
    private Object product;

    public void putProduct(Object product) {
        this.product = product;
    }

    public Object getProduct() {
        return product;
    }
}

백기선님 팁

  • T 대신 아무 문자를 써도 되지만 보통은 T를 사용함
  • T = type, N = number, K = key, E = element, V = value 등등
  • T는 바이트코드를 보면 결국 Object로 매핑됨 -> 이게 Erasure임
  • Upper Bounded Wildcard (어퍼바운드, extends) = <? extends Object>, 하위타입만(자식클래스)
  • Lower Bounded Wildcard (로월바운드, super) = <? super Object>, 상위타입만(부모클래스), 정의하는 부분에는 사용 불가능
  • 주로 어퍼바운드만 사용하고 로월바운드는 사용할 일이 없었음
  • 파라미터 변수화, 컴파일하면 타입이 없어짐 -> 이게 Erasure임
  • bridge method는 특정한 경우에 생김 -> 상속을 쓴 경우에 다형성을 보존하기 위해 생성
  • 타입이 바인딩이 되지 않은 경우에는 Object로 바뀜(Stack<E> E data;-> Stack Object data;)
  • 타입이 바인딩된 경우는 첫번째 바인딩 된 클래스로 대체됨(Stack<E extends Comparable<E>> E[] data; -> Stack Comparable[] data;)
  • Erasure = 제네릭 타입의 제거, 컴파일러는 제네릭 타입을 이용해서 소스 파일을 체크하고 필요한 곳에 형 변환을 넣어줌, 컴파일된 파일에는 제네릭 타입에 대한 정보가 없음 -> 이전소스코드와 호환성을 유지하기 위함(하위 호환성)
  • 제네릭에 primitive 타입을 사용하지 못하는 이유는 Object클래스를 상속받고 있지 않기 때문임
public class GenericArray<T> {
    private T[] array;

    public GenericArray(int size) {
//        this.array = new T[size]; // Type parameter 'T' cannot be instantiated directly
        this.array = (T[]) new Object[size];
    }
}
  • new 연산자를 사용하기 때문에 배열은 위와 같이 선언해야 됨
  • new 연산자는 heap영역에 생성된 객체를 할당하는데 제네릭은 컴파일 타임에 동작하는 문법이므로 이 시점에는 T의 타입이 어떤 타입인지 알 수 없기 때문에 Object 타입으로 생성한 다음 타입 캐스팅을 해주어야 사용할 수 있음
  • 위와 비슷한 이유로 static 변수에도 제네릭 타입을 사용할 수 없음
  • 브릿지 메소드 = 제네릭 클래스를 상속받거나 제네릭 인터페이스를 구현하는 클래스 또는 인터페이스를 컴파일 할 때 컴파일러는 타입 Erasure 프로세스의 일부로 브리지 메서드라는 합성 메서드를 만들어야 할 수도 있음, 일반적으로 브릿지 메서드에 대해 걱정할 필요는 없지만 간혹 stack trace에 나타나는 경우 당황할 수 있기 때문에 알아두는 게 좋음
  • Erasure 때문에 타입이 지워진다고 하지만 메타데이터로 남기 때문에 리플렉션으로 타입을 추론할 수 있음

'Java' 카테고리의 다른 글

'filter()' and 'map()' can be swapped  (0) 2023.07.31
[자바스터디 15주차] 람다식  (0) 2023.07.24
[자바스터디 13주차] I/O  (0) 2023.07.10
[자바스터디 12주차] 애노테이션  (0) 2023.07.05
[자바스터디 11주차] Enum  (0) 2023.07.03