클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법
이는 객체별로 다른 타입의 자료가 저장될 수 있도록 함
Ex)
ArrayList<String> list = new ArrayLisy<>()⇒ 꺾쇠 괄호 : 제네릭
즉, 배열의 타입을 지정하듯이 리스트 자료형 같은 컬렉션 클래스나 메서드에서 사용할 내부 데이터 타입을 파라미터 주듯이 외부에서 지정하는 타입을 변수화 하는 기능
⇒ 변수를 선언할 때 타입을 지정 해주듯이, 제네릭은 객체에 타입을 지정 해주는 것
함수나 클래스에서 사용할 타입을 외부에서 유연하게 지정할 수 있도록 해주는 변수
제네릭은 <> 괄호 키워드를 사용하는데 이를 다이아몬드 연산자라고 부름
이 꺾쇠 관호 안에 식별자 기호를 지정함으로써 파라미터화 할 수 있음
⇒ 마치 메서드가 매개변수를 받아 사용하는 것과 비슷하여 제네릭의 타입 매개변수 / 타입 변수라고 부름
List , <
T> :타입 매개변수
List stringList = new ArrayList() , <
String> :매개변수화 된 타입
<T>는 제네릭을 선언하는 부분이고,T는 그 제네릭 타입을 사용하는 타입 매개변수
이러한 타입 매개변수는 제네릭을 이용한 클래스나 메서드를 설계할 때 사용됨
class FruitBox<T> { => 외부에서 타입을 지정받음 !!
List<T> fruits = new ArrayList<>();
public void add(T fruit) {
fruits.add(fruit);
}
}
FruitBox<Integer> intBOx = new FruitBox<>();
FruitBox<Double> doubleBOx = new FruitBox<>();
...
위 코드를 보면 외부에서 지정하는 타입으로 인해 타입이 정해지는 것을 볼 수 있는데
, 이를 구체화( Specialization )라고 함
타입 파라미터 생략
FruitBox<Integer> intBOx = new FruitBox<>()
앞에서 제네릭을 지정해주었기에 new 부분에는 제네릭 타입을 다시 지정할 필요가 없음
타입 파라미터 할당 가능 타입
제네릭에서 할당 받을 수 있는 타입은 Reference 타입뿐이다 !
즉, int, double .. 등의 기본형을 제네릭의 파라미터 타입으로 넘길 수 없다는 것
또한 제네릭 타입 파라미터에 클래스가 타입으로 온다는 것은, 상속을 통해 관계를 맺는 다형성의 원리가 적용이 된다는 것 !
class Fruit { }
class Apple extends Fruit { }
class Banana extends Fruit { }
class FruitBox<T> {
List<T> fruits = new ArrayList<>();
public void add(T fruit) {
fruits.add(fruit);
}
}
public class Main {
public static void main(String[] args) {
FruitBox<Fruit> box = new FruitBox<>();
// 제네릭 타입은 다형성 원리가 그대로 적용 가능
box.add(new Fruit());
box.add(new Apple());
box.add(new Banana());
}
}
복수 타입 파라미터
제네릭 타입 파라미터는 반드시 한 개만 사용하라는 법이 없으며, 만약 타입 지정이 여러 개가 필요한 경우 여러 개를 지정할 수 있음
<> 안에서 ,로 구분
class Apple {}
class Banana {}
class FruitBox<T, U> {
List<T> apples = new ArrayList<>();
List<U> bananas = new ArrayList<>();
public void add(T apple, U banana) {
apples.add(apple);
bananas.add(banana);
}
}
public class Main {
public static void main(String[] args) {
// 복수 제네릭 타입
FruitBox<Apple, Banana> box = new FruitBox<>();
box.add(new Apple(), new Banana());
box.add(new Apple(), new Banana());
}
}
중첩 타입 파라미터
제네릭 객체를 제네릭 타입 파라미터로 받는 형식도 표현할 수 있음
public static void main(String[] args) {
ArrayList<LinkedList<String>> list = new ArrayList<LinkedList<String>>();
LinkedList<String> node1 = new LinkedList<>();
node1.add("aa");
node1.add("bb");
LinkedList<String> node2 = new LinkedList<>();
node2.add("11");
node2.add("22");
list.add(node1);
list.add(node2);
System.out.println(list); // [[aa, bb], [11, 22]]
}
컴파일 시점에 타입 검사를 통해 예외 방지
자바의 Object를 사용한다면 ??
class FruitBox {
private Object[] fruit;
public FruitBox(Object[] fruit) {
this.fruit = fruit;
}
public Object getFruit(int index) {
return fruit[index];
}
}
FruitBox box = new FruitBox(new Apple[]{new Apple(), new Apple()});
Apple a = (Apple) box.getFruit(0); // 형 변환 해줘야 함, OK
~~Banana b = (Banana) box.getFruit(1)~~; // 컴파일 시점에 오류가 발생하지 않고 런타임 때 발생
Object 배열 안에 Apple 객체가 들어간 상태인데 이를 다시 꺼낼 때는
컴파일러 입장에서 “안에 뭐가 들어 있을지 몰라서” 꺼낼 때Object라고 간주하기 때문에 캐스팅을 다시 해줘야 함
제네릭을 사용한다면 ??
Class FruitBox<T> {
private T[] fruit
public FruitBox(T[] fruit) {
this.fruit = fruit;
}
public T getFruit(int idx) {
return fruit[idx];
}
}
FruitBox<Apple> box = new FruitBox<>(new Apple[]{new Apple()});
Apple a = box.getFruit(0); // 형변환도 필요 없음
Banana b = box.getFruit(1); // 컴파일 에러 발생! ( 미리 방지 )
불필요한 캐스팅을 없애 성능 향상
Apple[] arr = { new Apple(), new Apple(), new Apple() };
FruitBox box = new FruitBox(arr);
// 가져온 타입이 Object 타입이기 때문에 일일히 다운캐스팅을 해야함 - 쓸데없는 성능 낭비
Apple apple1 = (Apple) box.getFruit(0);
Apple apple2 = (Apple) box.getFruit(1);
Apple apple3 = (Apple) box.getFruit(2);
---
// 미리 제네릭 타입 파라미터를 통해 형(type)을 지정해놓았기 때문에 별도의 형변환은 필요없음
FruitBox<Apple> box = new FruitBox<>(arr);
Apple apple = box.getFruit(0);
Apple apple = box.getFruit(1);
Apple apple = box.getFruit(2);
제네릭 타입의 객체는 생성이 불가
class Sample<T> {
public void someMethod() {
// Type parameter 'T' cannot be instantiated directly
T t = new T();
}
}
제네릭 타입을 지정해서 객체를 생성하는것은 불가능 함
→ new 연산자 뒤에 제네릭 타입 파라미터가 오면 안됨
static 멤버에 제네릭 타입이 올 수 없음
public class Box<T> {
// static T value; // "Cannot make a static reference to the non-static type T"
static int count; // 가능
T instanceValue; // 인스턴스 변수는 가능
}
static 멤버는 클래스가 로딩될 때 메모리에 올라가는데, 제네릭 타입 파라미터 T는 인스턴스가 생성될 때 결정되기 때문에, static 영역에는 T의 타입이 아직 확정되지 않은 상태
따라서 타입이 정해지지 않은 채로 메모리에 올라가게 되어, 타입 안정성을 보장할 수 없기 때문에 컴파일 에러가 발생
즉, T의 타입이 뭔지 결정되기도 전에 static 영역에 들어가게 되니까
타입 불변성 ( Java ver )
기본적으로 타입 간의 상속 관계가 있어도, 제네릭 타입끼리는 상속 관계가 성립하지 않음
즉, Apple이 Fruit의 자식 클래스라고 해도 List은 List의 하위 타입이 아님
class Fruit {}
class Apple extends Fruit {}
class Banana extends Fruit {}
Apple[] appleArr = new Apple[1];
Fruite[] fruits = appleArr; // 이건 가능
fruits[0] = new Orange(); // 오류 발생, fruits는 Banana[]를 참조하고 있는데 Orange를 넣으니까
List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = appleList; // 컴파일 오류!
만약 위의 오류가 되는 코드가 허용된다면
fruitList.add(new Banana) 가 성립되므로 List<Apple> 안에 banana가 들어가버림
⭐⭐⭐
자바에서 배열은 공변성을 허용하지만, 제네릭 타입( List, Set .. )은 불변성을 지님
자바에서는 이러한 문제 해결을 위해 와일드 카드를 사용 : ? extends , ? super → 읽기만 가능