안녕하세요. 명월입니다.
이 글은 Java에서의 제네릭 타입(Generic type)에 대한 글입니다.
우리가 클래스의 내부 맴버 변수의 데이터 작성할 때, 보통 데이터 타입을 설정합니다.
그러나 이 데이터 타입을 작성할 때 사용하는 것이 아니고 클래스를 선언할 때 외부에서 선언해서 사용할 때가 있을 수도 있습니다.
Copy! [소스 보기] LinkedStack.javapublic 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 함수의 파라미터 데이터 타입의 차이뿐인데 클래스를 계속 만들어 내는 것 조금 아깝습니다.
그래서 여기서 사용하는 타입이 제네릭 타입입니다. 제네릭 약이랑 뜻이 비슷하네요.. 껍데기만 만들어 놓고 내용물은 외부에서 설정하는 것입니다.
Copy! [소스 보기] LinkedStack.java// 클래스 옆에 <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의 사용법
제네릭은 클래스에서만 사용하는 것이 아니고 함수에서도 사용 가능합니다.
Copy! [소스 보기] Example.java// 제네릭을 이용한 인터페이스 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 클래스에 제네릭을 지정했습니다. 그리고 함수에서는 인터페이스의 제네릭 타입으로 반환값의 타입을 설정했습니다.
제네릭은 데이터 타입을 특정 클래스로 한정할 수도 있습니다.
Copy! [소스 보기] Example.java// 일반 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)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Study > Java' 카테고리의 다른 글
[Java] 21. 어노테이션 (Annotation) (0) | 2020.05.25 |
---|---|
[Java] 20. iterator(for-each)과 Stream API (0) | 2020.05.20 |
[Java] 19. 람다식(Lambda)를 사용하는 방법 (0) | 2020.05.19 |
[Java] 18. 익명 클래스(Anonymous class)와 클로저(closure) (0) | 2020.05.18 |
[Java] 16. 예외처리(try~catch~finally, throw)를 하는 방법 (0) | 2020.05.14 |
[Java] 15. 열거형(이진 데이터 비트 연산자 사용 예제) (0) | 2020.05.13 |
[Java] 14. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성) (0) | 2020.05.12 |
[Java] 13. 추상 클래스(abstract) 그리고 상속 금지(final) (0) | 2020.05.11 |