안녕하세요, 여행벌입니다.

오늘은 자바 5에서 처음 소개가 되어 자바 8까지 상당히 많은 내용이 발전된 문법 '제네릭' 에 대해서 포스팅해보도록 하겠습니다.

기본 문법 구조부터 얻는 이점, 특징까지 차근차근 정리해보겠습니다.


제네릭(Generics)

 제네릭이란 말 그대로 "일반화" 입니다. 자바에서의 제네릭은 "자료형"을 일반화하는 문법입니다.

 즉, 자료형을 일반화시켜서 자료형에 의존적이지 않은 클래스나 메소드를 정의할 수 있습니다.

제네릭클래스(Generics Class)

 제네릭은 자료형을 일반화시킨다고 했습니다. 자료형을 정하긴 정해야하는데 그럼 어떻게 정하길래, 자료형을 일반화시킨다고 말할까요??

 제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법입니다.

 제네릭 기법을 이용해 Box 라는 이름의 클래스를 만들어 보았습니다. 자료형을 일반화시키기 위해 T 라는타입매개변수를 이용했고, 인스턴스를 생성할 때, 외부에서 자료형을 지정하는 모습을 볼 수 있습니다.

 또, 클래스 이름인 Box 옆에 <T> 라고 기술해줘야 "T는 인스턴스 생성 시 자료형을 결정할 거에요." 라고 자바에게 알려 줄 수 있습니다.

 이처럼 제네릭 기법을 활용하면 자료형에 의존적이지 않은 클래스를 만들어서 사용할 수 있습니다.

제네릭 기본 용어

T : 타입매개변수(Type Parameter)

Box<String> 에서 String : 타입 인자(Type Argument)

Box<String> : 매개변수화 타입(Parameterized Type)     이라고 부릅니다.

타입매개변수 규칙

 제네릭을 이용하기 위해서는 3가지 타입매개변수 규칙을 지켜줘야 합니다.

1. 타입매개변수는 한 문자로 이름을 짓는다.

2. 타입매개변수의 이름은 대문자로 짓는다.

3. 타입매개변수에는 기본 자료형을 넘겨줄 수 없다.

 보통 T(Type), V(Value), N(Number), K(Key), E(Element) 를 많이 이용하고 Box<int> 와 같이 타입매개변수에 기본 자료형을 넘겨줄 수 없습니다. 하지만, Wrapper 클래스를 이용하면 되기 때문에 문제는 없습니다.

타입인자 생략

1
Box<String> box1 = new Box<String>();
cs

 인스턴스를 생성할 때 위와 같이 타입 인자를 지정해줘야 합니다. 하지만, 아래와 같이 타입 인자를 생략할 수도 있습니다.

1
Box<String> box1 = new Box<>();
cs

 자바컴파일러가 Box<String> box1 을 보고 타입 인자가 String 인 것을 유추할 수 있기 때문입니다.

제네릭클래스 생성자

 제네릭클래스도 당연히 클래스 생성자가 있어야합니다. 일반적인 클래스 생성자와 문법은 동일합니다. 

1
2
3
4
5
6
class Box<T>{
    T box;
    public Box(T box) {
        this.box = box;
    }
}
cs

제네릭클래스 이점

 분명, 자바의 모든 객체는 Object 를 상속한다고 했습니다. 클래스나 메소드를 Object 자료형으로 만든다면, 자료형에 얽매이지않는 클래스나 메소드를 만들 수 있지 않을까요?? 물론, Object를 이용해서 제네릭 문법을 활용한 클래스와 동일한 작업을 하도록 구현 할 수 있습니다.

 그렇다면 제네릭 클래스를 사용하면 무슨 이점이 있을까요??

 

 똑같은 작업을 제네릭클래스와 Object를 이용한 클래스로 각각 구현해나가며 알아보겠습니다.

 

1) 과일을 담을 수 있는 Box를 만든다.

2) Box에 어떤 과일을 담는다.

3) Box에 담긴 과일을 꺼낸다.

 

 먼저, 일반적인 클래스를 정의하고 이를 이용해 Box 에 Apple 을 담았다가 꺼내보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Box{
    Object o;
    public Box(Object o) {
        this.o = o;
    }
    Object get() {
        return o;
    }
}
 
class Apple{ }
class Strawberry{ }
class Melon { }
 
public class test{
    public static void main(String args[]) {
        Box appleBox = new Box(new Apple());
        Apple apple1 = (Apple)appleBox.get();
    }
}
 
