안녕하세요. 명월입니다.
이 글은 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)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'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 |