Java의 정석 의 책을 읽고 정리한 내용입니다.
다양한 타입의 객체들을 메서드나 컬렉션 클래스에 컴파일 시의(
compile-time type check
)를 해주는 기능
🔔 지네릭스의 장점
(1) 타입 안정성을 제공한다.
(2) 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
class Box {
Object item;
void setItem(Object item) { this.item = item; }
Object getItem() { return item; }
}
➡️
class Box<T> {// 지네릭 타입 T를 선언
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
Box< T >
에서 타입 변수
라고 하며, Type
의 첫 글자에서 따왔다.Map<K,V>
와 같이 콤마,
를 구분자로 나열하면 된다.K
는 Key
(키)를 의미하고, V
는 Value
(값)을 의미한다.f(x, y) = x + y 가 f(k, v) = k + v
와 같이 이들은 기호의 종류만 다를 뿐 임의의 참조형 타입
을 의미한다는 것은 모두 같다.
지네릭 클래스가 된 Box
클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자에 타입 T
대신에 사용될 실제 타입을 지정해줘야 한다.
Box<String> b = new Bow<String>(); // 타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); // 에러. String이외의 타입은 지정불가
b.setItem("ABC"); // OK. String이므로 가능
String item = b.getItem(); // 형변환이 필요없음
✔️ 지네릭스의 용어
class Box<T> {}
Box<T> : 지네릭 클래스. 'T의 Box' 또는 `T Box`라고 읽는다.
T : 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box : 원시타입(raw type)
Box<String> b = new Box<String>();
Box<String>
: 타입 매개변수에 타입을 지정하는 것String
: 지정된 타입 String
와 같이
컴파일 후 Box<String>
과 Box< Integer>
는 이들의 원시 타입
인 Box
로 바뀐다. → 즉, 지네릭 타입이 제거된다.
✔️ 지네릭스의 제한
Box
의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다.static
멤버에 타입 변수 T
를 사용할 수 없다. T
는 인스턴스변수로 간주되기 때문이다.class Box<T> {
T[] itemArr; // Ok. T타입의 배열을 위한 참조변수
...
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가
...
return tmpArr;
}
...
}
new T[10]
과 같이 배열을 생성하는 것은 안 된다는 뜻이다.
✏️ 지네릭 배열을 생성해야할 필요가 있을 때
new
연산자대신Reflection API
의newInstance()
와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나,Object
배열을 생성해서 복사한 다음에T[]
로 형변환하는 방법 등을 사용한다.
class Box<T> {
ArrayList<T> List = new ArrayList<T>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i) }
ArrayList<T> getList() { return list; }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
Box<T>
의 객체를 생성
Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // 에러
Box<Apple> appleBox = new FruitBox<Apple>(); // Ok. 다형성
FruitBox
는 Box
의 자손이라고 가정할 때
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple()); // Ok
appleBox.add(new Grape()); // 에러. Box<Apple>에는 Apple객체만 추가 가능
Box< T>
의 객체에 void add(T item)
으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit()); // Ok
fruitBox.add(new Apple()); // Ok void add(Fruit item)
T
가 Fruit
인 경우, void add(Fruit item)
가 되므로 Fruit
의 자손들은 이 메서드의 매개변수가 될 수 있다.Apple
이 Fruit
의 자손이라고 가정했다.
import java.util.ArrayList;
class Fruit { public String toString() { return "Fruit"; }}
class Apple extends Fruit { public String toString() { return "Apple"; }}
class Grape extends Fruit { public String toString() { return "Grape"; }}
class Toy { public String toString() { return "Toy"; }}
public class FruitBoxEx1 {
public static void main(String[] args) {
Box<Fruit> fruitBox = new Box<>(); // Box<Fruit> fruitBox = new Box<Fruit>();와 같음
Box<Apple> appleBox = new Box<Apple>();
Box<Toy> toyBox = new Box<Toy>();
// Box<Grape> grapeBox = new Box<Apple>(); // 에러, 타입불일치
fruitBox.add(new Fruit());
fruitBox.add(new Apple()); // OK. void add(Fruit item)
appleBox.add(new Apple());
appleBox.add(new Apple());
// appleBox.add(new Toy()); // 에러, Box<Apple>에는 Apple만 담을 수 있음
toyBox.add(new Toy());
// toyBox.add(new Apple()); // 에러, Box<Toy>에는 Apple을 담을 수 없음
System.out.println(fruitBox);
System.out.println(appleBox);
System.out.println(toyBox);
}
}
// 지네릭 클래스
class Box<T> {
ArrayList<T> list = new ArrayList<>(); // ArrayList<T> list = new ArrayList<T>();와 같음
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
int size() { return list.size(); }
public String toString() { return list.toString(); }
}
[Fruit, Apple]
[Apple, Apple]
[Toy]
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
extends
를 사용하면, 특정 타입의 자손만 대입할 수 있게 제한할 수 있다.
FruitBox<Apple> appleBox = nuew FruitBox<Apple>(); // Ok
FruitBox<Toy> toyBox = nuew FruitBox<Toy>(); // 에러. Toy는 Fruit의 자손이 아니다.
Fruit
클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
fruitBox.add(new Apple()); // Ok. Apple이 Fruit의 자손
fruitBox.add(new Grape()); // Ok. Grape가 Fruit의 자손
add()
의 매개변수의 타입 T
도 Fruit
와 그 자손 타입이 될 수 있으므로, 여러 과일을 담을 수 있다.
interface Eatable {}
class FruitBox<T extends Eatable> {...}
T
에 Object
를 대입하면, 모든 종류의 객체를 저장할 수 있게 된다.extend
를 사용한다. (implements
가 아닌)
class FruitBox<T extends Fruit & Eatable> {...}
Fruit
의 자손이면서 Eatable
인터페이스도 구현해야한다면 &
기호로 연결한다.FruitBox
에는 Fruit
의 자손이면서 Eatable
을 구현한 클래스만 타입 매개변수 T
에 대입될 수 있다.
import java.util.ArrayList;
class Fruit implements Eatalbe {
public String toString() {
return "Fruit";
}
}
class Apple extends Fruit {
public String toString() {
return "Apple";
}
}
class Grape extends Fruit {
public String toString() {
return "Grape";
}
}
class Toy {
public String toString() {
return "Toy";
}
}
interface Eatalbe {}
public class FruitBoxEx2 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
// FruitBox<Grape> grapeBox1 = new FruitBox<Apple>(); 타입 불일치
// FruitBox<Toy> toyBox = new FruitBox<>(); 에러. Fruit의 자손 아님
fruitBox.add(new Fruit());
// 매개변수의 타입T도 Fruit와 그 자손 타입이 될 수 있음
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
// appleBox.add(new Grape()); Grape는 Apple의 자손이 아님
grapeBox.add(new Grape());
System.out.println("fruitBox-" + fruitBox);
System.out.println("appleBox-" + appleBox);
System.out.println("grapeBox-" + grapeBox);
}
}
// 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 & 기호로 연결
// Fruit와 그 자손들만 대입가능 -> T extends Fruit
class FruitBox<T extends Fruit & Eatalbe> extends Box<T> {
}
class Box<T> {
ArrayList<T> list = new ArrayList<T>();
void add(T item) {
list.add(item);
}
T get(int i) {
return list.get(i);
}
int size() {
return list.size();
}
public String toString() {
return list.toString();
}
}
fruitBox-[Fruit, Apple, Grape]
appleBox-[Apple]
grapeBox-[Grape]
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) { //< <Fruit>으로 지정
String tmp = "";
for(Fruit f : box.getList()) tmp += f + "";
return new Juice(tmp);
}
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
...
System.out.println(Juicer.makeJuice(fruitBox)); // OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox)); // 에러. FruitBox<Apple>
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + "";
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f +"";
return new Juice(tmp);
}
메서드 중복 정의
이다.➡️ 이럴 때, 와일드 카드를 사용한다!
✏️ 와일드 카드
?
로 표현- 와일드 카드는 어떠한 타입도 될 수 있다.
extends
와 super
로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.
<? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
💡 참고
- 지네릭 클래스와 달리 와일드 카드에는
&
를 사용할 수 없다. ➡️ 즉,<? extends T & E>
와 같이 할 수 없다!- 매개변수의 타입을
FruitBox<? extends Object>
로 하면, 모든 종류의FruitBox
가 매개변수로 가능하다.
static <T> void sort(List<T> list, Comparator<? super T> c)
static
옆에 있는 <T>
는 메서드에 선언된 지네릭 타입이다.Comparator
이다.Comparator
의 지네릭 타입에 하한 제한이 걸려있는 와일드 카드가 사용되었다.
Compparator<? super Apple> : Comparater<Apple>, Comparator<Fruit>, Comparator<Object>
Comparator<? super Grape> : Comparator<Grape>, Comparator<Fruit>, Comparator<Object>
매개변수의 타입이 Comparator<? super Apple>
이라는 의미란?
Comparator
의 타입 매개변수로 Apple
과 그 조상이 가능하다는 뜻Comparator<Apple>
, Comparator<Fruit>
, Comparator<Object>
중의 하나가 두 번째 매개변수로 올 수 있다는 뜻이다.
지네릭 메서드 : 메서드의 선언부에 지네릭 타입이 선언된 선언된 메서드
Collections.sort()
: 지네릭 메서드- 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
static <T> void sort(List<T> list, Comparator<? super T>c)
💡 참고
지네릭 메서드는 지네릭 클래스가 아닌 클래스에도 정의될 수 있다!
class FruitBox<T> {
...
static <T> void sort(List<T> list, comparator<? super T> c) {
...
}
}
FruitBox
에 선언된 타입 매개변수 T
와 지네릭 메서드 sort()
에 선언된 타입 매개변수 T
는 타입 문자만 같을 뿐 서로 다른 것이다.static
멤버에는 타입 매개변수를 사용할 수 없지만, 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다!
💡 참고
내부 클래스에 선언된 타입 문자가 외부 클래스의 타입 문자와 같아도 구별될 수 있다.
이전 makeJuice()
메서드를
static Juice makeJuice(Fruit<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + "";
return new Juice(tmp);
}
➡️
지네릭 메서드로 변경
static <T extends Fruit> Juice makeJuice(Fruit<T> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + "";
return new Juice(tmp);
}
이제 지네릭 메서드를 호출할 때 이와 같다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
...
System.out.println(Juice.<Fruit>makeJuice(fruitBox));
System.out.println(Juice.<Apple>makeJuice(appleBox));
fruitBox
와 appleBox
의 선언부를 통해 대입된 타입을 컴파일러가 추정할 수 있다.System.out.println(Juice.<Fruit>makeJuice(fruitBox)); // 대입된 타입을 생략할 수 있다.
System.out.println(Juice.<Apple>makeJuice(appleBox));
⚠️ 주의할 점
System.out.println(<Fruit>makeJuice(fruitBox)); // 에러. 클래스 이름 생략불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // Ok
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // Ok
this
나 클래스 이름
을 생략하고 메서드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 반드시 써줘야 한다!
public static void printAll(ArrayList<? extends Product> list,
ArrayList<? extends Product> list2) {
for(Unit u : list) {
System.outprintln(u);
}
}
↔️
public static <T extends Product> void printAll(ArrayList<T> list,
ArrayList<T> list2) {
for(Unit u : list) {
System.outprintln(u);
}
}
✔️ 복잡하게 선언된 지네릭 메서드 예시
public static<T extends Comparable<? super T>> void sort(List<T> list)
List<T>
를 정렬한다는 것은 알겠지만, 메서드에 선언된 지네릭 타입이 조금 복잡하다!
public static<T extends Comparable<T>> void sort(List<T> list)
List<T>
의 요소가 Comparable
인터페이스를 구현한 것이어야 한다.
public static <T extends Comparable<? super T>> void sort(List<T> list)
(1)
List<T>
- 타입
T
를 요소로 하는List
를 매개변수로 허용한다.(2)
<T extends Comparable<? super T>>
T
는Comparable
을 구현한 클래스이어야 하며 (<T extends Comparable>
).T
또는 그 조상의 타입을 비교하는Comparable
이어야 한다는 것(Comparable<? super T>
)을 의미한다.- 만일
T
가Student
이고,Person
의 자손이라면,<? super T>
는Student
,Person
,Object
가 모두 가능하다.
Box<Object> objBox = null;
Box<String> strBox = null;
objBox = (Box<Object>)strBox; // 에러 발생 Box<String> -> Box<Object>
strBox = (Box<Object>)objBox; // 에러 발생 Box<Object> -> Box<String>
Box<? extends Object> wBox = new Box<String>)();
Box
가 Box<? extends Object>
로 형변환은 가능하다!
static Juice makeJuice(FruitBox<? extends Fruit> box) {...}
FruitBox<? extends Fruit> box = new FruitBox<Fruit>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Grape>(); // OK
makeJuice
메서드의 매개변수에 다향성 적용
Optional<?> EMPTY = new Optional<>(); // 에러. 미확인 타입의 객체는 생성불가
Optional<?> EMPTY = new Optional<Object>(); // OK
Optional<?> EMPTY = new Optional<>(); // OK. 위의 문장과 동일
<?>
는 <? extends Object>
를 줄여 쓴 것이며, <>
안에 생략된 타입은 Object
이다.class Box<T extends Fruit>
의 경우 Box<?> b = new Box<>;
는 Box<?> b = new Box<Fruit>;
이다.
✔️ 정리
Optional<Object> → Optional<T> // 형변환 불가능
Optional<Object> → Optional<?> → Optional<T> // 형변환 가능. 경고 발생
Optional<Object>
를 Optional<String>
으로 직접 형변환하는 것은 불가능하지만, 와일드 카드가 포함된 지네릭 타입으로 형변환하면 가능하다.
Box<? extends Object> objBox = null;
Box<? extends String> strBox = null;
strBox = (Box<? extends String>)objBox; // OK. 미확정 타입으로 형변환 경고
objBox = (Box<? extends Object>)strBox; // OK. 미확정 타입으로 형변환 경고
*.class
)에는 제네릭 타입에 대한 정보가 없는 것이다.
✏️ 기본적인 지네릭 타입의 제거과정
(1) 제네릭 타입의 경계(bound
)를 제거한다.
<T extends Fruit>
라면 T
는 Fruit
로 치환된다. <T>
인 경우는 T
는 Object
로 치환된다. 그리고 클래스 옆의 선언은 제거된다.class Box<T extends Fruit>{
void app(T t){
// ...
}
}
➡️
// 제네릭 타입의 경계 제거
class Box{
void app(Fruit t){
// ...
}
}
(2) 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
List
의 get()
은 Object
타입을 반환하므로 형변환이 필요하다.T get(int i){
return list.get(i);
}
➡️
// 제네릭 타입 제거 후 타입이 일치하지 않으면, 형변환을 추가한다.
Fruit get(int i){
return (Fruit)list.get(i);
}
와일드 카드가 포함되어 있는 경우
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp ="";
for(Fruit f : box.getList()) tmp += f +" ";
return new Juice(tmp);
}
➡️
static Juice makeJuice(FruitBox box){
String tmp ="";
Iterator it = box.getList().iterator();
while(it.hasNext()){
tmp += (Fruit)it.next() + " ";
}
return new Juice(tmp);
}
💡 참고
- 지네릭스 공부방법 : 다른 장을 공부하면서 부족하다고 느끼는 부분을 다시 복습하는 방식으로 학습하면서 이해의 폭을 넓혀가야 한다!