ArrayList<Hamburger> tvList = new ArrayList<Tv>();
tvList.add(new Hamburger()); // OK
tvList.add(new Drink()); // 컴파일 에러, Tv 외에는 다른 타입은 저장이 불가능하다.
기존에 ArrayList는 Object 배열을 가지고 있기 때문에 모든 종류의 객체가 저장이 가능하다. 만약 특정 객체(Hamburger)만 저장하고 싶다는 가정하에 제네릭이 존재하기 전에 특정 객체 외에 다른 객체가 저장이 되도 잡을 수 있는 방법이 없었다. 하지만 제네릭이 도입되면서 List에 타입을 한번 설정하고 객체를 생성하면 생성 시 설정한 객체 외에는 컴파일러가 막아주는 역할을 수행한다.
public class GenericsTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("Test");
System.out.println(list);
}
}
결과 💡
[10, 20, Test]
기본적으로 Object 배열이기 때문에 어떤 타입이 올 수 있다. 또한 중간에 형변환하는 과정에 있어서 런타임 오류가 발생할 가능성이 높다.
public class GenericsTest {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("30");
System.out.println((Integer)list.get(2)); //마지막으로 저장된 문자열 30을 int형으로 형변환 할려고 함
}
}
결과 💡
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at com.lee.company.GenericsTest.main(GenericsTest.java:12)
public class GenericsTest {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList();
list.add(10);
list.add(20);
list.add("Test");
// 이미 제네렉을 통해 String 타입으로 선언했기 때문에 형변환 필요가 없다.
System.out.println((Integer)list.get(2));
System.out.println(list);
}
}
컴파일조차 되지도 않을 뿐더라 IDE에서 오류로 표시해준다. 이를 통해 제네릭을 이용하면 타입의 대한 안정성 하나는 보장받을 수 있고, 귀찮은 형변환의 번거로움을 줄일 수 있다는 사실을 얻었다 💡
잠깐 공부했지만, 개인적인 생각으로썬 개발자라면 누구나 오류를 보게된다. 하지만 그 오류도 컴파일 에러이냐, 런타임 에러이냐에 따라 처리하는 속도가 달라진다. 아마 잠재적으로 치명적인 오류를 발생시킬 수 있는 런타임 에러보단, 애초당시에 컴파일 에러를 발생시키는 것이 훨씬 좋은 방법이라고 생각하기 때문에 이러한 기능이 추가되지 않았다 싶다.
// 추가 설명이 필요한 듯
컴파일 타임에 타입 체크를 하기 때문에 런타임에서 ClassCastException 같은 예외로부터 안정성을 보장받을 수 있다.
ArrayList<String> list = new ArrayList(); // <String>이 타입변수에 지정됨
ArrayList<E> list = new ArrayList<E>();
ArrayList<String> list = new ArrayList<String>();
ArrayList<Hamburger> list = new ArrayList<Hambuger>(); // 사용자 지정 클래스가 올 수 있음
타입인자 | 설명 |
---|---|
Type | |
Element | |
Key | |
Number | |
Value | |
Result |
클래스를 설계할 때 구체적인타입을 명시하지 않고 타입 파라미터로 넣어두었다가 실제 해당 클래스를 인스턴스화 시킬 때 구체적인 타입을 지정하면 제네릭 타입의 클래스를 생성할 수 있다.
public class SampleGenericsDemo<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
public class GenericsTest {
public static void main(String[] args) {
SampleGenericsDemo<String> demo = new SampleGenericsDemo<>();
demo.setT("Test");
System.out.println(demo.getT());
}
}
결과 💡
Test
제네릭으로 클래스를 생성할 수 있을 뿐만 아니라 인터페이스도 설계가 가능하다
public interface GenericInterfaceDemo<T> {
T testMethod();
}
public class GenericsTest implements GenericInterfaceDemo<String> {
public static void main(String[] args) {
System.out.println(new GenericsTest().testMethod());
}
@Override
public String testMethod() {
return "제네릭 인터페이스로 구현한 메소드입니다.";
}
}
결과 💡
제네릭 인터페이스로 구현한 메소드입니다.
public class MultiTypeGeneric<K, V> {
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
public class GenericsTest {
public static void main(String[] args) {
MultiTypeGeneric<String, Integer> demo = new MultiTypeGeneric<>();
demo.setKey("TestKey");
demo.setValue(1234);
System.out.println("Key : " + demo.getKey() + ", Value : " + demo.getValue());
}
}
결과 💡
Key : TestKey, Value : 1234
class FruitBox<T extends Fruit> { // Fruit 클래스 포함한 자식 타입으로만 지정가능
ArrayList<T> list = new ArrayList<>();
}
만약 Apple 이라는 클래스가 Fruit의 자식클래스라고 가정하에 제네릭 클래스를 사용한다면
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // Apple은 Friut의 자식이기 때문에 성공!
FruitBox<Toy> toyBox = new FruitBox<Toy>(); // Toy는 Fruit의 자식이 아니기 때문에 에러 발생!
interface Eatable{}
class FruitBox<T extends Eatable> {}
Box<Apple> appleBox = new Box<Apple>();
class Box<T> {
static T item; // 에러
static int compare(T t1, T t2) {...} // 에러
}
class Box<T> {
T[] itemArr;
T[] toArray() {
T[] tmpArr = new T[10]; // new 연산자 뒤에는 타입 변수가 올 수 없다.
}
}
: 와일드 카드의 상한 제한. T와 그 자손들만 가능 제한 없음. 모든 타입이 가능. 와 동일
public BeefHamburger makeHamBurger(PattyBox<? extends Beef> patty) {
...
// Beef 클래스를 상속받는 TenToOneBeef, FourToOneBeef 둘 다 들어올 수 있다.
}
System.out.println(Hamburger.makeHamburger(new PattyBox<TenToOneBeef>)); // 10 : 1 패티
System.out.println(Hamburger.makeHamburger(new PattyBox<FourToOneBeef>)); // 4 : 1 패티
static <T> void sort(List<T> list, Comparator<? super T> c)
class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c) {
...
}
}
클래스의 타입 매개변수가 범위적으로 봐도 훨씬 크지만 우선순위는 제네릭 메소드의 타입 매개변수가 더 우선시 된다.
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {
String temp = "";
for (Fruit fruit : box.getList()) {
temp += fruit + " ";
}
return new Juice(temp);
}
제네릭 메소드는 메소드를 호출할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것
와일드 카드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것
여기서부턴 자바 라이브 스티디원 중 한명인 ssonsh 님의 글을 많이 참고하여 작성하였습니다.
Box<Object> objBox = null;
Box box = (Box)objBox; // 제네릭 타입 -> 원시 타입. 경고 발생
objBox = (Box<Object)box; // 원시 타입 -> 제네릭 타입. 경고 발생
위 코드는 컴파일 상 오류는 발생하지 않지만, 다만 경고가 발생한다. 이를 통해 제네릭 타입과 제네릭 타입이 아닌 타입간의 형변환은 자유롭지만, 경고가 발생한다라는 사실을 알 수 있다.
대입된 타입이 다른 제네릭 타입 간의 형변환은 가능할까?
Box<Object> objBox = null;
Box<String> strBox = null;
objBox = (Box<Object>)strBox;
strBox = (Box<String>)objBox;
대입된 타입이 Object 타입일지라도 에러가 발생한다.
와일드 카드가 사용된 제네릭 형태로 형변환을 할 수 있을까?
Box<? extends Object>에 대입될 수 있는 타입은 여러개
Box<String> 을 제외한 다른 타입은 Box<String>으로 형변환 될 수 없기 때문이다.
형변환은 가능하긴 하지만, 와일드 카드는 타입이 확정된 타입이 아니므로 컴파일러는 미확정 타입으로 형변환하는 것이라고 경고를 보내준다.
컴파일러는 제네릭 타입을 이용해 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 제네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 제네릭 타입에 대한 정보가 없는 것이다.
public class App {
public static void main(String[] args) {
Box<String> strBox = new Box<>();
strBox.setItem("string box!");
}
}
이렇게 하위호환성을 유지해야 함으로 원시타입 지원 + 제네릭을 구현할 때 소거(erasure) 방식을 이용했다.
class Box<T extends Fruit>{
void add(T t){
...
}
}
class Box{
void add(Fruit t){
...
}
}
T get(int i){
return list.get(i);
}
Fruit get(int i){
return (Fruit)list.get(i);
}
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f : box.getList()) temp += f + " ";
return new Juice(temp);
}
static Juice makeJuice(FruitBox box){
String tmp = "";
Iterator it = box.getList().iterator();
while(it.hasNext()){
tmp += (Fruit)it.next() + " " ;
}
return new Juice(temp);
}
확장된 제네릭 타입에서 다형성을 보존하기 위해 Bridge Method를 생성하기도 한다.