학습 목표 : 자바의 제네릭에 대해 학습하세요.
다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능
컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
타입 안정성을 높인다는 것은 의도하지 않은 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의
타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄어준 다는 뜻이다.
class Box {
Object item;
void setItem(Object item) { this.item = item; }
Object getItem() { return item; }
}
Box라는 클래스가 있는데 이 클래스를 제네릭 클래스로 변경해보자.
class Box<T> { // 제네릭 타입 T 선언
T item;
void setItem(T item) { this.item = item; }
T getItem() { return item; }
}
클래스 옆에 <T>
를 붙이면 된다.
그리고 'Object' 를 모두 'T' 로 변경한다.
T
를 '타입 변수(type variable)' 라고 하며, 'Type'의 첫 글자에서 따온 것이다. ArrayList<E>
인 경우, 타입 변수 E는 'Element(요소)'의 첫 글자를 사용한 예이다. Map<K, V>
과 같이 콤마를 구분자로 사용한다. 기호의 종류만 다를 뿐 모두 '임의의 참조형 타입' 을 의미한다.
Box<String> b = new Box<String>(); // 타입 T 대신 실제 타입 지정
b.setItem(new Object()); // Error! String 이외의 타입은 지정불가
b.setItem("Hello~~"); // OK. String 타입이므로 가능
String item = b.getItem(); // 형변환 필요없음
제네릭 타입 T대신 String을 지정해서 사용하는 예제이다.
T 대신 Box<String>
String 타입을 지정했기 떄문에, 제네릭 클래스 Box<T>
는
아래와 같이 정의된 것이다.
class Box {
String item;
void setItem(String item) { this.item = item; }
String getItem() { return item; }
}
다시한번 제네릭의 용어를 먼저 정리하고 넘어가자.
class Box<T> {}
Box<T>
T
Box
Box<String> b = new Box<String>();
Box<String>
String
class Box<T> {
static T item; // Error!
static int compare(T t1, T t2) { ... } // Error!
}
static 멤버
에 타입 변수 T를 사용할 수 없다. class Box<T> {
T[] itemArr; // OK. T타입의 배열을 위한 참조변수
...
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; // Error! 제네릭 배열 생성불가
return tepArr;
}
}
제네릭 배열 타입의 참조변수를 선언하는 것은 가능하다.
new T[10]
과 같이 배열을 생성하는 것은 에러이다.
new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 한다. 위 코드에서 Box 클래스
컴파일 시점에 new T[10]
의 T가 어떤 타입일지 알 수 없기 때문에 에러발생.
instanceof 연산자도 new와 동일한 이유로 T를 피연산자로 사용할 수 없다.
꼭 제네릭 배열 생성이 필요하다면, 리플렉션의 newInstatnce() 와 같이 동적으로 객체 생성하는 메소드,
Object 배열을 생성해서 복사한 다음에 'T[]' 로 형변환하는 방법 등을 사용.
Box<Apple> appleBox = new Box<Apple>(); // OK.
Box<Apple> appleBox = new Box<Kiwi>(); // ERROR!
Box<Fruit> appleBox = new Box<Apple>(); // ERROR! 대입된 타입이 다름
Box<Apple> appleBox = new FruitBox<Apple>(); // OK. 다형성
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>(); // OK. 위와 동일. Java 7부터 생략가능
Box<Fruit> fruitBox = new Box<>();
fruitBox.add(new Fruit()); // OK.
fruitBox.add(new Apple()); // OK.
'void add(T item)'
메소드의 매개변수로 자손인 Apple도 가능하다. class Fruit {
@Override
public String toString() {
return "Fruit";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "Apple";
}
}
class Grape extends Fruit {
@Override
public String toString() {
return "Grape";
}
}
class Toy {
@Override
public String toString() {
return "Toy";
}
}
class Box<T> {
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get (int i) { return list.get(i); }
int size() { return list.size(); }
@Override
public String toString() {
return list.toString();
}
}
public class FruitBoxEx1 {
public static void main(String[] args) {
Box<Fruit> fruitBox = new Box<>();
Box<Apple> appleBox = new Box<>();
Box<Toy> toyBox = new Box<>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
appleBox.add(new Apple());
appleBox.add(new Apple());
toyBox.add(new Toy());
System.out.println(fruitBox);
System.out.println(appleBox);
System.out.println(toyBox);
}
}
위에서 배운 개념을 이용하여 제네릭 클래스 생성 및 사용 예제를 작성한 것이다.
class FruitBox<T extends Fruit> { // Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
...
}
'extends'
를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다. FruitBox<Apple> appleBox = new FruitBox<>(); // OK.
FruitBox<Toy> toyBox = new FruitBox<>(); // ERROR! Toy는 Fruit의 자손이 아님
interface Eatable {}
class FruitBox<T extends Eatable> { ... }
클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면,
이때도 'implements'가 아닌 'extends'
를 사용한다.
class FruitBox<T extends Fruit & Eatable> { ... }
public class FruitBox <T extends Fruit & Eatable> extends Box<T> {}
public class Fruit implements Eatable {
@Override
public String toString() {
return "Fruit";
}
}
public interface Eatable {}
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<Toy> toyBox = new FruitBox<Toy>(); // ERROR.
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
appleBox.add(new Apple());
// appleBox.add(new Grape()); // ERROR.
grapeBox.add(new Grape());
System.out.println("fruit-box - " + fruitBox);
System.out.println("apple-box - " + appleBox);
System.out.println("grape-box - " + grapeBox);
}
}
처음 만든 예제의 Fruit 클래스를 조금 수정하고 FruitBox 클래스와 Eatable 인터페이스를 생성 후
위에서 배운 제한된 제네릭 클래스에 대해 테스트하는 예제이다.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
makeJuice(FruitBox<Fruit>)
처럼 특정 타입을 지정해야 한다. 그런데, 'FruitBox<Fruit>'
처럼 고정하면, 'FruitBox<Apple>'
타입의 객체는
makeJuice()의 매개변수가 될 수 없다.
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
그럼 위와같이 여러 타입의 매개변수를 갖는 makeJuice()를 여러개 오버로딩 하면될까?
-> 컴파일 에러가 발생한다.
💡 제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않음
그래서 위의 결과는 메소드 오버로딩이 아니라, '메소드 중복 정의'가 된다.
이 문제를 해결하기 위해 와일드 카드 '?' 를 사용할 수 있다.
<? extends T> 와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한, T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능. <? extends Object>와 동일
💡 제네릭 클래스와 달리 와일드 카드에는 '&'를 사용하지 못함. <? extends T & E> 이런 표현이 안됨.
와일드 카드를 사용해서 위의 문제가 있는 부분을 해결해보자.
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
FruitBox<Fruit>, FruitBox<Apple>, FruitBox<Grape>
를 사용할 수 있다. extends
를 사용해서 Fruit와 그 자손들이 올 수 있다. public class Juice {
String name;
Juice(String name) {
this.name = name + "Juice";
}
@Override
public String toString() {
return name;
}
}
public class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for (Fruit f : box.getList()) {
tmp += f + " ";
}
return new Juice(tmp);
}
}
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());
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
}
}
// 결과
Apple Grape Juice
Apple Apple Juice
위의 설명에 나온 예제들을 테스트한 결과이다.
Fruit<Apple>
도 makeJuice의 매개변수로 들어가서 잘 동작하는 것을 볼 수 있다.
다음은 'super'로 와일드카드를 제한
한 경우의 예제이다.
public class Fruit {
String name;
int weight;
public Fruit () {}
public Fruit(String name, int weight) {
this.name = name;
this.weight = weight;
}
@Override
public String toString() {
return name + "(" + weight + ")";
}
}
public class Apple extends Fruit {
public Apple() {
}
public Apple(String name, int weight) {
super(name, weight);
}
}
public class Grape extends Fruit {
public Grape() {
}
public Grape(String name, int weight) {
super(name, weight);
}
}
public class AppleComp implements Comparator<Apple> {
@Override
public int compare(Apple a1, Apple a2) {
return a2.weight - a1.weight;
}
}
public class GrapeComp implements Comparator<Grape> {
@Override
public int compare(Grape g1, Grape g2) {
return g2.weight - g1.weight;
}
}
public class FruitComp implements Comparator<Fruit> {
@Override
public int compare(Fruit f1, Fruit f2) {
return f1.weight - f2.weight;
}
}
public class FruitBoxEx4 {
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Grape> grapeBox = new FruitBox<>();
appleBox.add(new Apple("GreenApple", 300));
appleBox.add(new Apple("GreenApple", 100));
appleBox.add(new Apple("RedApple", 200));
grapeBox.add(new Grape("GreenGrape", 400));
grapeBox.add(new Grape("GreenGrape", 300));
grapeBox.add(new Grape("RedGrape", 200));
Collections.sort(appleBox.getList(), new AppleComp());
Collections.sort(grapeBox.getList(), new GrapeComp());
System.out.println(appleBox);
System.out.println(grapeBox);
System.out.println();
Collections.sort(appleBox.getList(), new FruitComp());
Collections.sort(grapeBox.getList(), new FruitComp());
System.out.println(appleBox);
System.out.println(grapeBox);
}
}
// 결과
[GreenApple(300), RedApple(200), GreenApple(100)]
[GreenGrape(400), GreenGrape(300), RedGrape(200)]
[GreenApple(100), RedApple(200), GreenApple(300)]
[RedGrape(200), GreenGrape(300), GreenGrape(400)]
Collections.sort()를 이용해서 appleBox와 grapeBox에 담긴 과일을 무게별로 정렬하는 예제이다.
sort() 메소드를 열어보면 아래와 같이 생겼다.
public static <T> void sort(List<T> list, Comparator<? super T> c) {
list.sort(c);
}
첫 번째 매개변수는 정렬할 대상
두 번째 매개변수는 정렬할 방법이 정의된 Comparator 인데, 제네릭 타입에 하한 제한(super) 이
걸려있는 와일드 카드가 사용되었다.
만약, sort()에서 와일드 카드를 사용하지 않았다고 가정해보자.
public static <T> void sort(List<T> list, Comparator<T> c) {
list.sort(c);
}
그리고, 타입 매개변수 T에 Apple이 대입되면, 위의 정의는 아래와 같이 바뀐다.
public static void sort(List<Apple> list, Comparator<Apple> c) {
list.sort(ㅊ);
}
따라서, List<Apple>
을 정렬하고 싶다면 Comparator<Apple>
이 필요하다.
여기까지는 별 문제가 없는데, List<Grape>
를 정렬하고 싶어진다면?
-> Comparator<Apple>
로는 List<Grape>
를 정렬할 수 없어서 Comparator<Grape>가 필요하다.
그런데 위의 작성된 코드를 보면 Comparator<Apple>
과 Comparator<Grape>
의 코드는 동일하다.
타입만 다를 뿐 완전히 똑같다! 코드의 중복이 발생하고 중복을 넘어서 새로운 List<Kiwi>
같은
Fruit의 자손이 생길 때 마다 Comparator<Kiwi>
같은 코드를 새로 만들어야하는 문제가 발생한다.
이 문제를 해결하기 위해 sort()가 하한 제한(super)의 와일드 카드를 사용 하고 있는 것이다.
public static <T> void sort(List<Apple> list, Comparator<? super Apple> c) {
list.sort(c);
}
원래 정의되어있는 sort()에 타입으로 Apple이 대입되면 위와 같다.
Comparator<? super Apple>
이니까 Apple과 그 조상들이 가능한 것이다. Comparator<Apple>
Comparator<Fruit>
Comparator<Object>
Comparator<? super Grape>
도 마찬가지이다. Comparator<Grape>
Comparator<Fruit>
Comparator<Object>
이제 문제를 해결해보자.
예제 코드를 보면 FruitComp를 만들어서 List<Apple>
과 List<Grape>
를
동시에 정렬한 코드를 확인 할 수 있다.
Collections.sort(appleBox.getList(), new FruitComp());
Collections.sort(grapeBox.getList(), new FruitComp());
제네릭 메소드?
메소드의 선언부에 제네릭 타입이 선언된 메소드
class FruitBox<T> {
static <T> void sort(List<T> list, Comparartor<? super T> c) {
...
}
}
반환 타입 바로 앞
이다. FrutBox<T>
와 static <T> void sort()
의 T는 문자만 같을 뿐 다르다. // 이전 코드
static Juice makeJuice(FruitBox<? extends Fruit> box) {
...
}
// 제네릭 메소드 변경
static <T extends Fruit> Juice makeJuice(FruitBox<? extends T> box) {
...
}
앞에 등장했던 makeJuice()를 제네릭 메소드로 변경한 코드이다.
위 메소드를 호출할 때는 아래와 같이 타입 변수에 타입을 대입해야한다.
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
그러나, 대부분 컴파일러가 타입 추정을 할 수 있어서 아래와 같이 생략해도 된다.
System.out.println(Juicer.makeJuice(fruitBox));
System.out.println(Juicer.makeJuice(appleBox));
주의할 점은 제네릭 메소드를 호출 할 때, 대입된 타입을 생략할 수 없는 경우에는
아래와 같이 참조변수나 클래스 이름을 생략할 수 없다.
System.out.println(<Fruit>.makeJuice(fruitBox)); // ERROR. 클래스 이름 생략불가
System.out.println(Juicer.<Fruit>makeJuice(fruitBox)); // OK.
System.out.println(this.<Fruit>makeJuice(fruitBox)); // OK.
같은 클래스 내에 있는 멤버들끼리는 참조변수나 클래스 이름, 즉 'this', '클래스이름'
을
생략하고 메소드 명으로만 호출이 가능하나, 대입된 타입이 있을 때는 반드시 써줘야 한다.
제네릭 메소드는 매개변수의 타입이 복잡할 때도 유용하게 쓰일 수 있다.
// 이전 코드
public static void printAll(ArrayList<? extends Product> list,
ArrayList<? extends Produce> list2) {
...
}
// 제네릭 메소드로 변경
public static <T extends Product> void printAll(ArrayList<T> list,
ArrayList<T> list2) {
...
}
public static <T extends Comparable<? super T>> void sort(List<T> list)
<T extends Comparable>
) Comparable<? super T>
) public class Kiwi extends Fruit implements Comparable<Kiwi> {
public Kiwi() {
}
public Kiwi(String name, int weight) {
super(name, weight);
}
@Override
public int compareTo(Kiwi o) {
return o.weight - this.weight;
}
}
public class FruitBoxEx4 {
public static void main(String[] args) {
FruitBox<Kiwi> kiwiBox = new FruitBox<>();
kiwiBox.add(new Kiwi("Kiwi", 400));
kiwiBox.add(new Kiwi("Kiwi", 500));
kiwiBox.add(new Kiwi("GoleKiwi", 200));
Collections.sort(kiwiBox.getList());
System.out.println(kiwiBox);
}
}
// 결과
[Kiwi(500), Kiwi(400), GoleKiwi(200)]
위의 개념을 적용한 예제이다.
Kiwi 클래스가 T에 대입될 클래스이다.
Kiwi 클래스는 Comparable을 구현하고, Comparable은 Kiwi 또는 그 조상이 가능하므로
Kiwi로 지정하였다.
제네릭 타입과 원시 타입(raw type)간 형변환이 가능할까?
Box box = null;
Box<Object> objectBox = null;
box = (Box)objectBox; // OK. 제네릭 타입 -> 원시타입. 경고발생
objectBox = (Box<Object>)box; // OK. 원시 타입 -> 제네릭 타입. 경고발생
제네릭 타입과 넌제네릭(non-generic) 타입은 경고가 발생할 뿐 형변환이 가능하다.
Box<Object> objectBox = null;
Box<String > stringBox = null;
objectBox = (Box<Object>)stringBox; // ERROR. Box<String> -> Box<Object>
stringBox = (Box<String>)objectBox; // ERROR. Box<Object> -> Box<String>
대입된 타입이 다른 제네릭 타입 간에는 형변환이 불가능하다.
불가능 하다는 사실은 이미 14-1 파트에서 자연스럽게 알 수 있었던 사실이다.
Box<Object> objectBox = new Box<String>(); // ERROR.
위 코드가 에러를 발생한 다는 것을 배웠기 때문이다.
그렇다면 다음의 코드는 어떨까?
Box<? extends Object> wBox = new Box<String>(); // OK.
형변환이 된다.
static Juice makeJuice(FruitBox<? extends Fruit> box) { ... }
FruitBox<? extends Fruit> box = new FruitBox<Fruit>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Apple>(); // OK
FruitBox<? extends Fruit> box = new FruitBox<Grape>(); // OK
그래서 아까 등장했던 makeJuice 메소드의 매개변수에 다형성이 적용될 수 있었던 것이다.
FruitBox<? extends Fruit> box = null;
FruitBox<Apple> appleBox = (FruitBox<Apple>)box;
위 코드처럼 반대로 형변환이 가능하긴 하지만, 확인되지 않은 형변환이라는 경고가 발생한다.
FruitBox<? extends Fruit>
에 대입될 수 있는 타입이 여러개이고, FruitBox<Apple>
을 제외한 다른 타입은 FruitBox<Apple>
로 형변환 될 수 없기 때문이다. public final class Optional<T> {
/**
* Common instance for {@code empty()}.
*/
private static final Optional<?> EMPTY = new Optional<>(null);
...
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
...
}
조금 더 실질적인 예를 살펴보기 위해 java.util.Optional 클래스의 소스 일부분을 확인해보자.
위의 코드에서 EMPTY의 타입을 Optional<Object>
가 아닌 Optional<?>
로 한 이유는
Optional<T>로 형변환
이 가능하기 때문이다.
Optional<?> wopt = new Optional<Object>();
Optional<Object> oopt = new Optional<Object>();
Optional<String> sopt1 = (Optional<String>)wopt; // OK. 형변환 가능
Optional<String> sopt2 = (Optional<String>)oopt; // ERROR! 형변환 불가
empty() 메소드의 반환 타입이 Optional<T>
이므로 EMPTY를 형변환 할 수 있도록
Optional<?>
와일드 카드가 포함된 제네릭 타입을 사용한 것이다.
컴파일러는 제네릭 타입을 이용하여 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다.
그리고, 제네릭 타입을 제거한다.
이렇게 하는 이유는 제네릭이 도입되기 이전의 소스코드와의 호환성을 유지하기 위해서이다.
1️⃣. 제네릭 타입의 경계(bound)를 제거
<T extends Fruit>
라면 T -> Fruit로 치환된다. <T>
인 경우 T -> Object로 치환된다. // 변경 전
class Box<T extends Fruit> {
void add(T t) {
...
}
}
// 변경 후
class Box {
void add(Fruit t) {
...
}
}
2️⃣. 제네릭 타입을 제거 후 타입이 일치하지 않으면, 형변환을 추가
List의 get()은 Object 타입을 반환하므로 형변환이 필요하다.
// 변경 전
T get(int i) {
return list.get();
}
// 변경 후
Fruit get(int i) {
return (Fruit)list.get(i);
}
와일드 카드가 포함되어 있는 경우에는 적절한 타입으로 형변환이 추가된다.
// 변경 전
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
// 변경 후
static Juice makeJuice(FruitBox box) {
String tmp = "";
Iterator it = box.getList().iterator();
while(it.hasNext()) {
tmp += (Fruit)it.next() + " ";
}
return new Juice(tmp);
}
제네릭 타입 제거 테스트를 위해 Box 제네릭 클래스를 이용할 것이다.
public class Box<T extends Fruit> {
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get (int i) { return list.get(i); }
...
}
위에서 작성한 Box 클래스인데 bytecode를 열어서 타입 매개변수 T가 어떻게 되었는지 확인해보자.
// class version 58.0 (58)
// access flags 0x21
// signature <T:Lcom/jihan/javastudycode/week14/Fruit;>Ljava/lang/Object;
// declaration: com/jihan/javastudycode/week14/Box<T extends com.jihan.javastudycode.week14.Fruit>
public class com/jihan/javastudycode/week14/Box {
// compiled from: Box.java
// access flags 0x0
// signature Ljava/util/ArrayList<TT;>;
// declaration: list extends java.util.ArrayList<T>
Ljava/util/ArrayList; list
// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 6 L1
ALOAD 0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
PUTFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
RETURN
L2
LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L2 0
// signature Lcom/jihan/javastudycode/week14/Box<TT;>;
// declaration: this extends com.jihan.javastudycode.week14.Box<T>
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x0
// signature (TT;)V
// declaration: void add(T)
add(Lcom/jihan/javastudycode/week14/Fruit;)V
// parameter item
L0
LINENUMBER 7 L0
ALOAD 0
GETFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
ALOAD 1
INVOKEVIRTUAL java/util/ArrayList.add (Ljava/lang/Object;)Z
POP
RETURN
L1
LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L1 0
// signature Lcom/jihan/javastudycode/week14/Box<TT;>;
// declaration: this extends com.jihan.javastudycode.week14.Box<T>
LOCALVARIABLE item Lcom/jihan/javastudycode/week14/Fruit; L0 L1 1
// signature TT;
// declaration: item extends T
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x0
// signature (I)TT;
// declaration: T get(int)
get(I)Lcom/jihan/javastudycode/week14/Fruit;
// parameter i
L0
LINENUMBER 8 L0
ALOAD 0
GETFIELD com/jihan/javastudycode/week14/Box.list : Ljava/util/ArrayList;
ILOAD 1
INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;
CHECKCAST com/jihan/javastudycode/week14/Fruit
ARETURN
L1
LOCALVARIABLE this Lcom/jihan/javastudycode/week14/Box; L0 L1 0
// signature Lcom/jihan/javastudycode/week14/Box<TT;>;
// declaration: this extends com.jihan.javastudycode.week14.Box<T>
LOCALVARIABLE i I L0 L1 1
MAXSTACK = 2
MAXLOCALS = 2
...
public class com/jihan/javastudycode/week14/Box
<T extends Fruit>
가 지워졌다. add(Lcom/jihan/javastudycode/week14/Fruit;)V
void add(T item)
의 T 값이 Fruit 로 치환됐다.get(I)Lcom/jihan/javastudycode/week14/Fruit;
T get (int i)
의 T 값이 Fruit 로 치환됐다. INVOKEVIRTUAL java/util/ArrayList.get (I)Ljava/lang/Object;
CHECKCAST com/jihan/javastudycode/week14/Fruit