[JAVA] Generic
Generic
Generic 을 사용하는 이유
-
코드 재사용성 증가: 동일한 로직을 다양한 타입에 대해 반복해서 작성하지 않아도 됨
→
List<String>List<Integer>등 동일한List로직 재사용 -
타입 안정성 향상: 컴파일 타임에 타입 체크 가능
→ 잘못된 타입 사용 시 컴파일 에러 발생 → 런타임 오류 방지
-
불필요한 캐스팅 제거: 형 변환이 필요없어 가독성 ⬆️, 오류 가능성 ⬇️
→
String s = (String) list.get(0);대신에String s = list.get(0); - 유연한 API 설계 가능: 사용할 타입을 외부에서 받아오기 때문에 다양한 상황에 대응 가능
-
한번에 여러 타입 매개변수를 선언 가능
→
Map<K,V>
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
public class StringBox {
//문자를 보관하고 꺼내는 단순한 기능
private String value;
public void set(String value) {
this.value = value;
}
public String get() {
return value;
}
}
public class IntegerBox {
//숫자를 보관하고 꺼내는 단순한 기능
private Integer value;
public void set(Integer value) {
this.value = value;
}
public Integer get() {
return value;
}
}
public static void main(String[] args) {
IntegerBox integerBox = new IntegerBox();
integerBox.set(111); //오토 박싱으로 int -> Integer 자동변환
Integer integer = integerBox.get();
System.out.println("integer = " + integer);
StringBox stringBox = new StringBox();
stringBox.set("hello");
String string = stringBox.get();
System.out.println("string = " + string);
//double, boolean 을 포함한 다양한 타입을 담는 박스가 필요하다면 각각의 타입별로 클래스를 새로 만들어야 한다
//담는 타입이 수십개라면 어떻게 이 문제를 해결할까?
}
다형성으로 중복 해결 시도
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
public class ObjectBox {
private Object value;
public void set(Object object) {
this.value = object;
}
public Object get() {
return this.value;
}
}
public static void main(String[] args) {
ObjectBox integerBox = new ObjectBox();
integerBox.set(10);
Object object = (Integer) integerBox.get(); //다운 캐스팅
System.out.println("integer = " + object);
ObjectBox stringBox = new ObjectBox();
stringBox.set("hello");
String str = (String) stringBox.get(); //다운 캐스팅
System.out.println("str = " + str);
//잘못된 타입의 인수 전달
integerBox.set("문자100");
Integer result = (Integer) integerBox.get(); //String -> Integer 캐스팅 예외
System.out.println("result = " + result);
}
문제점
-
반환 타입이 맞지 않음
→
Integer = Object자식은 부모를 담을 수 없다, 즉 성립하지 않는다. 다운캐스팅 필요 -
잘못된 타입의 인수 전달
→ 값을 꺼낼 때 문제 발생
-
타입 안정성 문제 발생
제네릭 사용
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
public class GenericBox<T> { //<T>를 선언하면 제네릭 클래스 T를 타입 매개변수라 함.
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
public static void main(String[] args) {
//GenericBox<T>는 원하는 타입(T)으로 만들 수 있음
//생성 시점에 T의 타입 결정
GenericBox<Integer> integerBox = new GenericBox<>();
/*integerBox.set("1000"); //Integer 타입만 허용, 컴파일 오류*/
integerBox.set(1000);
Integer integer = integerBox.get(); //Integer 타입 반환 (캐스팅 X)
System.out.println("integer = " + integer);
GenericBox<String> stringBox = new GenericBox<>();
stringBox.set("스트리 잉");
/*stringBox.set(2000); //String 타입만 허옹*/
String string = stringBox.get();
System.out.println("string = " + string);
GenericBox<Double> doubleBox = new GenericBox<>();
doubleBox.set(100.0); //Double 타입만 허옹*/
System.out.println("doubleValue = " + doubleBox.get());
//타입 직접 입력
GenericBox<Integer> integerBox2 = new GenericBox<Integer>();
//타입 추론: 생성하는 제네릭 타입 생략 가능
GenericBox<Integer> integerBox3 = new GenericBox<>();
}
//출력 결과
integer = 1000
string = 스트리 잉
doubleValue = 100.0
제네릭 클래스는 생성시점에 **
1
new GenericBox<Integer>()
제네릭 클래스를 사용하면
GenericBox객체를 생성하는 시점에 원하는 타입을 지정할 수 있다.
한번에 여러 타입 매개변수를 선언 가능
1
public class GenericBox<K, V> {}
GenericBox<Integer> 와 같은 코드는 실제로 만들어지는게 아니라 컴파일러에서 내가 입력한 타입 정보를 기반으로 이러한 코드가 있다 가정하고 컴파일 과정에 타입 정보를 반영해준다. 이 과정에서 타입이 맞지 않으면 컴파일 오류 발생
제네릭 타입 매개변수와 타입 인자
메서드의 매개변수는 사용할 값에 대한 결정을 나중으로 미루고, 제네릭 타입 매개변수는 사용할 타입에 대한 결정을 나중으로 미루는 것 메서드는 매개변수에 인자를 전달해 사용하는 값 결정
제네릭 클래스는 타입 매개변수에 타입 인자를 전달해 사용할 타입 결정
제네릭 타입(Generic Type) → GenericBox<T>
타입 매개변수(type parameter) → GenericBox<T> 의 T
타입 인자(type argument) → GenericBox<Integer> 의 Integer
타입 인자로 기본형은 사용할 수 없다. 래퍼 클래스(Integer, Double)를 사용해야 한다.
제네릭 메서드
제네릭 타입과 제네릭 메서드는 제네릭을 사용하지만 서로 다른 기능 제공.
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
public class GenericMethod {
public static Object objectMethod(Object obj) {
System.out.println("objectMethod " + obj);
return obj;
}
//재네릭 메서드를 정의할 땐 메서드의 반환 타입 왼쪽 <>를 사용해 <T>와 같이 타입 매개변수를 적어줌
public static <T> T geneticMethod(T t) {
System.out.println("geneticMethod " + t);
return t;
}
/**
* 제네릭 메서드도 타입 매개변수 제한 가능 Number와 그 자식만 받을 수 있음!
*/
public static <T extends Number> T numberMethod(T t) {
System.out.println("numberMethod " + t);
return t;
}
}
/**
* 제네릭 메서드는 인스턴스 메서드와 스테틱 메서드에 모두 적용할 수 있당
*/
class StBox<T> { //제네릭 타입
static <V> V staticMethod(V v) {return v;} //static 메서드에 제네릭 메서드 도입
<Z> Z instanceMethod(Z z) {return z;} //인스턴스 메서드에 제네릭 메서드 도입 가능
}
class Box<T> {
/**
* 제네릭 타입은 static 메서드에 타입 매개변수를 사용할 수 없다.
* 제네릭 타입은 객체를 생성하는 시점에 타입이 정해지지만
* static 메서드는 인스턴스 단위가 아니라 클래스 단위로 작동하기 때문에 제네릭과 무관
*/
T instanceMethod(T t) {return t;} //인스턴스 메서드에선 가능
//static T staticMethod(T t) {return t;} 재네릭 타입의 T 사용 불가
}
- 클래스 전체가 아닌, 메서드 단위로 제네릭을 도입할 때 사용
- 메서드를 호출하는 시점에
<Object>와 같이 타입을 정하고 호출 - 인스턴스 메서드와
static메서드 모두 적용 가능 - 제네릭 타입보다 제네릭 메서드가 높은 우선순위를 가짐
메서드를 호출하는 시점에 타입 인자를 전달해 타입을 지정한다. 즉 타입을 지정하면서 메서드를 호출
제네릭 메서드의 타입 매개변수 제한
1
2
3
4
5
6
7
/**
* 제니릭 메서드도 타입 매개변수 제한 가능 Number와 그 자식만 받을 수 있음!
*/
public static <T extends Number> T numberMethod(T t) {
System.out.println("numberMethod " + t);
return t;
}
제네릭 메서드 실행 순서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1. 전달
WhildcardEx.printGenericV1(dogBox);
//제네릭 메서드
//2. Box<Dog> dogBox를 전달. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
//Box<T> box 제네릭 타입을 받아 출력
System.out.println("T = " +box.get());
}
//3. 타입 인자 결정
static <Dog> void printGenericV1(Box<Dog> box) {
System.out.println("T = " +box.get());
}
//4. 최종 실행 메서드
static void printGenericV1(Box<Dog> box) {
System.out.println("T = " +box.get());
}
와일드 카드
프로그래밍에서 * , ? 과 같이 하나 이상의 문자들을 상징하는 특수 문자를 뜻한다. 즉 여러 타입이 들어올 수 있음.
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
public class WhildcardEx {
//제네릭 메서드
//Box<Dog> dogBox를 전달. 타입 추론에 의해 타입 T가 Dog가 된다.
static <T> void printGenericV1(Box<T> box) {
//Box<T> box 제네릭 타입을 받아 출력
System.out.println("T = " +box.get());
}
//제네릭 메서드 X 일반적인 메서드
//Box<Dog> dogBox를 전달. 와일드 카드 ?는 모든 타입을 받을 수 있음.
//?만 사용한 다 받는 와일드카드를 비제한 와일드카드라 함.
static void printWildcardV1(Box<?> box) {
// ? = 아무거나 ex) Box<Dog> Box<Cat> Box<Object>
System.out.println("? = " +box.get());
}
static <T extends Animal> void printGenericV2(Box<T> box) {
//T에 상한을 Animal로 해서 Animal의 기능 사용 가능
T t = box.get();
System.out.println("이름 = " +t.getName());
}
static void printWildcardV2(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " + animal.getName());
}
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
//T에 상한을 Animal로 해서 Animal의 기능 사용 가능
T t = box.get();
System.out.println("이름 = " +t.getName());
return t; //반환까지
}
- 와일드 카드는
?를 사용해 정의 - 와일드 카드는 제네릭 타입이나, 제네릭 메서드를 선언하는 것이 아니다.
- 와일드 카드는 제네릭 타입을 활용할 때 사용.
비제한 와일드카드
? 만 사용해서 제한 없이 모든 타입을 다 받는 와일드카드
상한 와일드 카드
1
2
3
4
5
6
7
//와일드카드에도 상한을 걸 수 있다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " +animal.getName());
return animal; //반환까지
}
//와일드 카드는 이미 만들어진 지네릭 타입을 가져다 활용할 때 사용
- 제네릭 메서드와 같이 와일드카드에도 상한 제한 가능
? extends Animal사용 즉Animal과 그 하위 타입만 입력 받는다.box.get();을 통해 꺼낼 수 있는 타입의 최대부모는Animal따라서Animal타입으로 조회 가능- 즉
Animal타입의 기능을 호출할 수 있다.
하안 와일드 카드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Box<Object> objectBox = new Box<>();
Box<Animal> animalBox = new Box<>();
Box<Dog> dogBox = new Box<>();
Box<Cat> catBox = new Box<>();
// Animal 포함 상위 타입 전달가능
writeBox(objectBox); //가능
writeBox(animalBox); //가능
//writeBox(dogBox); //하한이 Animal
//writeBox(catBox); //하한이 Animal
Animal animal = animalBox.get();
System.out.println("animal = " + animal );
}
static void writeBox(Box<? super Animal> box) { //Animal 이상만 가능
box.set(new Dog("멍멍이",100));
}
와일드 카드 실행 순서
1
2
3
4
5
6
7
8
9
10
11
//1. 전달
WhildcardEx.printWildcardV1(dogBox);
//2. 최종 실행 메서드
//제니릭 메서드 X 일반적인 메서드
//Box<Dog> dogBox를 전달. 와일드 카드 ?는 모든 타입을 받을 수 있음.
//?만 사용한 다 받는 와일드카드를 비제한 와일드카드라 함.
static void printWildcardV1(Box<?> box) {
// ? = 아무거나 ex) Box<Dog> Box<Cat> Box<Object>
System.out.println("? = " +box.get());
}
타입 매개변수가 필요한 경우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static <T extends Animal> T printAndReturnGeneric(Box<T> box) {
//T에 상한을 Animal로 해서 Animal의 기능 사용 가능
T t = box.get();
System.out.println("이름 = " +t.getName());
return t; //반환까지
}
//와일드카드에도 상한을 걸 수 있다.
static Animal printAndReturnWildcard(Box<? extends Animal> box) {
Animal animal = box.get();
System.out.println("이름 = " +animal.getName());
return animal; //반환까지
}
//전달한 타입을 반환 가능
Dog dog = WildcardEx.printAndReturnGeneric(dogBox)
//전달한 타입 반환 불가 Animal 타입으로 반환
Animal animal = WildcardEx.printAndReturnWildcard(dogBox)
- 메서드의 타입을 특정 시점에 변경하려면 제네릭 타입이나, 제네릭 메서드 사용
- 와일드카드는 이미 만들어진 제네릭 타입을 받아 활용할때 사용
- 메서드의 타입들을 타입인자를 통해 변경 불가 (일반적인 메서드라 생각)
제네릭 메서드가 필요한 상황이면
<T>를 사용 그렇지 않으면 와일드 카드 사용
제네릭 메서드 vs 와일드카드
타입 매개변수가 존재하는 제네릭 메서드는 특정 시점에 타입 매개변수에 타입 인자를 전달 해 타입을 결정해야 한다. 이런 과정은 매우 복잡하다. 반면 와일드 카드를 사용하는 printWildcardV1 메서드는 일반적인 메서드에 사용가능하고 매개변수로 제네릭 타입을 받을 수 있다는 것 뿐 제네릭 메서드보다 덜 복잡하게 동작하지 않는다.
단순 일반 메서드에 제네릭 타입을 받을 수 있는 매개변수가 있을 뿐
제네릭 타입이나 제네릭 메서드가 꼭 필요한 상황이 아니라면 와일드 카드를 사용하자!
타입 이레이저
- 제네릭은 자바 컴파일에서만 사용하고 컴파일 이후에는 삭제. 즉 제네릭 타입 매개변수가 사라짐
- 컴파일 전
.Java에는 제네릭의 타입 매개변수가 존재하지만 컴파일 이후 자바 바이트코드.class에는 타입 매개변수가 존재하지 않음. - 따라서 런타임에 타입을 활용하는 코드는 작성 불가