cs

 자바의 모든 객체는 Object를 상속한다는 점을 이용해 Object를 담는 Box를 만들었습니다. 그 후, Apple을 Box에 담았다가, Box에서 get 메소드를 이용해 꺼내보았습니다.

 우리는 Box에 어떤 값도 넣을 수 있고, 어떤 값도 꺼낼 수 있기를 원하기 때문에 Object로 잘 구현했습니다. 그럼, 이 코드의 불편한 점은 무엇일까요?? 바로 get 메소드를 이용할 때 형변환을 항상 해야된다는 점입니다! 우리는 Box에 Apple 클래스를 넣을 수도 있고, Melon 클래스를 넣을 수도 있습니다. 따라서 get 메소드에서는 Object를 return 해주게되고, 우리가 원하는 자료형으로 다시 형 변환을 해야 한다는 불편함이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Box<T>{
    T box;
    public Box(T box) {
        this.box = box;
    }
    T get() {
        return box;
    }
}
 
class Apple{ } 
class Strawberry{ } 
class Melon { }
 
public class test{
    public static void main(String args[]) {
        Box<Apple> appleBox = new Box(new Apple());
        Apple apple1 = appleBox.get();
    }
}
cs

제네릭 클래스를 이용한다면 위와 같이 형 변환 없이도 같은 기능을 구현할 수 있습니다.

 

 또, 제네릭 클래스는 다음과 같은 이점이 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Box{
    Object o;
    public Box(Object o) {
        this.o = o;
    }
    Object get() {
        return o;
    }
}
 
class Apple{ }
class Strawberry{ }
class Melon { }
 
public class test{
    public static void main(String args[]) {
        Box box1 = new Box("Apple");
        box1.get();
    }
}
cs

 우리는 Box 클래스를 정의할 때, "과일을 담는 박스" 라고 정의했습니다. 하지만, 지금 "Apple" 이라는 문자열을 Box에 저장하고 get() 메소드를 이용하고 있습니다. 문법적으로 문제가 있을까요?? String 도 Object 를 상속하기 때문에 개발자의 의도와 전혀 다르게 Box가 사용되고 있지만 컴파일 단계에서 문제가 드러나지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Box<T>{
    T box;
    public Box(T box) {
        this.box = box;
    }
    T get() {
        return box;
    }
}
 
class Apple{ }
class Strawberry{ }
class Melon { }
 
public class test{
    public static void main(String args[]) {
        Box<Apple> box1 = new Box<>("Apple");
        box1.get();
    }
}
cs

 제네릭 클래스를 이용하면 <Apple> 을 담을 Box를 만들기 때문에 위와 같이 문자열을 잘못 입력하면 컴파일 단계에서 에러가 드러납니다.

1
2
3
4
Exception in thread "main" java.lang.Error: Unresolved compilation problem: 
    Cannot infer type arguments for Box<>
 
    at HelloWorld/Hello.test.main(test.java:19)
cs

 제네릭 클래스를 이용하면 위에서 보았듯이, 형 변환을 하지 않아도 되고, 자료형과 관련된 실수가 컴파일 단계에서 드러난다는 굉장히 큰 이점이 있습니다.

제네릭 메소드

 메소드도 클래스와 마찬가지로 제네릭 문법을 이용해서 "자료형"에 의존적이지 않은 메소드를 만들 수 있습니다. 기본 구조는 다음과 같습니다.

1
<T> 반환형 메소드이름(매개변수)
cs

 <T> 를 통해서 이 메소드는 제네릭 메소드입니다! 라고 자바 머신에게 알려줍니다.

 그럼 제네릭 메소드를 활용해서 위에서 만들었던 제네릭 클래스 Box의 내용물을 꺼내오는 메소드를 만들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Box<T>{
    T box;
    public Box(T box) {
        this.box = box;
    }
    T get() {
        return box;
    }
}
 
class Opener{
    public static <T> T open(Box<T> box) {
        return box.get();
    }
}
 
class Apple{ }
class Strawberry{ }
class Melon { }
 
public class test{
    public static void main(String args[]) {
        // box1 에 Apple 을 담았습니다.
        Box<Apple> box1 = new Box<>(new Apple());
        // Opener 클래스의 open 메소드를 통해 상자에 담겨있는 Apple을 꺼냅니다.      
        Apple apple1 = Opener.open(box1);
    }
}
cs

 제니릭 클래스를 매개변수로 받아 안에 있는 내용물을 return 해주는 open 제네릭 메소드를 만들었습니다. 우리가 기존에 알던 메소드와 구조적으로 크게 다른 점은 없으나 "자료형" 을 일반화 시킬 수 있습니다. 즉, 박스에 어떤 자료형이 담겨있어도 꺼내올 수 있는 메소드입니다.


이번 포스팅에서는 제네릭 클래스의 기본 문법 구조 위주로 알아보았습니다.

다음 포스팅에서는 더 나아가 고급 제네릭 기법에 대해서도 다뤄보겠습니다.

 

 

 

+ Recent posts