제네릭은 어떻게 선언하고 사용해?

현정재·2024년 6월 29일
1

제네릭(Generic)은 프로그래밍 언어에서 타입(Type)에 독립적인 코드를 작성할 수 있도록 하는 기법입니다.
제네릭을 사용하면 데이터 타입이나 클래스를 정의할 때 실제 사용될 타입을 지정하지 않고, 추상적인 타입으로 남겨둘 수 있습니다.
이를 통해 코드의 재사용성을 높이고, 타입 안정성을 유지하면서 일반적인 알고리즘을 구현할 수 있습니다.

주로 제네릭은 다음과 같은 장점을 제공합니다:

  • 재사용성: 한 번 작성한 제네릭 코드는 다양한 타입에 대해 재사용할 수 있습니다.
  • 타입 안정성: 컴파일 시점에서 타입 체크를 할 수 있어 런타임 에러를 방지할 수 있습니다.
  • 성능: 제네릭은 타입 변환을 줄여 성능을 향상시킬 수 있습니다.
  • 가독성: 코드의 가독성을 높여 유지 보수를 용이하게 합니다.

Java 제네릭 타입 매개변수 선언과 사용 방식

제네릭 타입의 위치와 선언 방식은 사용 용도에 따라 다릅니다. 메서드 선언 시 제네릭 타입 매개변수를 선언하는
방식과 클래스나 인터페이스 선언 시 제네릭 타입 매개변수를 선언하는 방식에는 차이가 있습니다.
이를 명확히 이해하기 위해 다음과 같은 사항을 살펴보겠습니다.

클래스나 인터페이스 선언 시

제네릭 클래스나 인터페이스를 제네릭으로 선언할 때는 다음과 같이 합니다:

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);
}

제네릭 메서드와 클래스의 차이

클래스/인터페이스 수준에서 제네릭:

  • 제네릭 타입 매개변수는 클래스나 인터페이스 선언에 붙습니다.
    예: class Box, interface Comparable

메서드 수준에서 제네릭:

  • 제네릭 타입 매개변수는 메서드 선언에 붙습니다.
    예: public void print(T value)

Map 예제

제네릭 타입을 사용하는 예제 중 하나가 Map 인터페이스입니다:

Map<Integer, String> map = new HashMap<>();
여기서 Map<Integer, String>Map 인터페이스가 두 개의 제네릭 타입 매개변수 KV를 사용한다는 것을 의미합니다.

메서드의 제네릭 타입 매개변수

다음은 메서드 선언에서 제네릭 타입 매개변수를 선언하고 사용하는 예제입니다:

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)에서 SBaseEntity의 하위 타입으로
제한된 제네릭 타입 매개변수이며, 이 매개변수를 메서드의 반환 타입과 매개변수 타입으로 사용하고 있습니다.

제네릭 예제 모음

자바의 제네릭은 타입 안정성을 제공하고 코드 재사용성을 높이는 데 매우 유용합니다. 이번 포스팅에서는 제네릭을 이용한 다양한 예제를 살펴보겠습니다.

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 인터페이스를 구현하는 제네릭 타입을 사용하여 두 값을 비교하고, 
그 중 큰 값을 반환합니다. TComparable을 구현해야 하기 때문에 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의 객체를 받아 그 타입의 이름을 출력합니다.







profile
wonttock

0개의 댓글