August 29, 2020
추가 참고글
컴파일시 타입을 체크해 주는 기능(compile-time type check) - JDK1.5부터
한계란?
package test;
import java.util.ArrayList;
public class GenericTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("30"); //사실 숫자만 넣고 싶었는데 String 넣었음에도 구분못함
Integer i = (Integer)list.get(2); // 컴파일은 OK, 그러나 실행시에 에러가 발생한다.
System.out.println(list);
}
}
ClassCastException(형변환)
실행에러가 난다. 왜냐하면 list.get()의 모든 타입은 Object 라서 (Integer)로 형변환이 가능하다는 로직 때문에(Object->String 형변환 가능) 컴파일이 가능하다. 하지만 실제로는 list.get(2)의 형은 “30”, 즉 String
이므로 Integer
로 형변환이 불가능하므로 에러 발생. 컴파일러는 실제로 안에 뭐가(String, Interger 등) 들어있는지 체크하지 못하므로 한계.Generics
. 컴파일러에게 타입 정보면 제공해주면 가능.package test;
import java.util.ArrayList;
public class GenericTest {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(10);
list.add(20);
//list.add("30"); // 컴파일러가 에러를 잡아준다. String은 들어갈 수 없음
list.add(30); //지네릭스 덕분에 타입 체크가 강화됨
Integer i = list.get(2); //원래는 list.get()의 리턴이 Object라서 형 변환해줘야 하지만. 형변환도 생략가능. 애초에 Integer밖에 못 들어오는 것을 알고 있기에.
System.out.println(list);
}
}
<Object>
옵션을 주면 된다. 애초에 ArrayList는 Object를 넣기 때문에 안 넣어도 되지 않나? 아니다. JDK 1.5부터는 반드시 명시해야 한다.ArrayList<Object> list = new ArrayList<Object>();
클래스 안에 Object 타입이 있는 것들은 일반클래스
-> 지네릭클래스
로 변경되었다. 지네릭 타입을 반드시 써줘야 하는 클래스들이 있다. 타입을 꼭 지정해 줘야 한다.
<E>
)은 지네릭클래스다.<E>
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv()) //ok
tvList.add(new Audio()) // 컴파일 에러. Tv 외에 다른 타입은 저장 불가
<E>
를 선언해서 사용<E>
, <T>
이외에 다른 것들 <X>
, <AB>
등 여러가지 다 써도 된다.<E>
대신 실제 타입 <Tv>
을 지정(대입)ArrayList<Tv> tvList = new ArrayList<Tv>();
<Tv>
는 일치해야 한다.public class ArrayList<E> extends AbstractList<E>{
private transient E[] elementData;
public boolean add(E o) { /* 내용생략 */}
public E get(int index) { /* 내용생략 */}
...
}
public class ArrayList extends AbstractList<Tv>{
private transient Tv[] elementData;
public boolean add(Tv o) { /* 내용생략 */}
public Tv get(int index) { /* 내용생략 */}
...
타입 변수 대신 실제 타입이 지정되면, 형변환 생략가능
원래는 아래와 같았다.
ArrayList tvList = new ArrayList();
tvList.add(new Tv());
Tv t = (Tv)tvList.get(0); // 꺼낼 때의 타입이 Object이므로
하지만, 아래와 같이 형변환 생략가능
ArrayList<Tv> tvList = new ArrayList<Tv>();
tvList.add(new Tv());
Tv t = tvList.get(0);
Box<T>
T의 Box
또는 T Box
라고 읽는다.T
Box
<String>
이 되기 전의 그냥 Arrayex1) class Box<T>
{ }
<T>
- 지네릭클래스ex2)
Box<Stirng> b = new Box<String>();
<String>
은 대입된 타입(매개변수화된 타입, parameterized type). 참조변수의 타입과 생성자의 타입 <String>
은 일치되어야 한다.<String>
말고 다른 타입을 적용해도 된다.<String>
는 지네릭 타입 호출참조 변수와 생성자의 대입된 타입은 일치해야 한다.
(클래스 관계)
class Product {}
class Tv extends Product {}
class Audio extends Product {}
ArrayList<Tv> list = new ArrayList<Tv>(); //OK. 일치
ArrayList<Product> list = new ArrayList<Tv>(); //에러, 불일치(다형성 성립 안됨) 즉 부모 - 조상 관계라도 안된다
public static void printAll(ArrayList<Product> list) {
for(Product p : list)
System.out.println(p);
}
(메인함수)
public static void main(String[] args) {
ArrayList<Product> productList = new ArrayList<Product>();
ArrayList<Tv> tvList = new ArrayList<Tv>();
List<Tv> tvList2 = new ArrayList<Tv>(); // OK. 다형성
// public boolean add(Product e) {}로 변환됨
productList.add(new Tv());
productList.add(new Audio()); // Audio가 Product의 자손이므로 가능
// public boolean add(Tv e) {}로 변환됨
tvList.add(new Tv());
tvList.add(new Audio()); // 안됨. Tv와 Audio는 아무 관계 없으므로
printAll(tvList); //컴파일 에러가 난다
}
List<Tv> list = new ArrayList<Tv>(); //OK. 다형성. ArrayList가 List를 구현
List<Tv> list = new LinkedList<Tv>(); //OK. 다형성. LinkedList가 List를 구현
매개변수의 다형성도 성립
boolean add(E e) {...}
-> boolean add(Product e) {...}
로 변환되기 때문. 매개변수에는 Product와 그 자손들이 다 들어갈 수 있다.ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); //OK, 하지만 list.get(1)로 꺼낼 때는 타입이 Product이다.
list.add(new Audio()); //OK
Product p = list.get(0); // OK. 형변환이 필요없음
Tv t = (Tv)list.get(1); // (Tv)로 형변환 해줘야 한다.
Tv t = (Tv)List.get(1)
에서 (Tv)로 형변환 하는 이유는?
E get(int index) {...}
-> Product get(int index) {...}
이므로 get의 타입은 Object가 아니라 Product 이기 때문에<E>
클래스를 작성할 때, Object타입 대신 T와 같은 타입 변수를 사용
public interface Iterator {
boolean hasNext();
Object next(); //next()의 리턴이 Object 타입
void remove();
}
Iterator it = list.iterator();
while(it.hasNext()){
Student s = (Student)it.next(); //형변환 필요
...
}
public interface Iterator<E>{
boolean hasNext();
E next(); // next()의 리턴이 Student 타입
void remove();
}
Iterator<Student> it = list.iterator(); // 타입 명시
while(it.hasNext()){
Student s = (Student)it.next(); //형변환 필요 X
...
}
public class test {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<Student>();
list.add(new Student("자바왕", 1, 1));
list.add(new Student("자바짱", 1, 2));
list.add(new Student("홍길동", 3, 4));
Iterator<Student> it = list.iterator();
//Iterator it = list.iterator();
while(it.hasNext()) {
// System.out.println((Student).it.next()).name);
System.out.println(it.next()).name); // 훨씬 간결
}
}
}
HashMap<String, Student> map = new HashMap<String, Student>(); // 생성
map.put("자바왕", new Student("자바왕", 1, 1, 100, 100, 100)); // 데이터 저장
public class HashMap<K, V> extends AbstractMap<K, V> {
...
public V get(Object key) { 내용생략 }
public V put(K key, V value) { 내용생략 }
public V get(Object key) { 내용생략 }
...
}
public class HashMap extends AbstractMap {
...
public Student get(Object key) { 내용생략 }
public Student put(Student key, Student value) { 내용생략 }
public Student get(Object key) { 내용생략 }
...
}
Student s1 = (Student)map.get("1-1");
가 Student s1 = map.get("1-1");
로 가능하다(형 변환 필요없음)HashMap<String, Student> map = new HashMap<>();
class FruitBox<T extends Fruit>{ // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
}
public class Test {
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<Apple>(); //OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러. Toy는 Fruit의 자손이 아님
}
}
implements가 아니다
)interface Eatable {}
class FruitBox<T extends Eatable> {...} // Eatable도 인터페이스
interface Eatable {}
class Fruit implements Eatable {
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 Test {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
FruitBox<Grape> grapeBox = new FruitBox<Grape>();
// FruitBox<Grape> grapeBox = new FruitBox<Apple>(); //에러, 앞 뒤 타입 불일치
// FruitBox<Toy> toyBox = new FruitBox<Toy>(); // 에러, FruitBox는 Fruit를 상속받아야 하며 Eatable를 구현한 것만 들어올 수 있다. Toy는 해당 안됨
Box<Toy> toyBox2 = new Box<Toy>(); // OK
fruitBox.add(new Fruit()); //Box클래스에서 add(T item)메서드가 add(Fruit item) 으로 변경되기 때문에 다형성에 의해 Fruit포함 자손들 다 add 매개변수로 들어올 수 있다.
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);
}
}
class FruitBox<T extends Fruit & Eatable> extends Box<T> {} //클래스(Fruit)와 인터페이스(Eatable) 둘 다 구현할 때는 , 로 연결하면 안되고 &로 구분해야 한다.
//사실 위는 class FruitBox<T extends Fruit> extends Box<T> {} 와 똑같다. Fruit이 이미 Eatable을 구현했으므로 Fruit만 상속받더라도 Eatable을 구현한 것과 같은 결과
class Box<T> {
ArrayList<T> list = new ArrayList<T>(); // item을 저장할 list
void add(T item) {list.add(item);} //박스에 item을 추가
T get(int i) {return list.get(i);} //박스에서 item을 꺼낼 때
int size() {return list.size();}
public String toString() {return list.toString();}
}
class Box<T> {}
Box<Apple> appleBox = new Box<Apple>(); //OK. Apple객체만 저장가능
Box<Grape> grapebox = new Box<Grape>(); //OK. Apple객체만 저장가능
제약1. static멤버에 타입 변수 사용 불가
class Box<T> {
static T item; // 에러
static int compare(T t1, T t2) {...} // 에러
...
}
제약2. 객체 생성할 때(배열 생성할 때 등) 타입 변수 사용불가. 타입 변수로 배열 선언은 가능. new 연산자 뒤에 T못 쓴다. (new 연산자는 뒤의 타입이 확정되어 있어야 하는데, new 연산자 뒤에 T를 쓰면 뭐가 오는지 확실하게 모르므로)
class Box<T>{
T[] itemArr; //지네릭 배열 선언은 OK. T타입의 배열을 위한 참조변수
...
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가, new T 자체가 불가하다.
...
}
}
와일드 카드를 사용하면 참조변수의 타입과 생성자의 타입이 불일치 해도 가능하다
ArrayList<? extends Product> list = new ArrayList<Tv>(); // OK
ArrayList<? extends Product> list = new ArrayList<Audio>(); // OK
와일드 카드의 3가지 용법
<? extends T>
와일드 카드의 상한 제한. T와 그 자손들만 가능(가장 많이 쓰임)<? super T>
와일드 카드의 하한 제한. T와 그 자손들만 가능<?>
제한없음. 모든 타입이 가능 <? extends Object>와 동일static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp : "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
..(중략)..
//아래 둘 다 가능. 만약 makeJuice(FruitBox<Fruit> box)로 정의되어 있으면 매개변수로 new FruitBox<Fruit>() 만 들어갈 수 있다. (일치하는 것만 가능). 즉, 매개변수 FruitBox<? etends Fruit> box 는 FruitBox<Fruit>와 Fruitbox<Apple> 2가지 모두를 포함한다.(자신 포함 자식들도 들어갈 수 있으니)
System.out.println(Juicer.makeJuice(new FruitBox<Fruit>()));
System.out.println(Juicer.makeJuice(new FruitBox<Apple>()));
static <T> void Sort(List<T> list, Comparator<? super T> c)
클래스의 타입 매개변수<T>
와 메서드의 타입 매개변수 <T>
는 별개
<T>
와 메서드에 있는 <T>
는 별개다. 전자에 String, 후자에 Integer로 따로 설정 가능class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c){
...
}
}
메서드를 호출할 때마다 타입을 대입해야(대부분 생략 가능)
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Fruit> fruitBox = new FruitBox<Apple>();
...
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); //여기서 <Fruit>은 생략가능. 위의 new FruitBox<Fruit>(); 에서 이미 <Fruit> 이라고 이미 명시했기 때문에.
System.out.println(Juicer.<Apple>makeJuice(appleBox));//여기서 <Apple>은 생략가능. 위의 new FruitBox<Apple>>(); 에서 이미 <Apple> 이라고 이미 명시했기 때문에.
...
//밑의 파라미터 부분의 FruitBox<T>를 정의하는 것이 static 바로 뒤에 <T extends Fruit>임
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
String tmp : "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
System.out.println(<Fruit>makeJuice(fruitBox)); //에러, 클래스 이름 생략불가
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK
지네릭 메서드 vs 와일드카드 메서드
지네릭 메서드
는 지네릭 클래스처럼 호출할 때마다 다른 타입을 대입할 수 있는 것이며, 와일드카드
는 하나의 참조변수로 대입된 타입이 다른 여러 지네릭 객체를 다룰 수 있게 하기 위한 것. (용도가 조금 다르다)지네릭 메서드
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String tmp : "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
와일드카드 메서드
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp : "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
지네릭 타입과 원시 타입 간의 형변환은 바람직하지 않다.(경고 발생)
class Box<T> {}
로 선언이 되어있는데 타입을 대입하지 않고 Box만 쓰면 안된다라는 것. 원시타입과 지네릭 타입을 섞어서 쓰지 말기Box<Object> objBox = null;
Box box = (Box)objBox; // OK. 지네릭 타입(Box<Object>) -> 원시 타입(Box). 가능하지만 경고 발생
objBox = (Box<Object>)box; // OK. 원시타입(Box) -> 지네릭 타입(Box<Object>). 가능하지만 경고 발생
Box b = null;
Box<String> bStr = null;
b = (Box)bStr; // Box<String> -> Box 가능, 그러나 경고
bStr = (Box<String>)b; // Box -> Box<String> 가능, 그러나 경고
Box b = new Box<String>();
이 가능. 왜냐하면 Box b = (Box)new Box<String>();
처럼 (Box)가 생략이 되어 있기 때문에. b는 대입된 타입이 없는 원시타입이기 때문에 b.add(new Integer(100));
가능하다. 그래서 Box<String> b = new Box<String>();
으로 수정하면 b.add(new Integer(100));
이 에러가 뜬다.Box<Object> objBox = null;
Box<String> bStr = null;
objBox = (Box<Object>)strBox; // 에러. Box<String> -> Box<Object> 안됨
strBox = (Box<String>)objBox; // 에러. Box<Object> -> Box<String> 안됨
Box<String> b = new Box<Object>();
이 안된다는 말은 Box<String> b = (Box<String>)new Box<Object>();
처럼 형변환이 안된다는 말이다.Box<Object>objBox = (Box<Object>)new Box<String>(); // 에러, 형변환 불가능
Box<? extends Ojbect> wBox = (Box<? extends Object>)new Box<String>(); // OK, 형변환 가능
Box<? extends Object> wBox = new Box<String>(); //OK, 위 문장과 동일(형변환이 생략되어 있는 것임). String은 Object의 자손이므로 가능
FruitBox<? extends Fruit> abox = new FruitBox<Apple>();
FruitBox<Apple> appleBox = (FruitBox<Apple>)aBox; // OK. 경고발생
지네릭 타입의 경계(bound)를 제거
class Box<T>{
void add(T t) {
...
}
}
class Box{
void add(Object obj){
...
}
}
또,
class Box<T extends Fruit>{
void add(T t) {
...
}
}
class Box{
void add(Fruit t){
...
}
}
T get(int i) {
return list.get(i); // (Fruit)list.get(i)처럼 타입 T(ex. Fruit)로 형변환이 생략되어 있음
}
T get(int i)
에서의 T가 컴파일에 의해서 Fruit으로 변환)Fruit get(int i) {
return (Fruit)list.get(i);
}
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f : box.getList()) // box.getList()가 Fruit과 그 자손이므로 형변환 필요x(와일드 카드 덕분)
tmp += f + " ";
}
stataic Juice makeJuice(FruitBox box){
String tmp = "";
Iterator it = box.getList().iterator();
while(it.hasNext()){
tmp += (Fruit)it.next() + " "; // 형변환 필요
}
}
참고 : 자바의 정석 - 남궁성