다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입을 체크해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 변수 T를 선언하여 어떤 타입이든 한 가지 타입을 정해서 담을 수 있다.
class Box<T> {}
지네릭스를 사용하면 외부(인스턴스 생성 시나 메소드 호출 시)에서 타입을 지정할 수 있는데 타입 파라미터로 참조 타입만 가능하다.참조 타입인 래퍼 클래스나 클래스 타입은 가능하지만 기본 타입은 지정할 수 없다.
Box<Apple> appleBox = new Box<>();
Box<Grape> grapeBox = new Box<>();
그러나 모든 객체에 대해 동일하게 동작하는 static멤버에 타입 변수 T를 사용할 수 없다. T는 인스턴스변수로 간주되는데 static멤버는 인스턴스변수를 참조할 수 없기 때문이다.
또한 지네릭 타입의 배열을 생성할 수 없다. 컴파일 시 타입 T가 뭔지 정확히 알아야하는데 컴파일 시 T가 어떤 타입인지 알 수 없기 때문이다.
즉, static 멤버와 new 연산자 뒤에 타입 변수 T를 선언할 수 없다.
class Box<T> {
static T item; // 에러
T[] itempArr; // T타입의 배열을 위한 참조변수 선언 가능
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // 에러. 지네릭 배열 생성불가
}
}
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();
}
}
객체 생성 시 참조변수와 생성자에 대입된 타입이 일치해야 한다. 두 타입이 상속관계에 있어도 마찬가지이다.
Box<Fruit> fruitBox = new Box<Fruit>();
Box<Fruit> fruitBox1 = new Box<Apple>(); // 에러
단, 두 지네릭 클래스의 타입이 상속관계에 있고, 대입된 타입이 같은 것은 가능하다.
Box<Apple> appleBox1 = new FruitBox<Apple>();
class FruitBox<T> extends Box {
}
JDK 1.7부터는 추청 가능한 타입을 생략할 수 있는데 참조변수의 타입으로 Box가 Apple타입의 객체만 저장한다는 것을 알 수 있기 때문에 생성자에 반복해서 타입을 지정해주지 않아도 된다.
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>();
import java.util.ArrayList;
public class FruitBoxEx1 {
public static void main(String[] args) {
// 지네릭 클래스 Box의 객체 생성 시 객체별로 다른 타입 지정 가능
Box<Fruit> fruitBox = new Box<Fruit>(); // 객체 생성 시 참조변수와 생성자에 대입된 타입 일치해야함
//Fruit와 Apple이 상속관계에 있다해도 대입된 타입이 다르면 에러
// Box<Fruit> fruitBox1 = new Box<Apple>();
Box<Apple> appleBox = new Box<>();
// 두 지네릭 클래스의 타입이 상속관계에 있고 대입된 타입이 같은 것은 가능 -> 다형성
Box<Apple> appleBox1 = new FruitBox<Apple>();
Box<Grape> grapeBox = new Box<>();
Box<Toy> toyBox = new Box<>();
fruitBox.add(new Fruit());
// Fruit의 자손들은 메서드 매개변수가 될 수 있음
fruitBox.add(new Apple()); // void add(Fruit item)
appleBox.add(new Apple());
// appleBox.add(new Grape()); Box<Apple>에는 Apple만 담을 수 있음
appleBox1.add(new Apple());
toyBox.add(new Toy());
System.out.println(fruitBox);
System.out.println(appleBox);
System.out.println(toyBox);
}
}
class FruitBox<T> extends Box {
}
// Box는 지네릭 클래스로 한 가지 타입을 정해서 객체 생성 가능
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();
}
}
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";
}
}
지네릭 타입에 extends를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
Fruit의 자손만 타입으로 지정가능하다.
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>();
}
package ch12.FruitBoxEx2;
import java.util.ArrayList;
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);
}
}
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 {
}
// 클래스 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();
}
}
static메서드의 매개변수 타입을 지네릭 타입으로 고정해 놓으면 여러 가지 타입의 매개변수를 갖는 메소드를 만들 수 없다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) { // Fruit타입의 객체만 매개변수로 받을 수 있음
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
이를 해결하기 위해 와일드 카드를 사용하는데 와일드 카드는 어떠한 타입도 될 수 있다.
< ? extends T >
: 와일드 카드의 상한 제한. T와 그 자손들만 가능< ? super T >
: 와일드 카드의 하한 제한. T와 그 부모들만 가능< ? >
: 제한 없음. 모든 타입이 가능. < ? extends Object >
와 동일class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) { // Fruit와 그 자손들만 가능
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
와일드 카드를 사용하면 다음과 같이 다형성이 적용되어 참조 변수 타입과 생성자의 타입이 일치하지 않아도 된다.
// Fruit와 그 자손들 가능
// 와일드 카드 적용하면 참조 변수 타입과 생성자의 타입에서 다형성 적용 가능
FruitBox<? extends Fruit> appleBox = new FruitBox<Apple>();
appleBox = new FruitBox<Grape>();
import java.util.ArrayList;
public class FruitBoxEx3 {
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
fruitBox.add(new Apple());
fruitBox.add(new Grape());
appleBox.add(new Apple());
appleBox.add(new Apple());
// 매개변수로 FruitBox<Fruit>뿐만 아니라 FruitBox<Apple>, FruitBox<Grape>도 가능
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
}
}
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 Juice {
String name;
public Juice(String name) {
this.name = name + "Juice";
}
public String toString() {
return name;
}
}
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) { // Fruit와 그 자손들만 가능
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
class FruitBox<T extends Fruit> 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);
}
ArrayList<T> getList() {
return list;
}
int size() {
return list.size();
}
public String toString() {
return list.toString();
}
}
메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 하며 지네릭 타입의 선언 위치는 반환 타입 바로 앞이다. 이 때 지네릭 클래스에 정의된 타입 파라미터와 지네릭 메서드에 정의된 타입 파라미터는 전혀 다른 것이다.
class FruitBox<T> {
static<T> void sort(List<T> list, Comparator<? super T> c) {}
}
클래스에서 지정한 지네릭 유형과 별도로 메소드에서 독립적으로 지네릭 유형을 선언하는 이유는 뭘까?
정적 메소드 선언 시 필요하기 때문이다.
지네릭은 인스턴스화 되었을 때 타입이 지정되는데 정적 메서드는 프로그램 실행 시 이미 메모리에 올라가 있으며 객체 생성 없이 클래스 이름으로 바로 접근 가능하다. 따라서, 클래스의 지네릭 타입과 정적 메소드 사용 시 별개의 지네릭 타입을 지정해줘야한다.