Java Generic(제네릭)

1. Generic(제네릭)이란?

제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어듭니다.

타입 안정성을 높인다는 것은 의도 하지 않은 타입의 객체가 저장되는 것을 막고 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여줄 수 있습니다.

즉, 타입 안정성을 제공하며 타입체크와 형변환을 생략할 수 있습니다. 성능상으로도 타입을 체크할 필요가 없기때문에 이점을 가지고 있습니다.

제네릭을 사용하지않으면 불편한점

1
2
3
4
5
6
7
8
9
10
11
12
package JavaGrammar.Generic;
import java.util.ArrayList;
import java.util.List;

public class Generic_blog {
public static void main(String[] args) {
List arrList = new ArrayList<>();
arrList.add("");
int num = (int) arrList.get(0);

}
}

제네릭을 사용하면 좋은점

1
2
3
4
5
6
7
8
9
10
class GenericType<T>{
T t;
public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}

이런식으로 제네릭을 선언해놓으면 타입명시하지 않아도 필요할때마다 형을 반환시킬 수 있게됩니다. 타입의 최소화를 기대할 수 있습니다.

타입 인자

타입 내용
<T> Type
<E> Element
<K> Key
<N> Number
<V> Value
<R> Result

2. Generic Interface

인터페이스를 선언하여 구현체를 해당클래스에서 타입을 지정할 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface GenericInterface<T>{
T getTypeNumber();
}

class GenericType<T>{
T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}
class GenericInterfaceClass implements GenericInterface<Integer>{
int num;
@Override
public Integer getTypeNumber() {
return this.num;
}
}

3. Generic Extends

상속관게에서 제네릭을 활용하여 <T extends Class명> 형식으로 해당 타입에 맞게 객체를 핸들링을 진행할 수도 있습니다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

class Fruit1{
@Override
public String toString() {
return "Fruit{}";
}
}
class Apple1 extends Fruit1{
@Override
public String toString() {
return "Apple{}";
}
}
class Grape1 extends Fruit1{
@Override
public String toString() {
return "Grape{}";
}
}
class Toy1 extends Fruit1{
@Override
public String toString() {
return "Toy{}";
}
}

public class FruitBox1 {
public static void main(String[] args) {
Box1<Fruit1> fruitBox1 = new Box1<Fruit1>();
Box1<Apple1> fruitBox2 = new Box1<Apple1>();
Box1<Grape1> fruitBox3 = new Box1<Grape1>();
Box1<Toy1> fruitBox4 = new Box1<Toy1>();
//Box<Grape> fruitBox5 = new Box<Apple>(); // 에러타입 불일치

fruitBox1.add(new Fruit1());
fruitBox1.add(new Apple1()); // 타입 실패
System.out.println("fruitBox1 = " + fruitBox1);
fruitBox2.add(new Apple1());
fruitBox2.add(new Apple1());
// fruitBox2.add(new Toy()); apple만 담을 수 있음.
System.out.println("fruitBox2 = " + fruitBox2);

fruitBox4.add(new Toy1());
// fruitBox4.add(new Apple()); toy만 담을 수 있음

System.out.println("fruitBox4 = " + fruitBox4);
}
}
class Box1<T extends Fruit1>{
List<T> list = new ArrayList<T>();

public Box1() {
}

void add(T item){
list.add(item);
}
T get(int i){
return list.get(i);
}
int size(){
return list.size();
}

@Override
public String toString() {
return "Box{" +
"list=" + list +
'}';
}
}

4. Generic 와일드카드

  • ? extends T 와일드 카드의 상한 제한 - T와 그 자손들만 가능합니다.
  • ? super T 와일드카드 하한 제한 - T와 그 조상들만 가능합니다.
  • 모든 타입가능 ? extends Object와 같은 의미를 가지고 있습니다.

T로 전달되는 이름은 Eatable Interface를 직접적으로, 혹은 간접적으로 구현한 이름만 올 수 있습니다. 즉, 제네릭 클래스의 타입 인자를 인터페이스로 제한을 할 수 있습니다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package JavaGrammar.Generic;
import java.util.ArrayList;
import java.util.List;

/**
* ? extends T 와일드 카드의 상한 제한 - T와 그 자손들만 가능
* ? super T 와일드카드 하한 제한 - T와 그 조상들만 가능
* <?>모든 타입가능 ? extends Object와 같은 의미
*/

class Fruit2 implements Eatable{
@Override
public String toString() {
return "Fruit{}";
}
}
class Apple2 extends Fruit2{
@Override
public String toString() {
return "Apple{}";
}
}
class Grape2 extends Fruit2{
@Override
public String toString() {
return "Grape{}";
}
}
class Toy2{
@Override
public String toString() {
return "Toy{}";
}
}
// 여기에 &기호로 T타입이랑 연결한다. Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.
interface Eatable{

}
public class FruitBox2 {
public static void main(String[] args) {
Box2<Fruit2> fruitBox1 = new Box2<Fruit2>();
Box2<Apple2> fruitBox2 = new Box2<Apple2>();
Box2<Grape2> fruitBox3 = new Box2<Grape2>();
// Box2<Toy2> fruitBox4 = new Box2<Toy2>();
//Box<Grape> fruitBox5 = new Box<Apple>(); // 에러타입 불일치

fruitBox1.add(new Fruit2());
fruitBox1.add(new Apple2()); // 타입 실패
System.out.println("fruitBox1 = " + fruitBox1);
fruitBox2.add(new Apple2());
fruitBox2.add(new Apple2());
// fruitBox2.add(new Toy()); apple만 담을 수 있음.
System.out.println("fruitBox2 = " + fruitBox2);

//fruitBox4.add(new Toy2());
// fruitBox4.add(new Apple()); toy만 담을 수 있음
//System.out.println("fruitBox4 = " + fruitBox4);
}
}
// Frit의 자손이면서 Eatable을 구현한 클래스만 타입 매개변수 T에 대입될 수 있다.
class Box2<T extends Fruit2 & Eatable>{
List<T> list = new ArrayList<T>();

public Box2() {
}

void add(T item){
list.add(item);
}
T get(int i){
return list.get(i);
}
int size(){
return list.size();
}

@Override
public String toString() {
return "Box{" +
"list=" + list +
'}';
}
}

5. Generic 제한

  • 타입변수는 인스턴스 변수로 간주되기 떄문에 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수를 사용할 수 없습니다.
  • 제네릭 배열 타입의 참조변수는 선언할 수 있으나 제네릭 배열을 생성하는 것은 불가능합니다.