본문 바로가기

코딩/JAVA 공부

[JAVA] 자바의 정석 _ Generics

 * Generics(지네릭스)

- 컴파일 시 타입을 체크해주는 기능(compile-time type check) --> 런타임 에러(ClassCastException)를 컴파일 에러로 끌고 올 수 있음!

//Tv 객체만 자정할 수 있는ArrayList 생성
ArrayList<Tv> tvList = new ArrayList<Tv>();

tvList.add(new Tv()); // Ok
tvList.add(new Audio()); // 컴파일 에러.

- 형 변환의 번거로움을 줄여줌.

ArrayList list = new ArrayList;
list.add(1);
list.add(2);
Integer i = (Integer)list.get(1); // 꼭 형변환 필요, Object type을 반환하므로.
---------------------------------------------
ArrayList<Integer> list = new ArrayList<Integer>;
list.add(1);
list.add(2);
Integer i = list.get(1); // 형변환 필요 없음

** Generics 개념이 도입된 이후 (JDK 1.5 이후) Generics가 필요한 곳엔 (클래스에서 선언돼 있으면?) 반드시 적어줘야 함.

Object 객체를 넣는다고 해도 Object를 적어줘야 함 (그래서 ArrayList 선언할 때 밑줄이 있었구나!)

 

 * 타입 변수

- Generics 클래스 작성 시, Object타입 대신 타입 변수(E) 선언해 사용

public class ArrayList extends AbstractList{ // 일반 클래스
    private transient Object[] elementData;
    public boolean add(Object o){...}
    public Object get(int index){...}
    ...
}
---------------------------------------------- // 위 아래 코드 내용은 동일(Object를 타입변수 E로 선언)
public class ArrayList<E> extends AbstractList<E>{ // 지네릭 클래스
    private transient E[] elementData;
    public boolean add(E o){...}
    public E get(int index){...}
    ...
}

- 이후 객체 생성 시, 타입 변수(E) 대신 실제 타입을 지정해야 함 (대입)

참조 변수와 생성자에 지정해야 하며 두 개가 같아야 함. (와일드카드 사용하면 다를 수 있음)

 

 - 지네릭스 용어 : 지네릭 클래스, 원시 타입, 타입 변수

지네릭스 용어

- 지네릭 타입 호출 : Box<String> b = new Box<String>();

String은 언제든지 변경 가능!

 

-지네릭 타입과 다형성 

지네릭 클래스 간, 매개변수의 다형성은 성립

// 지네릭 클래스 간의 다형성
List<Tv> list = new ArrayList<Tv>(); //Ok

// 매개변수의 다형성
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv());	// Tv 가 Product의 자손이면 Ok
list.add(new Audio());	// Audio가 Product의 자손이면 Ok
 // 대신 get할 때 Product형을 리턴하므로, 이걸 Tv로 받으려면 형변환 필요
 Tv tv = list.get(1); // 오류! get으로 불러오는건 Product이므로.
 Tv tv = (Tv)list.get(1); // Ok

 * 지네릭 클래스 예시

 - Iterator<E>

public interface Iterator{ // 일반 클래스
    boolean hasNext();
    Object next();
    void remove();
}

Iterator it = list.iterator();
    while(it.hasNext()){
    	Student s = (Student)it.next(); // 형변환 O
        ...
    }
-----------------------------------
public interface Iterator<E>{ // 지네릭스 클래스
    boolean hasNext();
    E next();
    void remove();
}

Iterator<Student> it = list.iterator();
    while(it.hasNext()){
    	Student s = it.next();  // 형변환 X
        ...
    }

- HashMap<K,V>

여러 개의 타입 변수가 필요한 경우 콤마를 구분자로 선언.

HashMap의 key, value의 타입 각각 지정 가능.

public class HashMap<K,V> extends AbstractMap<K,V>{
    public V get(Object key){...}
    public V put(K key, V value){...}
    public V remove(Object key){...}
    ...
}

HashMap<String, Student> map = new HashMap<String, Student>();
map.put("자바왕", new Student("자바왕",1,1,100,100,100,100));

 

 * 제한된 지네릭 클래스

