위처럼 지네릭이 도입되기 이전의 코드와의 호환을 위해 예전방식으로 객체를 생성하는것이 허용된다만, 타입을 지정하지 않아 안전하지 않다는 경고가 표시된다.
왠만하면 반드시 타입을 지정해주자
매개변수와의 유사성
Box<String>과 Box<Integer>는 지네릭 클래스 Box<T>에 서로 다른 타입을 대입해 호출한 것일 뿐, 이 둘이 별개의 클래스를 의미하지 않는다. (같은 클래스라는 말이다.)
컴파일 후에 둘다 모두 이들의 원시타입인 Box로 바뀐다. 지네릭 타입이 제거된다는 의미이다.
지네릭 클래스의 제한
static 멤버에 타입변수 T를 사용할 수 없다.
T는 인스턴스 변수로 간주되는데 static 멤버는 인스턴스 변수를 참조할 수 없다.
static멤버는 타입이 동일한 것이여야 한다. 어떤 객체에서 호출해도 모두 동일하게 동작하며 공유되기 때문이다.
지네릭 타입의 배열을 생성하는것도 허용되지 않는다.
그 이유는 new 연산자 때문인데 이 연산자는 컴파일 시점에 타입T가 뭔지 정확히 알아야한다. Box<T>를 컴파일 하는 시점에 T가 어떤 타입이 될지 알수 없기 때문에 instanceof 도 같은 이유로 사용할수 없다
class Box<T> {
T[] itemArr;
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; //에러 지네릭 배열 생성불가
...
}
}
지네릭 클래스의 객체 생성과 사용
Box.java
import java.util.ArrayList;
public 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(); }
}
FruitBox.java
public class FruitBox<T> extends Box<T>{
}
main.java
public class Main {
public static void main(String[] args) {
Box<Apple> appleBox = new Box<Apple>();
Box<Fruit> fruitBox = new Box<Fruit>();
appleBox.add(new Apple());
//당연하지만 Apple타입으로 생성한 참조변수니 Apple객체만 넣을수있다
//appleBox.add(new Grape());
//상속관계의 타입들 다 가능, void add(Fruit item)
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
fruitBox.add(new Grape());
//참조변수와 생성자에 대입된 타입이 일치해야한다. 일치하지 않으면 에러
//Box<Grape> appleBox2 = new Box<Apple>();
//상속관계에 있더라도 에러
//Box<Fruit> FruitBox = new Box<Apple>();
//지네릭 클래스가 상속관계에 있는건 괜찮다
Box<Apple> appleBox1 = new FruitBox<Apple>();
}
}
class Fruit {
public String toString(){
return "Fruit";
}
}
class Grape extends Fruit{
public String toString(){
return "Grape";
}
}
class Apple extends Fruit{
public String toString(){
return "Apple";
}
}
class Toy {
public String toString(){
return "Toy";
}
}
제한된 지네릭 클래스
매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법
제한하지 않으면 모든 종류의 타입이 지정되기 때문에 fruitBox에 Toy를 담을수도 있다
extends 사용
FruitBoxExtendsFruit.java
/**
* 제한된 지네릭 클래스
* Fruit타입만 타입 매개변수 T에 지정할 수 있다
* 다형성에서 조상타입 참조변수로 자손타입 객체를 가리킬 수 있는것 처럼 Fruit와 그 자손 타입까지 가능하다
* @param <T>
*/
public class FruitBoxExtendsFruit<T extends Fruit> extends Box {
ArrayList<T> list = new ArrayList<T>();
}
main.java
FruitBoxExtendsFruit<Apple> appleFruitBox = new FruitBoxExtendsFruit<Apple>();
//Fruit클래스의 자손들만 담을 수 있다는 제약이 추가되어 Toy는 못담음
//FruitBox2<Toy> toyFruitBox = new FruitBox2<Toy>();
//여전히 상속관계의 타입들 다 가능함
FruitBoxExtendsFruit<Fruit> fruitBoxExtendsFruit = new FruitBoxExtendsFruit<Fruit>();
fruitBoxExtendsFruit.add(new Apple());
fruitBoxExtendsFruit.add(new Grape());
Fruit와 Eatable 인터페이스 구현
인터페이스를 구현해야 한다는 제약이 존재한다면
Eatable.interface
public interface Eatable {
}
/**
* 인터페이스도 타입 매개변수 T에 지정할 수 있다
* extends를 사용하는 것에 주의
* Fruit의 자손이면서 Eatable인터페이스도 구현해야한다면 다음처럼 & 기호로 연결한다
* @param <T>
*/
public class FruitBoxExtendsFruitandEatable<T extends Fruit & Eatable> extends Box {
ArrayList<T> list = new ArrayList<T>();
}
Fruit의 자손이면서 Eatable을 구현한 클래스
public class FruitImplEatable extends Fruit implements Eatable{
public String toString(){
return "Fruit";
}
}
public class Juice {
String name;
Juice(String name) {
this.name = name + "Juice";
}
public String toString(){ return name; }
}
Juicer.java
public class Juicer {
/**
* 와일드 카드를 사용하면 FruitBox<Fruit>뿐만 아니라 Fruit<Apple>등 자손들도 사용가능하다.
*/
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()){
tmp += f + " ";
}
return new Juice(tmp);
}
/**
* 매개변수에 과일박스를 대입하면 주스를 만들어 반환하는 Juicer클래스
* 이 클래스는 지네릭클래스도 아니고 static 은 타입 매개변수 T를 사용할 수 없다.
* 그래서 Apple을 타입으로한 FruitBox를 매개변수로 넣어주면 에러가 발생..
*/
// static Juice makeJuice(FruitBox<Fruit> box) {
//
// String tmp = "";
// for (Fruit f : box.getList()){
// tmp += f + " ";
// }
// return new Juice(tmp);
// }
/**
* Apple타입을 사용하기 위해서 오버로딩 코드를 짜면 에러가 발생한다.
* 지네릭 타입이 다른것 만으로는 오버로딩이 성립하지 않기 때문이다.
* 지네릭 타입은 컴파일 할때만 사용하고 제거하기 때문에 이는 메서드 중복 정의가 되어 에러가 난다.
*/
// static Juice makeJuice(FruitBox<Apple> box) {
//
// String tmp = "";
// for (Fruit f : box.getList()){
// tmp += f + " ";
// }
// return new Juice(tmp);
// }
}
main.java
FruitBox<Fruit> fruit_FruitBox = new FruitBox<Fruit>();
FruitBox<Apple> apple_FruitBox = new FruitBox<Apple>();
//Fruit와 그 자손인 Apple도 가능함
System.out.println("Juicer.makeJuice(fruit_FruitBox) = " + Juicer.makeJuice(fruit_FruitBox));
System.out.println("Juicer.makeJuice(apple_FruitBox) = " + Juicer.makeJuice(apple_FruitBox));
Collections.sort()를 이용한 정렬
Fruit
class Fruit {
String name;
int weight;
Fruit(String name, int weight){
this.name = name;
this.weight = weight;
}
public String toString(){
return name+"{"+weight+"}";
}
}
public class FruitComp implements Comparator<Fruit> {
public int compare(Fruit t1, Fruit t2){
return t1.weight = t2.weight;
}
}
Apple
class Apple extends Fruit {
Apple(String name, int weight) {
super(name, weight);
}
public String toString(){
return name+"{"+weight+"}";
}
}
public class AppleComp implements Comparator<Apple> {
public int compare(Apple t1, Apple t2){
return t2.weight - t1.weight;
}
}
main.java
public class Main {
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<Apple>();
appleBox.add(new Apple("GreenApple", 300));
appleBox.add(new Apple("GreenApple", 100));
appleBox.add(new Apple("GreenApple", 200));
Collections.sort(appleBox.getList(), new AppleComp());
System.out.println("appleBox = " + appleBox);
Collections.sort(appleBox.getList(), new FruitComp());
System.out.println("appleBox = " + appleBox);
}
}
이는 Collections.sort()를 이용해 appleBox에 담긴 과일을 무게별로 정렬하는 것이다. Collections의 선언부는 다음과 같다
static <T> void sort (List<T> list, Comparator<? super T> c)
이는 지네릭 메서드이다. list는 정렬할 대상, c는 정렬할 방법이 정의 된 Comparator이다. 지금 와일드 카드가 사용되어 new FruitComp로도 Apple을 정렬할 수 있다. 만일 와일드 카드를 사용하지 않는다면 Apple은 Comparator<Apple>로 Grape는 Comparator<Grape>로만 정렬할 수 있을것이다. 새로운 과일이 생길때마다 ~Comp.java를 만들어줄수는 없으니 와일드카드로 하한 제한을 해주는것이다.
T에 Apple이 대입되면 다음과 같다
static <Apple> void sort (List<T> list, Comparator<? super Apple> c)
Comparator<? super Apple>은 Comparator의 타입 매개변수로 Apple과 그 조상이 가능하다는거다. 그래서 new FruitComp로 다른 과일들도 정렬가능하다.
몰론 과일의 조상을 Fruit로 상속해주어야 한다.
지네릭 메서드
메서드 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라 한다.
지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의 된 매개변수는 전혀 별개의 것이다.
static 멤버에는 타입 매개변수를 사용할 수 없지만 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.
이 타입 매개변수는 메서드 내에서만 지역적으로 사용되기 때문에 지역변수를 선언한 것과 같다고 생각하면 이해하기 쉽다. 그렇기에 static 이든 아니든 상관이 없다.
makeJuice를 지네릭 메서드로 바꾸면 다음과 같다.
static <T extends Fruit>Juice makeJuice (FruitBox<T> box) {
String tmp = "";
for(Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
이 메서드를 호출할 땐 아래와 같이 타입 변수에 타입을 대입해야 한다.
하지만 대부분의 경우 컴파일러가 타입을 추정할 수 있어 생략해도 된다.
한 가지 주의할 점은 지네릭 메서드를 호출할 때 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다는 것이다. 단지 기술적인 이유이므로 지켜야한다.
FruitBox<Fruit> fruit_FruitBox = new FruitBox<Fruit>();
FruitBox<Apple> apple_FruitBox = new FruitBox<Apple>();
//Fruit와 그 자손인 Apple도 가능함
System.out.println("Juicer.makeJuice(fruit_FruitBox) = " + Juicer.<Fruit>makeJuice(fruit_FruitBox));
System.out.println("Juicer.makeJuice(apple_FruitBox) = " + Juicer.<Apple>makeJuice(apple_FruitBox));
매개변수의 타입이 복잡할 때 유용하게 사용가능하다.
static void printAll (ArrayList<? extends Product> list,
ArrayList<? extends Product> list2) {
for (Unit u : list){
System.out.println(u);
}
}
//위의 코드를 간략하게 변경
static <T extends Product> void printAll (ArrayList<T> list,
ArrayList<T> list2) {
for (Unit u : list){
System.out.println(u);
}
}
지네릭 타입의 형변환
지네릭 타입과 지네릭 타입이 아닌 타입간의 형변환은 항상 가능하다.
하지만 대입된 타입이 다른 지네릭 타입 간에는 형변환이 불가하다.
Box<Object> objBox = null;
Box<String> strBox = null;
objBox = (Box<Object>) strBox; //에러
//이미 아래와 같은 방식으로 생성이 불가능하다는 것을 배웠다 이는 위처럼 형변환이 불가능하다는 것을 간접적으로 알려준다.
Box<Object> objBox = new Box<String>();
//하지만 다음의 문장은 형변환이 가능하다
Box<? extends Object> objBox = new Box<String>();
//반대의 경우는 성립하지만 확인되지 않은 형변환이라는 경고가 발생한다.
FruitBox<? extends Fruit> box = null;
Box<Apple> appleBox = (FruitBox<Apple>) box;
Optional 클래스
public final class Optional<T> {
//EMPTY에 비어있는 Optional 객체를 생성해서 저장
private static final Optional<?> EMPTY = new Optional<>();
//<?> -> <? extends Object>를 줄여쓴것
//<>안에 생략된 타입은 <Object>이다
//따라서 풀어쓰면 Optional<? extends Object> EMPTY = new Optional<Object>();
private final T value;
//EMPTY를 형변환해서 반환
//EMPTY의 타입을 Optional<Object>가 아닌 Optional<?>로 한 이유는 Optional<T>로 형변환이 가능하기 때문이다. 만약 Optional<Object>면 형변환이 불가능하다.
public static<T> Optional<T> empty() {
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
}
정리하면 Optional<Object>를 Optional<String>으로 직접 형변환 하는것은 불가능하지만 와일드 카드가 포함된 지네릭 타입으로 형변환 하면 가능하다는 것이다.
참고로 와일드 카드가 사용된 지네릭 타입끼리도 다음과 같은 경우에 형변환이 가능하다. 다만 미확정 타입으로 형변환 하는 것이라는 경고가 뜬다.