-
[Java] 제네릭과 와일드카드IT Study/컴퓨터 기초 2023. 4. 5. 18:02728x90
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가 제네릭 클래스였다니..!)
자바 마스터가 되는 날까지... 화이팅 😊
'IT Study > 컴퓨터 기초' 카테고리의 다른 글
[Java] 스레드 (Thread) (0) 2023.04.08 [Java] 열거형 (Enum) (0) 2023.04.06 [Java] 컬렉션 프레임워크 4 (Map, HashMap 중심으로) (0) 2023.03.29 [Java] 컬렉션 프레임워크 3 (List, ArrayList 중심으로) (2) 2023.03.28 [Java] 컬렉션 프레임워크 2 (Set, HashSet 중심으로) (0) 2023.03.28