ABOUT ME

작은 디테일에 집착하는 개발자

Today
-
Yesterday
-
Total
-
  • [Java] 제네릭과 와일드카드
    IT Study/컴퓨터 기초 2023. 4. 5. 18:02
    728x90

    1. 제네릭이란?

    제네릭은 컴파일 시 객체의 타입을 체크하며, 객체의 타입을 미리 지정하여 타입 안정성을 확보할 수 있는 기능입니다.

    간단히 말해, 다룰 객체의 타입을 미리 명시해 줌으로써 번거로운 형 변환을 줄일 수 있습니다.

    예를 들어, ArrayList와 같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있기는 하지만 보통 한 종료의 객체를 담는 경우가 많습니다. 그럼에도 불구하고 ArrayList는 객체를 꺼낼 때마다 타입을 체크하고 형 변환을 하고, 원하지 않는 종류의 객체가 포함되는 것을 막을 방법이 없다는 문제가 있습니다. 이러한 문제들을 제네릭이 해결해 줍니다.

     

    2. 제네릭 클래스

    2-1. 제네릭 클래스의 선언

    제네릭 클래스는 타입 파라미터를 사용하여 선언하되, 클래스 이름 뒤에 <> 기호를 사용하여 *타입 파라미터를 명시합니다.

    public class GenericClass<T> {
        // 클래스 내부에서 T라는 타입 파라미터 사용
        private T item;
    
        public void setItem(T item) {
            this.item = item;
        }
    
        public T getItem() {
            return item;
        }
    }

     

    *타입 파라미터란? 제네릭에서 사용되는 변수의 형식을 나타내는 데 사용하는 식별자로, 개인이 정의하여 사용할 수 있습니다.

    그러나 일반적으로 E, T, K, V 등이 사용됩니다. (E :컬렉션의 요소 타입, T : 일반 타입, K : 맵의 키 타입, V : 맵의 값 타입)

    마치 수학식 f(x, y) = x + y와 f(a, b) = a + b가 다르지 않은 것처럼,

    타입 파라미터는 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 동일합니다.

     

    2-2. 제네릭 클래스의 예시

    제네릭 클래스는 다양한 형태로 사용될 수 있습니다.

    예를 들어, ArrayList와 같은 컬렉션 클래스는 제네릭을 활용하여 다양한 타입의 객체를 저장하고 사용할 수 있습니다.

     

    아래는 2-1에서 만든 GenericClass와 ArrayList의 소스코드 예시입니다.

    public class Main {
    	public static void main(String[] args) {
    		// GenericClass 제네릭 클래스를 사용하여 다양한 타입의 객체를 저장하는 예시
            
    		GenericClass<Integer> intGC = new GenericClass<>();
    		intGC.setItem(123);			// Integer 객체 저장
    		Integer intItem = intGC.getItem();	// Integer 객체 반환
    		System.out.println(intItem);
    		
    		GenericClass<String> stringGC = new GenericClass<>();
    		stringGC.setItem("Hello"); 		// String 객체 저장
    		String stringItem = stringGC.getItem();	// String 객체 반환
    		System.out.println(stringItem);
    		
    		// ArrayList 제네릭 클래스를 사용하여 다양한 타입의 객체를 저장하는 예시
            
    		ArrayList<Integer> intList = new ArrayList<>();
    		intList.add(1);				// Integer 객체 추가
    		intList.add(2);
    		intList.add(3);
    		Integer firstInt = intList.get(0);	// Integer 객체 가져오기
    		System.out.println(firstInt);
    		
    		ArrayList<String> stringList = new ArrayList<>();
    		stringList.add("Apple");		// String 객체 추가
    		stringList.add("Banana");
    		stringList.add("Cherry");
    		String firstString = stringList.get(0);	// String 객체 가져오기
    		System.out.println(firstString);
    	}
    }

     

    위의 예시는 GenericClass와 ArrayList라는 제네릭 클래스를 사용하여 다양한 타입의 객체를 저장하고 사용한 것입니다.

    위의 예시를 통해 제네릭 클래스를 활용하면 타입 안정성을 보장하며, 코드의 재사용성과 가독성을 높일 수 있습니다.

     

    3. 제네릭 인터페이스

    3-1. 제네릭 인터페이스의 선언

    제네릭 인터페이스는 인터페이스 선언 시 타입 파라미터를 사용하여 정의되는 인터페이스입니다.

    이때 타입 파라미터는 인터페이스 내에 사용되는 타입과 동일하며, 실제 사용될 때에는 구체적인 타입으로 대체됩니다.

    // 제네릭 인터페이스 선언
    interface Printable<T> {
        void print(T value);
    }

     

    인터페이스에서는 선언한 메서드를 선언만 할 수 있기 때문에,

    이를 상속받은 클래스에서 구현하고 이후 메인 클래스에서 사용할 수 있습니다.

    // 제네릭 인터페이스를 구현한 클래스
    class Printer implements Printable<String> {
        @Override
        public void print(String value) {
            System.out.println(value);
        }
    }

     

    3-2. 제네릭 인터페이스의 예시

    아래는 3-1에서 만든 제네릭 인터페이스와 이를 구현한 클래스의 객체를 생성하여 사용하는 예시입니다.

    public class Main {
        public static void main(String[] args) {
            // 제네릭 인터페이스를 구현한 클래스의 객체 생성 1
            Printer printer1 = new Printer();
            
            // 제네릭 인터페이스의 메서드 호출
            printer1.print("Hello, World!");
            
             // 제네릭 인터페이스를 구현한 클래스의 객체 생성 2
            Printer printer2 = new Printer();
            
            // 다른 타입의 값도 출력 가능
            int intValue = 100;
            printer2.print(Integer.toString(intValue)); // Integer.toString : int -> String
        }
    }

     

    예시에서는 String과 Integer을 사용했지만, 다양한 타입의 객체를 처리할 때에도 동일한 인터페이스를 사용할 수 있습니다.

    이렇게 제네릭 인터페이스를 활용하면, 타입에 종속되지 않는 유연한 코드를 작성할 수 있습니다.

     

    4. 제네릭 메서드

    4-1. 제네릭 메서드의 선언

    제네릭 메서드는 메서드 선언 시, 매개변수나 반환 값에 타입 파라미터를 사용합니다.

    이를 통해 메서드는 다양한 타입의 인자를 처리하거나 다양한 타입의 값을 반환할 수 있습니다.

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

     

    4-2. 제네릭 메서드의 예시

    4-1에서 만든 제네릭 메서드를 사용하는 예시입니다.

    public class Main {
        public static void main(String[] args) {
        	public static void main(String[] args) {
                Integer[] intArray = {1, 2, 3, 4, 5};
                String[] stringArray = {"Hello", "World"};
                
                // 제네릭 메서드 호출
                printArray(intArray);	// Integer 배열 출력
                System.out.println();
                printArray(stringArray);	// String 배열 출력
            }
        }
    }

     

    제네릭 메서드도 마찬가지로 메서드의 재사용성과 타입 안정성을 높일 수 있습니다.

     

    5. 제한된 제네릭

    타입 파라미터로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만,

    실상 여전히 모든 종류의 타입을 지정할 수 있다는 것은 변함없습니다.

    이에 제네릭 타입 매개변수의 종류를 제한하여 특정 타입의 객체만을 처리할 수 있도록 제한할 수 있습니다.

     

    5-1. extends

    extends 키워드를 통해 상한 타입을 제한할 수 있습니다. (쉽게 말해, 선언된 타입 이하를 사용할 수 있습니다.)

    // 제한된 제네릭 extends
    class NumberPrinter<T extends Number> { // Number 클래스를 상속받은 T 허용
        private T number;
        
        public NumberPrinter(T number) {
            this.number = number;
        }
        
        public void print() {
            System.out.println("Number: " + number);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            NumberPrinter<Integer> intPrinter = new NumberPrinter<>(10);
            intPrinter.print(); // 출력: Number: 10
            
            NumberPrinter<Double> doublePrinter = new NumberPrinter<>(3.14);
            doublePrinter.print(); // 출력: Number: 3.14
            
            // NumberPrinter<String> stringPrinter = new NumberPrinter<>("Hello"); // 컴파일 에러
        }
    }

     

    위의 예시에서는 Number 클래스를 상속받은 하위 클래스만을 사용할 수 있습니다.

    따라서 Number 클래스 이하인 Integer와 Double을 사용할 때에는 에러가 발생하지 않습니다.

    그러나 String은 Number 클래스를 상속하지 않기 때문에 컴파일 에러 발생합니다.

     

    5-2. super

    super 키워드를 통해 하한 타입을 제한할 수 있습니다. (쉽게 말해, 선언된 타입 이상을 사용할 수 있습니다.)

    super에 대한 예시는 앞으로 내용을 추가하도록 하겠습니다.

     

    6. 와일드카드란?

    제네릭에서 사용되는 와일드카드는 모든 타입을 나타내기 위해 사용되는 기호입니다.

    ? 물음표 기호를 사용하여, 제네릭을 사용하는 클래스 혹은 메서드의 파라미터나 리턴 타입에 사용됩니다.

    와일드카드를 통해 제네릭을 더 유연하게 사용할 수 있습니다.

    예를 들어, List<? super Integer> 는 Integer 타입의 객체뿐만 아니라

    그 상위 클래스인 Number,  Object 타입의 객체도 모두 리스트에 추가할 수 있습니다.

     

    6-1. 와일드카드의 사용

    6-1-1. <?> 

    모든 타입을 받을 수 있습니다.

    public static void printList(List<?> list) {
        for (Object ele : list) {
            System.out.print(ele + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        printList(intList);
    
        List<String> strList = Arrays.asList("one", "two", "three");
        printList(strList);
    }

     

    6-1-2. <? extends 상위타입>

    상한 타입을 제한할 수 있습니다. (선언된 타입 이하를 사용할 수 있습니다.)

    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list) {
            s += n.doubleValue();
        }
        return s;
    }
    
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        double sum = sumOfList(intList);
        System.out.println("Sum of integers = " + sum);
    
        List<Double> doubleList = Arrays.asList(1.2, 2.3, 3.5);
        sum = sumOfList(doubleList);
        System.out.println("Sum of doubles = " + sum);
    
        // 컴파일 에러: String은 Number의 자손 타입이 아님
        // List<String> stringList = Arrays.asList("one", "two", "three");
        // sum = sumOfList(stringList);
    }

     

    6-1-3. <? super 하위타입>

    하한 타입을 제한할 수 있습니다. (선언된 타입 이상을 사용할 수 있습니다.)

    public class Main {
    	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 = new ArrayList<Integer>();
    	    addNumbers(intList);
    	    System.out.println(intList);
    
    	    List<Number> numList = new ArrayList<Number>();
    	    addNumbers(numList);
    	    System.out.println(numList);
    
    	    List<Object> objList = new ArrayList<Object>();
    	    addNumbers(objList);
    	    System.out.println(objList);
    	}
    }

     

    오늘은 제네릭, 제한된 제네릭 그리고 와일드카드에 대한 예시를 알아봤습니다.

    저도 사용 방법을 정확히 모르고 있던 터라, 제네릭을 사용하는  데에 애를 먹었는데요.

    이번 블로그 글을 통해 정확한 개념을 잡고 가는 것 같습니다. (ArrayList가 제네릭 클래스였다니..!)

    자바 마스터가 되는 날까지... 화이팅 😊

Designed by Tistory.