- extends로 타입 변수를 제한할 수 있음 (인터페이스의 경우에도 extends 사용! implements 사용 X)

 + 인터페이스를 extends 할 때 콤마(,)가 아니라 &로 작성 필요

class FruitBos<T extends Fruit>{
    ArrayList<T> list = new ArrayList<T>();
    ...
}
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // Ok
FruitBox<Toy> toyBos = new FruitBox<Toy>(); // 컴파일 에러! Toy는 Fruit의 자손이 아님

 * 지레닉스의 제약

- 타입 변수에 대입은 인스턴스 별로 다르게 가능.

1. static멤버에 타입 변수 사용 불가!

 static 멤버는 모든 인스턴스에 공통인데, 인스턴스마다 다른 타입 변수 사용 불가.

2. 배열, 객체 생성할 때 타입 변수 사용불가.

class Box<T>{
    T[] itemArr; // Ok T타입의 배열을 위한 참조변수
    ...
    T[] toArray(){
    	T[] tmpArr = new T[itemArr.length]; // 에러! 타입 변수로 배열, 객체 생성은 불가.
    }
}

 * 와일드카드 <?>

- 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능 --> 타입 변수가 일치하지 않아도 Ok!

ArrayList<? extends Product> list = new ArrayList<Tv>(); //Ok
ArrayList<? extends Product> list = new ArrayList<Audio>(); //Ok
ArrayList<Product> list = new ArratList<Tv>(); // 에러!

<? extends T> : 와일드카드의 상한 제한, T와 그 자손들만 가능

<? super T>    : 와일드카드의 하한 제한, T와 그 조상들만 가능

<?>                 : 제한 없음. 모든 타입이 가능

 

- 메서드의 매개변수에 와일드카드 사용 가능

와일드카드 사용으로 매개변수에 data 타입이 T의 자손인 경우 다 들어올 수 있게 됨.

static Juice makeJuice(FruitBox<? extends Fruit> box){
    String tmp = "";
    for(Fruit f: box.getList()) tmp += f + " ";
    return new Juice(tmp);

System.out.println(Juicer.makeJuice(new FruitBox<Fruit>()));
System.out.println(Juicer.makeJuice(new FruitBox<Apple>())); // 둘 다 가능!

 

 * 지네릭 메서드

- 지네릭 타입이 선언된 메서드 (타입 변수는 메서드 내에서만 유효)

static <T> void sort(List<T> list, comparatoe<? super T> c)

클래스 타입 변수와 메서드 타입 변수의 이름이 같아도, 메서드에선 메서드 타입 변수를 사용함! 즉 둘이 다를 수 있음.

 

- 메서드를 호출할 때마다 타입을 대입해야. (대부분 생략 가능)

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(Juicer.<Fruit>makeJuice(fruitBox));

 

 * 지네릭 타입의 형변환

- 지네릭 타입과 원시 타입 간의 형변환은 바람직 하지 않다.

Box<Object> objBox = null;
Box box = (Box)objBox;	   // 에러는 없으나, 경고 발생 (지네릭 타입 --> 원시 타입)
objBox = (Box<Object>)box; // 에러는 없으나, 경고 발생 (원시 타입 --> 지네릭 타입)

- 서로 다른 타입의 지네릭 타입은 형변환 X

obj Box = (Box<Object>)strBox; // 에러

- 와일드카드가 사용된 지네릭 타입은 형변환 가능!

Box<Object>		objBox = (Box<Object>)new Box<String>(); 		// 불가능
Box<? extends Object>	objBox = (Box<? extends Object>)new Box<String>(); 	// 가능
Box<? extemds Object>	objBox = new Box<String>(); // 위 문장과 동일

 

 * 지네릭 타입의 제거

- 컴파일러는 지네릭  타입을 제거하고, 필요한 곳에 형변환 넣는다. + 생략한 형변환도 추가 + 와일드카드 변경.

코드 작성할 때 Object --> <T>로 바꾸면, 컴파일러가 다시 <T> --> Object로 변경한다. (하위 호환성 때문!)