[Java] 17. 제네릭 타입(Generic type)


Study/Java  2020. 5. 15. 21:29

안녕하세요. 명월입니다.


이 글은 Java에서의 제네릭 타입(Generic type)에 대한 글입니다.


우리가 클래스의 내부 맴버 변수의 데이터 작성할 때, 보통 데이터 타입을 설정합니다.

그러나 이 데이터 타입을 작성할 때 사용하는 것이 아니고 클래스를 선언할 때 외부에서 선언해서 사용할 때가 있을 수도 있습니다.

public class LinkedStack {
  // 내부 Node 클래스
  class Node {
    // 데이터를 넣을 변수
    int data;
    // 다음 포인터
    Node next;
    // 생성자
    Node(int data, Node next) {
      this.data = data;
      this.next = next;
    }
  }
  // 현재 Node 포인터
  private Node pointer = null;
  // add 함수는 데이터를 넣는다.
  public void add(int data) {
    // 현재 포인터에 Node를 만들어 값을 넣는다.
    // 다음 pointer는 이전에 있던 pointer 값을 넣는다.
    pointer = new Node(data, pointer);
  }
  // pop 함수는 pointer의 값을 리턴한다.
  public int pop() {
    // Stack 알고리즘에 데이터가 없으면 null exception을 내보낸다.
    if (pointer == null) {
      // Null 예외 처리
      throw new NullPointerException();
    }
    try {
      // 현재 포인터에 있는 값을 리턴
      return pointer.data;
    } finally {
      // 포인터 이동
      pointer = pointer.next;
    }
  }
  // 현재 포인터가 null인지 확인하는 함수
  public boolean isNull() {
    return pointer == null;
  }
  // 실행 함수
  public static void main(String... args) {
    // 연결형 Stack 클래스 선언
    LinkedStack ex = new LinkedStack();
    // 값을 넣는다.
    ex.add(10);
    ex.add(20);
    ex.add(30);
    ex.add(40);
    int sum = 0;
    // 값을 모두 더한다.
    while(!ex.isNull()) {
      sum += ex.pop();
    }
    // 총합의 값 출력
    System.out.println("ex sum " + sum);
  }
}

위 예제는 단순하게 만든 연결 리스트형 Stack 알고리즘입니다. 일단 예제로서 리스트나 맵처럼 데이터를 삽입하고 꺼내서 출력합니다.

간단하게 add로 값을 넣으면 역순으로 pop이 되면서 값을 모두 더하고 출력하는 간단한 예제입니다.


실무에서 이렇게 잘 사용하고 있다가 String형으로 된 연결 리스트형 Stack 알고리즘을 사용하고 싶습니다.

그럼 위와 같은 클래스를 똑같이 만들어야 겠네요. 로직이 비슷하니 추상화 상속 작업을 통해 만들면 됩니다.

또 Double형이 사용하고 싶으면? 클래스를 만들고 추상화 상속 작업...


사실, Node의 data 맴버변수의 데이터 타입과 pop함수의 반환 타입, add 함수의 파라미터 데이터 타입의 차이뿐인데 클래스를 계속 만들어 내는 것 조금 아깝습니다.

그래서 여기서 사용하는 타입이 제네릭 타입입니다. 제네릭 약이랑 뜻이 비슷하네요.. 껍데기만 만들어 놓고 내용물은 외부에서 설정하는 것입니다.

