본문 바로가기
프로그래밍/JAVA

초보 자바 프로그래밍(35) - 제너릭 (Generics)

by 머니테크리더 2023. 5. 2.
반응형

자바 제너릭 대표 이미지
자바 제너릭 대표 이미지

🔖 INDEX

     

     

    제너릭 (Generics)은 자바에서 매우 중요한 프로그래밍 개념으로, 코드의 유연성과 재사용성을 높이는 데 도움이 되는 기능입니다. 제너릭을 사용하면 클래스, 인터페이스, 메서드에 타입 매개 변수를 적용할 수 있습니다. 이를 통해 다양한 타입의 객체를 처리하면서, 타입 안전성을 유지할 수 있습니다.

     

    제너릭의 주요 이점

    제너릭의 주요 이점은 다음과 같습니다: ​

    1. 타입 안전성(Type Safety): 제너릭을 사용하면 컴파일 시점에서 타입 검사를 수행할 수 있으므로, 잘못된 타입의 객체가 사용되는 것을 방지할 수 있습니다. 이로 인해 런타임에 발생할 수 있는 ClassCastException 등의 예외를 줄일 수 있습니다.
    2. 코드 재사용성(Reusability): 제너릭을 사용하면 여러 타입의 객체를 처리하는 공통 코드를 작성할 수 있습니다. 이를 통해 코드 중복을 줄이고, 유지 보수를 편리하게 할 수 있습니다.
    3. 타입 체크 및 형 변환 감소: 제너릭을 사용하면 컴파일러가 타입 체크를 자동으로 수행하므로, 개발자가 명시적으로 형 변환을 수행할 필요가 줄어듭니다.

     

    제너릭의 구문 정의 및 사용 예제

    제너릭은 다음과 같은 구문으로 정의됩니다:

    class ClassName<T> { ... }

    여기서 'T'는 타입 매개 변수를 나타냅니다. 이를 사용하여 클래스 내에서 T 타입의 객체를 처리할 수 있습니다. 예를 들어, 아래와 같은 간단한 제너릭 클래스를 작성할 수 있습니다:

    public class Box<T> {
        private T content;
    
        public void setContent(T content) {
            this.content = content;
        }
    
        public T getContent() {
            return content;
        }
    }

    이제 Box 클래스는 다양한 타입의 객체를 저장할 수 있습니다. 예를 들어, Integer 객체를 저장하려면 다음과 같이 사용할 수 있습니다:

    // Box 클래스를 사용하여 Integer 타입의 객체를 저장할 수 있는 새로운 Box 인스턴스를 생성합니다.
    // 여기서 Integer는 Java의 기본 데이터 타입인 int를 래핑한 클래스입니다.
    Box<Integer> integerBox = new Box<>();
    
    // integerBox 인스턴스에 setContent 메서드를 호출하여 정수 값 10을 저장합니다.
    // 여기서 Integer 객체로 자동 박싱(autoboxing)되어 저장됩니다.
    integerBox.setContent(10);
    
    // integerBox 인스턴스에서 getContent 메서드를 호출하여 저장된 Integer 값을 가져옵니다.
    // 여기서 intValue 변수는 Integer 타입입니다.
    Integer intValue = integerBox.getContent();
    
    // 이제 intValue 변수에는 integerBox에 저장된 정수 값 10이 할당되어 있습니다.

    제너릭을 사용하면, 코드가 더욱 간결하고 안전해집니다.

     

     

    제너릭의 고급 기능

    제너릭은 위의 기능 외에 다양한 고급 기능을 제공합니다. 예를 들어, 제너릭의 타입 매개 변수에는 와일드카드를 사용할 수 있으며, 제한된 타입 매개 변수(Bounded Type Parameter)를 통해 특정 클래스의 하위 클래스만 허용할 수도 있습니다.

     

    이러한 기능들은 제너릭의 유연성을 더욱 높여줍니다.

     

    1. 와일드카드(Wildcard)

    와일드카드는 제너릭 타입의 범위를 지정할 수 있는 방법입니다. 이를 사용하면 특정 클래스의 하위 클래스 또는 상위 클래스에만 국한된 제너릭 타입을 허용할 수 있습니다. 와일드카드는 '?' 기호로 표시되며, 다음과 같이 사용할 수 있습니다.

    // processElements라는 메서드를 정의합니다. 이 메서드는 제너릭 와일드카드를 사용하여
    // 어떠한 타입의 요소도 포함할 수 있는 List를 매개변수로 받습니다.
    public static void processElements(List<?> elements) {
        
        // elements 리스트의 각 요소에 대해 반복문을 수행합니다.
        // 여기서 element 변수는 Object 타입이며, elements 리스트의 요소를 차례로 할당받습니다.
        for (Object element : elements) {
            
            // 현재 반복에서 할당된 element 객체를 출력합니다.
            // System.out.println 메서드는 element 객체의 toString 메서드를 호출하여
            // 문자열 표현을 출력합니다.
            System.out.println(element);
        }
        // 반복문이 종료되면, elements 리스트의 모든 요소가 출력되고 메서드가 종료됩니다.
    }

    위의 예제에서, processElements 메서드는 어떤 타입의 요소도 포함할 수 있는 List를 매개 변수로 받습니다.

     

    2. 제한된 타입 매개 변수(Bounded Type Parameter)

    제한된 타입 매개 변수를 사용하면 특정 클래스의 하위 클래스나 인터페이스를 구현한 클래스만을 타입 매개 변수로 허용할 수 있습니다. 이를 사용하여 제너릭 코드의 범위를 제한하고, 관련된 타입끼리만 처리하도록 할 수 있습니다. 제한된 타입 매개 변수는 'extends' 키워드를 사용하여 정의합니다.

    // Container라는 제너릭 클래스를 정의합니다. 이 클래스는 Number 클래스를 상속한 타입 T를 받습니다.
    // 이로 인해 Container는 Number 클래스와 그 하위 클래스 (예: Integer, Double 등)의 인스턴스를 저장할 수 있습니다.
    public class Container<T extends Number> {
        
        // 제너릭 타입 T의 객체를 저장할 private 멤버 변수 value를 선언합니다.
        private T value;
    
        // setValue 메서드를 정의합니다. 이 메서드는 제너릭 타입 T의 객체를 매개변수로 받아서
        // value 멤버 변수에 저장합니다.
        public void setValue(T value) {
            this.value = value;
        }
    
        // getValue 메서드를 정의합니다. 이 메서드는 value 멤버 변수에 저장된
        // 제너릭 타입 T의 객체를 반환합니다.
        public T getValue() {
            return value;
        }
    }
    // 이제 Container 클래스를 사용하여 Number 클래스의 하위 클래스 인스턴스를 저장하고 처리할 수 있습니다.

    위의 예제에서, Container 클래스는 Number 클래스의 하위 클래스만을 타입 매개 변수로 허용합니다. 이로 인해 Container는 Integer, Double 등의 숫자 타입만을 저장할 수 있게 됩니다.

     

    제너릭은 자바 프로그래밍에서 매우 중요한 개념으로, 코드의 안전성과 재사용성을 높이는 데 큰 도움이 됩니다. 제너릭을 이해하고 적절히 활용하면, 더욱 견고하고 유연한 코드를 작성할 수 있습니다.

     

    참고로, C++에서는 템플릿(Template)이라는 기능을 사용하여 제너릭과 유사한 프로그래밍을 할 수 있습니다.

     

    제너릭과 함께 알아두면 좋은 내용

    자바에서 제너릭과 함께 알아두면 좋은 내용들은 다음과 같습니다:

     

    1. 타입 인자 (Type Argument)

    제너릭 클래스나 인터페이스를 사용할 때 실제 타입을 지정해주는 것입니다.

    List<String> stringList = new ArrayList<>();

    여기에서 String이 타입 인자입니다.

     

    2. 타입 파라미터 (Type Parameter)

    제너릭 클래스나 인터페이스를 정의할 때 사용하는 것으로, 실제 타입을 받기 위한 플레이스홀더입니다.

    public class Container<T> {
        private T value;
        //...
    }

    여기에서 T가 타입 파라미터입니다.

     

    3. 제한된 타입 파라미터 (Bounded Type Parameter)

    타입 파라미터에 대해 특정 클래스 또는 인터페이스를 상속/구현한 타입만 허용하고 싶을 때 사용합니다.

    public class NumericContainer<T extends Number> {
        private T value;
        //...
    }

    여기에서 T는 Number 클래스를 상속한 타입만 허용합니다.

     

    4. 와일드카드 (Wildcard)

    제너릭 타입의 인스턴스를 처리할 때 타입에 대한 제약을 완화할 수 있는 기능입니다. 와일드카드를 사용하면 특정 타입이 아닌 모든 타입을 처리할 수 있습니다.

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

    여기에서 List<?>는 모든 타입의 리스트를 처리할 수 있습니다.

     

    5. 타입 소거 (Type Erasure) 

    자바에서 제너릭 코드는 컴파일 시점에 타입 정보가 소거되고, 런타임에는 제너릭 정보가 없습니다. 이로 인해 제너릭 코드는 런타임에 타입 변환을 수행할 수 없습니다.

     

    6. 제너릭 메서드 (Generic Method)

    메서드 내에서도 타입 파라미터를 사용하여 유연하게 처리할 수 있는 메서드입니다. 제너릭 메서드는 클래스의 제너릭 타입과 독립적으로 작동할 수 있습니다.

    public static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.print(item + " ");
        }
    }

    여기에서 <T>는 제너릭 메서드를 정의할 때 사용하는 타입 파라미터입니다.

     

     

    7. 제너릭 상속 (Generic Inheritance)

    제너릭 클래스 또는 인터페이스가 다른 제너릭 클래스 또는 인터페이스를 상속/구현할 때, 타입 파라미터를 전달하여 상속 관계를 구성할 수 있습니다.

    public interface MyList<T> extends List<T> {
        //...
    }

    여기에서 MyList<T>가 List<T>를 상속합니다.

     

    8. 불변성 (Invariance)

    자바에서 제너릭은 불변성을 따릅니다. 불변성은 특정 클래스의 인스턴스와 해당 클래스의 제너릭 인스턴스 간의 할당을 제한합니다. 즉, 서로 다른 타입 인자를 가진 제너릭 타입은 서로 호환되지 않습니다.

    import java.util.List;
    import java.util.ArrayList;
    
    class Animal {}
    class Mammal extends Animal {}
    
    public class InvarianceExample {
        public static void main(String[] args) {
            List<Animal> animalList = new ArrayList<>();
            List<Mammal> mammalList = new ArrayList<>();
    
            // 불변성 때문에 컴파일 오류
            // animalList = mammalList; 
            // List<Animal>과 List<Mammal>은 호환되지 않음
    
            // 정상적인 리스트 생성 및 할당
            animalList.add(new Animal());
            animalList.add(new Mammal());
    
            mammalList.add(new Mammal());
        }
    }

    위 예제에서 List과 List은 서로 호환되지 않으며, 불변성 때문에 컴파일 오류가 발생합니다. 이는 List이 Animal 타입의 객체뿐만 아니라 Mammal 타입의 객체도 저장할 수 있기 때문입니다. 따라서, List을 List에 할당하는 것은 안전하지 않습니다. 불변성이 적용되는 경우에도 공변성과 반공변성을 사용하여 제너릭 코드를 유연하게 만들 수 있습니다. 이는 위에서 설명한 공변성과 반공변성의 예제 코드에서 볼 수 있습니다. 이를 통해 개발자들은 불변성, 공변성, 그리고 반공변성을 적절하게 활용하여 제너릭 코드를 안전하고 유연하게 작성할 수 있습니다.

     

    9. 공변성과 반공변성 (Covariance and Contravariance)

    이 속성들은 제너릭 타입들 간의 상속 관계를 지정할 때 사용되는 개념입니다. 이 두 가지 개념을 이해하려면 먼저 자바에서의 상속 관계를 이해해야 합니다. 자바에서는 클래스와 인터페이스간에 상속 관계가 존재할 수 있습니다. 예를 들어, A가 B를 상속한다면 B는 A의 하위 타입이 됩니다.

    2023.05.02 - [JAVA] - 초보 자바 프로그래밍(36) - 공변성과 반공변성 (Covariance and Contravariance)

     

    초보 자바 프로그래밍(36) - 공변성과 반공변성 (Covariance and Contravariance)

    🔖 INDEX 공변성 (Covariance) 공변성은 하위 타입 간의 관계를 유지하는 것을 의미합니다. 예를 들어, 클래스 A가 클래스 B를 상속하고 있을 때, List와 List 사이의 관계를 고려해봅시다. 공변성이 허

    moneylogging.tistory.com

     

    10. 제너릭 배열 생성 제한

    자바에서는 제너릭 배열을 직접 생성할 수 없습니다. 이는 타입 소거로 인해 런타임에 타입 변환을 수행할 수 없기 때문입니다. 하지만 제너릭 배열을 간접적으로 사용할 수 있는 방법들이 있습니다.

     

    이러한 내용들은 자바의 제너릭을 사용하면서 다양한 문제에 대응하고, 코드의 유연성과 재사용성을 높이기 위한 기본적인 개념들입니다. 제너릭에 대해 이해하고 활용하면 자바 프로그래밍에서 좀 더 안전하고 효율적인 코드를 작성할 수 있습니다.

     

     

    댓글