제네릭(Generic)은 프로그래밍 언어에서 타입(Type)에 독립적인 코드를 작성할 수 있도록 하는 기법입니다.
제네릭을 사용하면 데이터 타입이나 클래스를 정의할 때 실제 사용될 타입을 지정하지 않고, 추상적인 타입으로 남겨둘 수 있습니다.
이를 통해 코드의 재사용성을 높이고, 타입 안정성을 유지하면서 일반적인 알고리즘을 구현할 수 있습니다.
제네릭 타입의 위치와 선언 방식은 사용 용도에 따라 다릅니다. 메서드 선언 시 제네릭 타입 매개변수를 선언하는
방식과 클래스나 인터페이스 선언 시 제네릭 타입 매개변수를 선언하는 방식에는 차이가 있습니다.
이를 명확히 이해하기 위해 다음과 같은 사항을 살펴보겠습니다.
제네릭 클래스나 인터페이스를 제네릭으로 선언할 때는 다음과 같이 합니다:
class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
여기서 T는 클래스 수준에서 선언된 제네릭 타입 매개변수입니다.
메서드를 제네릭으로 선언할 때는 메서드 선언부에 제네릭 타입 매개변수를 명시해야 합니다.
이는 다음과 같은 형태입니다:
public <T> void print(T value) {
System.out.println(value);
}
클래스/인터페이스 수준에서 제네릭:
메서드 수준에서 제네릭:
제네릭 타입을 사용하는 예제 중 하나가 Map 인터페이스입니다:
Map<Integer, String> map = new HashMap<>();
여기서 Map<Integer, String>은 Map 인터페이스가 두 개의 제네릭 타입 매개변수 K와 V를 사용한다는 것을 의미합니다.
다음은 메서드 선언에서 제네릭 타입 매개변수를 선언하고 사용하는 예제입니다:
public class Repository {
// <S extends BaseEntity>는 이 메서드가 사용할 제네릭 타입 매개변수 S를 선언
public <S extends BaseEntity> S save(S entity) {
// 엔티티를 저장하는 로직
return entity;
}
}
여기서 public <S extends BaseEntity> S save(S entity) 부분은 메서드 선언부에
제네릭 타입 매개변수를 선언한 것입니다. <S extends BaseEntity>는 save 메서드가
BaseEntity를 상속받는 타입 S를 사용함을 의미합니다.
위 설명을 종합하면, 다음과 같은 메서드 선언이 됩니다:
class BaseEntity {
// BaseEntity 클래스 내용
}
class User extends BaseEntity {
// User 클래스 내용
}
public class Repository {
public <S extends BaseEntity> S save(S entity) {
// 엔티티를 저장하는 로직
return entity;
}
public static void main(String[] args) {
Repository repository = new Repository();
User user = new User();
User savedUser = repository.save(user); // 여기서 S는 User 타입
// savedUser는 User 타입
}
}
여기서 Repository 클래스의 save 메서드는 BaseEntity를 상속받는 어떤 타입 S라도 받을 수 있으며,
그 타입 S를 반환합니다. User 클래스는 BaseEntity를 상속받았기 때문에, save 메서드는 User 객체를
인자로 받아 User 객체를 반환합니다.
따라서, public <S extends BaseEntity> S save(S entity)에서 S는 BaseEntity의 하위 타입으로
제한된 제네릭 타입 매개변수이며, 이 매개변수를 메서드의 반환 타입과 매개변수 타입으로 사용하고 있습니다.
자바의 제네릭은 타입 안정성을 제공하고 코드 재사용성을 높이는 데 매우 유용합니다. 이번 포스팅에서는 제네릭을 이용한 다양한 예제를 살펴보겠습니다.
1. 제네릭 클래스
// 제네릭 클래스 Box 선언
class Box<T> {
// 제네릭 타입 T의 변수를 선언
private T value;
// 제네릭 타입 T의 값을 설정하는 메서드
public void set(T value) {
this.value = value;
}
// 제네릭 타입 T의 값을 반환하는 메서드
public T get() {
return value;
}
}
// Box 클래스의 인스턴스를 String 타입으로 생성
Box<String> stringBox = new Box<>();
// Box에 "Hello" 문자열을 설정
stringBox.set("Hello");
// Box에서 값을 꺼내어 String 타입 변수에 저장
String str = stringBox.get();
위 예제는 Box라는 제네릭 클래스를 정의합니다. 이 클래스는 T라는 타입 파라미터를 사용하여,
어떤 타입이든 저장할 수 있는 박스를 만듭니다. set 메서드를 통해 값을 설정하고, get
메서드를 통해 값을 가져올 수 있습니다. 이 예제에서는 Box<String>을 사용하여 문자열을
저장하고 가져옵니다.
2. 제네릭 메서드
public class GenericMethodExample {
// 제네릭 메서드 선언, <T>는 이 메서드가 제네릭 타입 T를 사용함을 선언
public static <T> void printArray(T[] array) {
// 배열의 각 요소를 출력
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Integer 타입 배열 선언
Integer[] intArray = {1, 2, 3, 4, 5};
// String 타입 배열 선언
String[] strArray = {"A", "B", "C"};
// 제네릭 메서드 호출
printArray(intArray); // 출력: 1 2 3 4 5
printArray(strArray); // 출력: A B C
}
}
printArray 메서드는 제네릭 메서드로, 배열의 요소를 출력합니다. <T>는 이 메서드가
제네릭 타입 T를 사용함을 선언하며, T 타입의 배열을 인자로 받아 각 요소를 출력합니다.
3. 제네릭 인터페이스
// 제네릭 인터페이스 Container 선언
interface Container<T> {
void add(T item);
T get(int index);
}
// 제네릭 인터페이스를 구현한 클래스 ContainerImpl
class ContainerImpl<T> implements Container<T> {
private List<T> items = new ArrayList<>();
// 아이템을 추가하는 메서드
public void add(T item) {
items.add(item);
}
// 인덱스로 아이템을 가져오는 메서드
public T get(int index) {
return items.get(index);
}
}
// Container 인터페이스의 구현체를 String 타입으로 생성
Container<String> stringContainer = new ContainerImpl<>();
// "Hello" 문자열을 추가
stringContainer.add("Hello");
// 첫 번째 아이템을 가져옴
String item = stringContainer.get(0);
Container 인터페이스는 제네릭 타입 T를 사용하여 정의되며, ContainerImpl 클래스가
이를 구현합니다. 이 예제에서는 Container<String>을 사용하여 문자열을 저장하고 가져옵니다.
4. 제네릭 타입 제한 (상한)
// Number 클래스를 상속받는 제네릭 클래스 선언
public class BoundedTypeExample<T extends Number> {
private T number;
// 생성자에서 제네릭 타입의 값을 설정
public BoundedTypeExample(T number) {
this.number = number;
}
// number 값을 출력하는 메서드
public void printNumber() {
System.out.println(number);
}
public static void main(String[] args) {
// Integer 타입으로 BoundedTypeExample 인스턴스 생성
BoundedTypeExample<Integer> integerExample = new BoundedTypeExample<>(123);
// Double 타입으로 BoundedTypeExample 인스턴스 생성
BoundedTypeExample<Double> doubleExample = new BoundedTypeExample<>(123.45);
// 각각의 값 출력
integerExample.printNumber(); // 출력: 123
doubleExample.printNumber(); // 출력: 123.45
}
}
BoundedTypeExample 클래스는 Number 클래스를 상속받는 타입으로 제한된 제네릭 클래스를 정의합니다.
이 클래스는 Number 타입 또는 이를 상속받는 타입만 인자로 받을 수 있으며, 값을 출력하는 메서드를 제공합니다.
5. 제네릭 타입 제한 (하한)
public class LowerBoundedTypeExample {
// 하한 제한을 적용한 제네릭 메서드
public static void printList(List<? super Integer> list) {
// 리스트의 각 요소를 출력
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
// Number 타입 리스트 선언 및 값 추가
List<Number> numberList = new ArrayList<>();
numberList.add(1);
numberList.add(2.5);
// 제네릭 메서드 호출
printList(numberList); // 출력: 1, 2.5
}
}
printList 메서드는 Integer의 하위 타입인 List를 인자로 받아 각 요소를 출력합니다.
이 메서드는 리스트의 요소가 최소한 Integer 타입이거나 이를 상속받은 타입임을 보장합니다.
6. 와일드카드 제네릭
public class WildcardExample {
// 와일드카드 제네릭을 사용하는 메서드
public static void printList(List<?> list) {
// 리스트의 각 요소를 출력
for (Object obj : list) {
System.out.println(obj);
}
}
public static void main(String[] args) {
// String 타입 리스트 선언 및 값 추가
List<String> stringList = Arrays.asList("A", "B", "C");
// Integer 타입 리스트 선언 및 값 추가
List<Integer> intList = Arrays.asList(1, 2, 3);
// 제네릭 메서드 호출
printList(stringList); // 출력: A, B, C
printList(intList); // 출력: 1, 2, 3
}
}
printList 메서드는 와일드카드를 사용하여 어떤 타입의 리스트든 인자로 받아 출력할 수 있습니다.
이 메서드는 리스트의 요소 타입에 관계없이 모든 리스트를 출력할 수 있습니다.
7. 제네릭 타입의 배열 생성
public class GenericArray<T> {
private T[] array;
// 제네릭 배열 생성 (타입 안정성 경고 억제)
@SuppressWarnings("unchecked")
public GenericArray(int size) {
// 제네릭 배열 생성은 타입 안정성 문제로 인해 경고가 발생할 수 있음
array = (T[]) new Object[size];
}
// 배열의 특정 인덱스에 값을 설정하는 메서드
public void set(int index, T value) {
array[index] = value;
}
// 배열의 특정 인덱스에서 값을 가져오는 메서드
public T get(int index) {
return array[index];
}
public static void main(String[] args) {
// String 타입으로 GenericArray 인스턴스 생성
GenericArray<String> stringArray = new GenericArray<>(10);
// 배열의 첫 번째 요소에 "Hello" 설정
stringArray.set(0, "Hello");
// 배열의 첫 번째 요소 값을 출력
System.out.println(stringArray.get(0)); // 출력: Hello
}
}
GenericArray 클래스는 제네릭 타입의 배열을 생성하고 관리할 수 있는 기능을 제공합니다.
제네릭 배열 생성은 타입 안정성 문제로 경고가 발생할 수 있지만, 이를 억제하여 사용합니다.
8. 제네릭 타입의 비교
// 제네릭 클래스 Compare 선언, T는 Comparable 인터페이스를 구현해야 함
public class Compare<T extends Comparable<T>> {
private T first;
private T second;
// 생성자에서 두 값을 설정
public Compare(T first, T second) {
this.first = first;
this.second = second;
}
// 두 값 중 큰 값을 반환하는 메서드
public T getMax() {
return first.compareTo(second) > 0 ? first : second;
}
public static void main(String[] args) {
// Integer 타입으로 Compare 인스턴스 생성
Compare<Integer> compare = new Compare<>(3, 5);
// 큰 값 출력
System.out.println(compare.getMax()); // 출력: 5
}
}
Compare 클래스는 Comparable 인터페이스를 구현하는 제네릭 타입을 사용하여 두 값을 비교하고,
그 중 큰 값을 반환합니다. T는 Comparable을 구현해야 하기 때문에 compareTo 메서드를 사용할 수 있습니다.
9. 제네릭 생성자
public class GenericConstructor {
private double value;
// 제네릭 생성자 선언, T는 Number 클래스를 상속받아야 함
public <T extends Number> GenericConstructor(T value) {
this.value = value.doubleValue();
}
// value 값을 출력하는 메서드
public void printValue() {
System.out.println(value);
}
public static void main(String[] args) {
// Integer 타입 값을 사용하는 GenericConstructor 인스턴스 생성
GenericConstructor gc = new GenericConstructor(123);
// value 값 출력
gc.printValue(); // 출력: 123.0
}
}
GenericConstructor 클래스는 제네릭 생성자를 통해 Number 타입 또는 이를 상속받는 타입의 값을
받아 double로 저장합니다. 생성자는 제네릭 타입을 사용하여 다양한 숫자 타입을 처리할 수 있습니다.
10. 제네릭 클래스의 타입 파라미터 제한
// 기본 Shape 클래스
class Shape { }
// Shape 클래스를 상속받은 Circle 클래스
class Circle extends Shape { }
// Shape 클래스를 상속받은 Rectangle 클래스
class Rectangle extends Shape { }
// 제네릭 클래스 ShapePrinter 선언, T는 Shape를 상속받아야 함
class ShapePrinter<T extends Shape> {
// 제네릭 타입 T의 shape 객체를 출력하는 메서드
public void printShape(T shape) {
System.out.println("Printing shape: " + shape.getClass().getName());
}
}
// ShapePrinter 인스턴스를 Circle 타입으로 생성
ShapePrinter<Circle> circlePrinter = new ShapePrinter<>();
circlePrinter.printShape(new Circle()); // 출력: Printing shape: Circle
// ShapePrinter 인스턴스를 Rectangle 타입으로 생성
ShapePrinter<Rectangle> rectanglePrinter = new ShapePrinter<>();
rectanglePrinter.printShape(new Rectangle()); // 출력: Printing shape: Rectangle
ShapePrinter 클래스는 Shape 클래스를 상속받는 타입만을 처리할 수 있도록 제한된 제네릭 클래스를
정의합니다. printShape 메서드는 제네릭 타입 T의 객체를 받아 그 타입의 이름을 출력합니다.