// 클래스 옆에 <T>는 제네릭의 표시로서 예비 데이터 타입입니다.
public class LinkedStack<T> {
  // 내부 Node 클래스
  class Node {
    // 데이터를 넣을 변수 (데이터 타입을 외부에서 설정)
    T data;
    // 다음 포인터
    Node next;
    // 생성자
    Node(T data, Node next) {
      this.data = data;
      this.next = next;
    }
  }
  // 현재 Node 포인터
  private Node pointer = null;
  // add 함수는 데이터를 넣는다. (파라미터 데이터 타입은 외부에서 설정)
  public void add(T data) {
    // 현재 포인터에 Node를 만들어 값을 넣는다.
    // 다음 pointer는 이전에 있던 pointer 값을 넣는다.
    pointer = new Node(data, pointer);
  }
  // pop 함수는 pointer의 값을 리턴한다. (반환값의 데이터 타입은 외부에서 설정)
  public T pop() {
    // Stack 알고리즘에 데이터가 없으면 null exception을 내보낸다.
    if (pointer == null) {
      // Null 예외 처리
      throw new NullPointerException();
    }
    try {
      // 현재 포인터에 있는 값을 리턴
      return pointer.data;
    } finally {
      // 포인터 이동
      pointer = pointer.next;
    }
  }
  // 현재 포인터가 null인지 확인하는 함수
  public boolean isNull() {
    return pointer == null;
  }
  // 실행 함수
  public static void main(String... args) {
    // 연결형 Stack 클래스 선언
    // 내부 제네릭 데이터 타입은 Integer형
    LinkedStack<Integer> ex = new LinkedStack<>();
    // 값을 넣는다.
    ex.add(10);
    ex.add(20);
    ex.add(30);
    ex.add(40);
    int sum = 0;
    // 값을 모두 더한다.
    while (!ex.isNull()) {
      sum += ex.pop();
    }
    // 총합의 값 출력
    System.out.println("ex sum " + sum);
    // 내부 제네릭 데이터 타입은 String형
    LinkedStack<String> ex2 = new LinkedStack<>();
    // 값을 넣는다.
    ex2.add("a");
    ex2.add("b");
    ex2.add("c");
    // 값을 출력한다.
    while (!ex2.isNull()) {
      System.out.println("print - " + ex2.pop());
    }
  }
}

위 제네릭 타입을 보니 예전에 List나 Map에서 많이 사용하던 형태입니다.

링크 - [Java] 05. 배열과 List, Map의 사용법


제네릭은 클래스에서만 사용하는 것이 아니고 함수에서도 사용 가능합니다.

// 제네릭을 이용한 인터페이스
interface Callable<V> {
  V call();
}
// 인터페이스 상속, 제네릭 타입에 String을 선언
class Test implements Callable<String> {
  @Override
  public String call() {
    return "Hello world";
  }
}
public class Example {
  // 제네릭 함수, 제네릭 함수는 반환 타입 앞에 제네릭을 선언한다.
  public static <T> T test(Callable<T> func) {
    // 인터 페이스의 call 함수 호출
    return func.call();
  }

  // 실행 함수
  public static void main(String... args) {
    //Test 클래스
    Test test = new Test();
    // Test클래스는 제네릭이 String 타입이니 String타입으로 결과가 나온다.
    String data = test(test);
    // 콘솔 출력
    System.out.println("Test - " + data);
  }
}

인터페이스에 제네릭을 선언하고 상속받은 Test 클래스에 제네릭을 지정했습니다. 그리고 함수에서는 인터페이스의 제네릭 타입으로 반환값의 타입을 설정했습니다.


제네릭은 데이터 타입을 특정 클래스로 한정할 수도 있습니다.

// 일반 Sub 클래스
class Sub {
  // 맴버 변수
  private int data;
  // 생성자로 데이터를 받는다.
  public Sub(int data) {
    this.data = data;
  }
  // 출력한다.
  public int data() {
    return this.data;
  }
}
// 제네릭을 이용한 인터페이스
// T 제네릭은 Sub 클래스나 Sub 클래스를 상속 받는 데이터 타입으로 한정하빈다.
interface Callable<T extends Sub, V> {
  V call(T data);
}
// Callable 인터페이스를 상속
class Test implements Callable<Sub, String> {
  // T는 Sub 클래스를, V는 String으로 재설정한다.
  @Override
  public String call(Sub sub) {
    return "Parameter - " + sub.data();
  }
}
public class Example {
  // 실행 함수
  public static void main(String... args) {
    // Test 인스턴스 생성
    Test test = new Test();
    // Sub 인스턴스 생성
    Sub sub = new Sub(10);
    // Test 클래스의 call 함수에 sub 클래스를 넣으면 10이 나온다.(※이것이 디자인 패턴의 빌드 패턴)
    System.out.println(test.call(sub));
  }
}

굳이 제네릭을 안 쓴다고 해도 프로그램을 작성하지 못하는 것은 아닙니다. 그러나 Object타입을 쓸 함수의 파라미터 타입이나 반환 타입을 제네릭으로 설정해서 컴파일 단계에서 정합성을 체크하게 함으로써 프로그램의 품질을 많이 향상 시킬 수 있는 문법입니다.


여기까지 Java에서의 제네릭 타입(Generic type)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.