一般的類別和方法,只能使用具體類型:要麼是基本類型,要麼是自訂類型。如果要編寫可以應用於多種類型的程式碼,這種刻板的限制對程式碼的束縛就會很大。
有許多原因促成了泛型的出現,而最引人注目的一個原因,就是為了創造容器類別。 (泛型的主要目的之一就是用來指定容器要持有什麼類型的物件,而且由編譯器來保證類型的正確性)
#例如,在Java 實作加入泛型前,ArrayList 只維護一個Object 類型的陣列:
public class ArrayList{ private Object[] elementData; ... public Object get(int i){ ... } public void add(Object o){ ... } ... }
顯然,這樣的實作有兩個問題:
當取得一個值時,必須強制型別轉換:
ArrayList list = new ArrayList(); ... String str = (String)list.get(0);
list.add(new Integer(1));
對於我們的容器類,它是可以儲存任何物件的,但我們在使用時,一次只想也只應該向其中放入一種物件。基於這個需求,提出了型別參數化的概念,也就是泛型。
本質:實作的
型別參數化的概念,讓程式碼可以套用於多種類型
「泛型」核心
:告訴編譯器想使用什麼型別(指定特定型別參數或對其進行限制),然後編譯器幫你處理一切細節(類型「泛型」
初衷(1). 希望類別或方法具備最廣泛的表達能力,即透過解耦類別或方法與所使用的類型之間的約束;(2). 對容器類別而言,泛型在保證容器類別可以儲存任何類型物件的同時,又保證了容器類別一旦聲明自己將要保存的元素類型時,就不可再保存其他類型了,例如:
ArrayList<Fruit> list = new ArrayList<Fruit>(); fruits.add(new Fruit()); // OKfruits.add(new Apple()); // OKfruits.add(new Orange()); // OKfruits.add(new Object()); // Error
對象,但對於不屬於Fruit類型 的對象,編譯器杜絕將其放入列表中。 簡單地說:
泛型= 編譯時的類型檢查+ 編譯時的類型擦除(編譯器插入checkcast 等) + 運行時的
例1:
在 Java 1.7 以后,我们可以这样创建一个ArrayList:
ArrayList<String> list = new ArrayList<>;
list 变量的类型就决定了它引用的动态数组所能存储的元素类型,即后者的类型参数可以从变量中推断出。
例2:
public class TypeInference { public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) { Set<E> result = new HashSet<E>(s1); result.addAll(s2); return result; } public static void main(String[] args) { Set<Integer> integers = new HashSet<Integer>(); Set<Double> doubles = new HashSet<Double>(); Set<Number> numbers = null; //编译器的类型推断能力有限 numbers = TypeInference.union(integers, doubles); // Error numbers = TypeInference.<Number>union(integers, doubles); // OK } }
如例2所示,为 numbers 赋值时需要显式对类型参数实例化。
2.定义与语法
泛型类(参数化类)
public class Holder<T>{}
泛型接口(参数化接口)
public interface Generator<T>{}
泛型方法(参数化方法;所在类可以是泛型类,也可以不是;能够独立于类而产生变化;细粒度)
public <T> void f(T x){}
3.注意事项
只有当你希望使用的参数类型比某个具体类型(以及它的所有子类型)更加泛化时————也就是说,当你的代码能够跨多个类工作时,使用泛型才有所帮助;否则,使用多态就可以满足要求。
public class HasF { public void f(){...} }//以下两种实现方式所取得的效果是一样的//泛型实现class Manipulator1<T extends HasF>{ private T obj; public Manipulator1(T x){ this.obj = x; } public void manipulate(){ obj.f(); } }//多态实现class Manipulator2{ private HasF obj; public Manipulator2(HasF x){ this.obj = x; } public void manipulate(){ obj.f(); } }
泛型类的识别(误区)
先看下面两段代码:
// 第一段代码public class Pair<T> { private T first; private T second; public Pair(T first, T second){ this.first = first; this.second = second; } public void setFirst(T first){ this.first = first; } public T getFirst(){ return first; } public void setSecond(T second){ this.second = second; } public T getSecond(){ return second; } }
// 第二段代码public class DateInterval extends Pair<Date> { // 时间间隔类 public DateInterval(Date first, Date second){ super(first, second); } @Override public void setSecond(Date second) { super.setSecond(second); } @Override public Date getSecond(){ return super.getSecond(); } }
由泛型类的定义可知,Pair<T>
是一个泛型类,因为在类名后面有类型参数;类DateInterval 后面没有跟类型参数列表,因此该类就是一个 T 被替换为 Date 的实体类,其从 Pair<Date>
泛型类型 继承得到的方法列表,与泛型彻底无关。
对泛型类Linke<a href="http://www.php.cn/wiki/596.html" target="_blank">dL</a>ist<T>
的类型参数T
实例化所得到的不同泛型类型的理解
下图中,LinkedList<String>
,LinkedList<Point>
和 LinkedList<PolyLine>
是三种不同的类型,就像 Integer 和 String 一样,是两种互不相同的类型。但是,三者共享同一个 Class 对象,换句话说,三者在运行期的类型是一样的,但在编译期根据类型参数的不同成为截然不同的类型,下面代码可为例证。
public class TestClassTypes { public static void main(String[] args) { LinkedList<String> proverbs = new LinkedList<>(); LinkedList<Object> numbers = new LinkedList<>(); System.out.println("numbers class name: " + numbers.getClass().getName()); // Output: java.util.LinkedList System.out.println("proverbs class name: " + proverbs.getClass().getName()); // Output: java.util.LinkedList System.out.println("Compare Class objects: " + numbers.getClass().equals(proverbs.getClass())); // Output:true // 由于 LinkedList<String> 与 LinkedList<Object> 在编译期根本就是不同类型,所以下面代码编译不能通过: proverbs = (LinkedList<String>)numbers; // 类似于:把 Integer 类型实例强制转型为 String实例 赋给 String引用 // 每个类都是 Object 的子类 Object obj = (Object)numbers; System.out.println("obj class name " + obj.getClass().getName()); // Output: java.util.LinkedList // 会有转型安全的异常 proverbs = (LinkedList<String>)obj; System.out.println("obj in proverbs class name " + obj.getClass().getName()); // Output:java.util.LinkedList } }
在泛型类中, static 域或方法无法访问泛型类的类型参数;若静态方法需要使用泛型能力,就必须使其成为泛型方法(不与泛型类共享类型参数)
在一个类中,static 域或方法都是该类的 Class对象的成员,而我们知道泛型所创造出来的所有类型都共享一个 Calss对象, 因此实质上不受泛型参数限制,所以如下代码根本不能通过编译:
public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } }
但是要注意区分下面的一种情况:
public class Test2<T> { public static <T> T show(T one){//这是正确的 return null; } }
因为这是一个泛型方法
,在泛型方法中使用的 类型参数T
是自己在方法中定义的T
,而不是泛型类中的 T。
限制泛型可用的类型
在定义泛型类别时,预设可以使用任何的类型来实例化泛型类中的类型,但是如果想要限制使用泛型的类别时,即要求只能使用某个特定类型或者其子类型才能实例化该类型时,使用 extends 关键字指定这个类型必须是继承或者实现某个接口。一般地,当没有指定泛型继承的类型或实现的接口时,默认等价于使用 T extends Object
,因此,默认情形下任何类型都可以作为参数插入。特别地,为类型参数设定的第一个边界可以是类类型或接口类型,类型参数的第一个边界之后的任意额外边界都只能是接口类型,同时,一般将标记性接口放到靠后位置,这些类型参数之间有 & 相连接。
publc class MyClass<T extends Number & Serilizable>{ ... }
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型
在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一个父类的最小级,直到Object; 在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
//代码示例public class Test2{ public static void main(String[] args) { /**不指定泛型的时候*/ Integer i = Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型 Number f = Test2.add(1, 1.2); //这两个参数一个是Integer,一个是Double,所以取同一父类的最小级,为Number Object o = Test2.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object System.out.println(i.getClass().getName()); //输出: java.lang.Integer System.out.println(f.getClass().getName()); //输出: java.lang.Double System.out.println(o.getClass().getName()); //输出: java.lang.String /**指定泛型的时候*/ int a = Test2.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类 int b = Test2.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Double Number c = Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Double } //这是一个简单的泛型方法 public static <T> T add(T x,T y){ return y; } }
注意,这个例子中的两个输出是java.lang.Double和java.lang.String,而不是java.lang.Number和
java.lang.Object:
System.out.println(f.getClass().getName()); //输出: java.lang.DoubleSystem.out.println(o.getClass().getName()); //输出: java.lang.String
实际上,这个问题涉及泛型机制和多态两点。在例子中,类型参数T被编译器用Number替换,这是没问题的,因为无论整形还是浮点型都属于数型,这是由多态机制保证的。但是,无论x还是y,它们本质上还是各自的类型不会发生任何改变。要注意的是,这里的getClass()方法返回的变量的实际类型,即运行时类型而非编译时类型,因此返回y的类型是double而非number。
泛型的兼容性
(1) 从泛型类型生成的任何类型的引用都能存储到对应的原生类型的变量中
LinkedList list = new LinkedList<String>();
这样编写代码是合法兼容的,但是,不应该将这作为日常编程习惯的一部分,因为这种实践存在固有的风险:由于类型安全性检查是针对引用的,所以上述写法和如下写法实质上是一样的:
LinkedList list = new LinkedList();
(2) 从原生类型生成的引用能存储到任何类型的泛型类型的变量中
LinkedList<String> list1 = new LinkedList(); LinkedList<Integer> list2 = new LinkedList();
这样编写代码是合法兼容的,但是,由于我们可以将一个已经原生的 LinkedList对象 直接赋值此类引用,虽然在之后在添加元素是会进行类型安全检查,但之前的 LinkedList对象 所存储的元素可能五花八门,给程序带来隐患。具体请参照下图:
ArrayList 中 get 方法的源码:
/** * Returns the element at the specified position in this list. * * @param index index of the element to return * @return the element at the specified position in this list * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { RangeCheck(index); return (E) elementData[index]; // 只会让编译器认为该方法所返回的值是 E 类型,但此处转型不会起到预期效果,因为运行时 T 会被替换为 Object !!! }
primitive类型
不可以作为类型参数(八大类型)
若使用泛型方法可以取代将整个类泛型化,那么就应该使用泛型方法
泛型方法与可变参数列表可以很好的共存
public static <T> void f(T... args){}
1、 通配符
(1) 无界通配符
我们知道,通过为泛型类的每个类型形参提供类型实参,可以表达由这个泛型类定义的集合中的特定类型。例如,为了设定存储 String 的 ArrayList<String>
,就将类型参数设定为 String,所以动态数组类型就是 ArrayList<String>
。若不想为泛型类的类型参数提供具体类型,可以将参数设定为 “?”,这就是通配符的作用, 通配符类型可以表示任何类或接口类型
。
ArrayList<?> list = new ArrayList<String>(); list = new ArrayList<Double>(); list.add(e); // e cannot be resolved to a variable System.out.println(list1.size()); // OK
list 变量是ArrayList<?>
类型,所以能将指向任意类型的ArrayList<>
对象的引用存储在其中。但由于 list 是通配符类型参数的结果,所以存储引用的实际类型并不知道,因而无法使用这个变量调用任何与类型参数有关的方法。特别地,在 Java 集合框架中,对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素, 因为,其类型未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是 NULL(对 Null 而言,无所谓类型)。
(2) 深入理解无界通配符
我们有必要对以下三种类型进行区分:
List : 持有任何Object类型 的 原生List,编译器不会对原生类型进行安全检查;
List<?>
:具有某种特定类型 的 非原生List,编译器会进行安全检查;
List<Object>
: 编译器认为 List<Object>
是 List<?>
的子类型;
public class Wildcards { // Raw argument: static void rawArgs(Holder holder, Object arg) { holder.set(arg); // Warning: holder.set(new Wildcards()); // Same warning // OK, but type information has been lost: Object obj = holder.get(); } // Similar to rawArgs(), but errors instead of warnings: static void unboundedArg(Holder<?> holder, Object arg) { // holder.set(arg); // Error: // holder.set(new Wildcards()); // Same error // OK, but type information has been lost: Object obj = holder.get(); } }
2、 向上转型 / 通配符的上界 / 协变
在引入通配符的上界这一概念时,我们先看一下数组的一种特殊行为:基类型的数组引用
可以被赋予导出类型的数组
,如下面的代码所示:
class Fruit {} class Apple extends Fruit {} class Jonathan extends Apple {} class Orange extends Fruit {} public class CovariantArrays { public static void main(String[] args) { Fruit[] fruit = new Apple[10]; fruit[0] = new Apple(); // 编译期、运行期都 OK fruit[1] = new Jonathan(); // 编译期、运行期都 OK fruit[3] = new Fruit(); // 编译期 OK、运行期抛出 java.lang.ArrayStoreException(因为 fruit 的运行时类型是 Apple[], 而不是 Fruit[] 或 Orange[]) // 说明 Fruit[] 是 Apple[] 的父类型 System.out.println(Fruit[].class.isAssignableFrom(Apple[].class)); // true } }
由此可以说明:
由 12 行可知,该行代码编译期正常,则进一步说明:编译器的类型检查是针对引用的(Fruit型数组可以放入Fruit及其子类型对象);但在运行时,由于 fruit引用
实际上指的是一个 Apple数组
,而作为 Apple数组
则只可以向其中放入Apple及其子类型对象,因此当放入 Fruit对象时,抛出异常。
由 15 行可知,Fruit[] 是 Apple[] 的父类型,因此根据Java多态特性,前者可以指向后者对象。
我们知道,泛型的主要目标之一就是将这种错误检查移到编译期,那么,如果我们用泛型容器代替数组,那将会发生什么呢?
public class NonCovariantGenerics { List<Fruit> flist = new ArrayList<Apple>(); // Compile Error: Type Mismatch }
由以上代码可以知道,编译期根本不允许我们这么做。试想,如果编译期允许我们这样做,该容器就允许存入任何类型的对象,只要它是一种Fruit,而不像数组那样会抛出运行时异常,违背了泛型的初衷(泛型保证容器的类型安全检查)。所以,在编译期看来,List<Fruit> 和 List<Fruit> 根本就是两种不同的类型,并无任何继承关系
。
但是,有时你想要在以上两个类型之间建立某种向上转型关系,这就引出了通配符的上界。例如:
public class GenericsAndCovariance { public static void main(String[] args) { // 允许我们向上转型,向数组那样 List<? extends Fruit> flist = Arrays.asList(new Apple()); // Compile Error: can’t add any type of object: flist.add(new Apple()); // Compile Error flist.add(new Fruit()); // Compile Error flist.add(new Object()); // Compile Error flist.add(null); // Legal but uninteresting // We know that it returns at least Fruit: Fruit f = flist.get(0); Object o = flist.get(0); Apple a = flist.get(0); // Compile Error:Type mismatch flist.contains(new Apple()); // OK flist.indexOf(new Apple()); // OK } }
对于上述例子,flist 的类型就是List extends Fruit>了,但这并不意味着可以向这个 List 可以添加任何类型的 Fruit,甚至于不能添加 Apple。虽然编译器知道这个 List 持有的是 Fruit,但并不知道其具体持有哪种特定类型(可能是List
因此,一旦执行这种向上转型,我们就丢掉向其中添加任何对象的能力。更一般地,编译器会直接拒绝对参数列表中涉及通配符的方法的调用。因此,这意味着将由泛型类的设计者来决定哪些调用地安全的,并使用 Object类型 作为其参数类型,例如 contains 方法和 indexof 方法。例如,
public class Holder<T> { private T value; public Holder() { } public Holder(T val) { value = val; } public void set(T val) { value = val; } public T get() { return value; } public boolean equals(Object obj) { return value.equals(obj); } public static void main(String[] args) { Holder<Apple> Apple = new Holder<Apple>(new Apple()); Apple d = Apple.get(); Apple.set(d); Holder<? extends Fruit> fruit = Apple; // OK Fruit p = fruit.get(); d = (Apple) fruit.get(); // Returns ‘Fruit’,类型擦除,返回上界 // No warning,运行时异常 java.lang.ClassCastException Orange c = (Orange) fruit.get(); // fruit.set(new Apple()); // Cannot call set(),参数列表含通配符 // fruit.set(new Fruit()); // Cannot call set(),参数列表含通配符 fruit.equals(d); // OK,参数列表不含通配符 } }
3、超类型通配符 / 通配符的下界 / 逆变
我们可以使用超类型通配符
指定通配符的下界, 方法是<? super MyClass>
,甚至可以用在类型参数上<? super MyClass>(尽管我们不能对泛型参数给出一个超类型边界;即不能声明
。这使得我们可以安全的传递一个对象到泛型类型中,因此,有了超类型通配符,就可以向 Collection 写入了,如下图所示:
由图片可知,参数 apples 是 Apple 或 Apple的某种基类型 (例如:Fruit,Object,…) 的 List,也就是说,该 List 可以是 List
对于List super Apple>,在读取容器元素时,由于该容器所包含的元素可能是 Object类型、 Fruit类型 和 Apple类型,因此,从容器所读取到的元素只能确定是 Object类型的,如下面图片所示:
4、协变与逆变
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果 A,B 表示类型,f(⋅)表示类型转换,≤ 表示继承关系(比如,A ≤ B 表示A是B的子类);
f(⋅) 是逆变(contravariant)的,当 A≤B 时有 f(B)≤f(A) 成立;
f(⋅) 是协变(covariant)的,当 A≤B 时有 f(A)≤f(B) 成立;
f(⋅) 是不变(invariant)的,当 A≤B 时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
接下来,我们看看Java中的常见类型转换的协变性、逆变性或不变性:
(1).泛型
令f(A) = ArrayList<A>
,那么f(⋅) 是逆变、协变还是不变的呢?如果是逆变,则ArrayList<Integer>
是ArrayList<Number>
的父类型;如果是协变,则ArrayList<Integer>
是ArrayList<Number>
的子类型;如果是不变,二者没有相互继承关系。由于实际上ArrayList<Number>
和ArrayList<Integer>
无关,所以泛型是不变
的。
(2).数组
令f(A) = A[],容易证明数组是协变的:
Number[] numbers = new Integer[3];
5、实现泛型的协变与逆变
我们知道Java 中的泛型是不变的
,可我们有时需要实现泛型的逆变与协变,怎么办呢? 这时,通配符 ? 派上了用场:
<? extends>
实现了泛型的协变
,比如:
ArrayList<? extends Apple> l3 = new ArrayList<>(); ArrayList<? extends Fruit> l4 = new ArrayList<>(); l4 = l3;
对于 ArrayList<? extends Apple> 类型
,我们知道其表示某种具体类型(只是没有确定下来),但是无论其具体指的是ArrayList<Apple> 类型
还是ArrayList<Jonathan> 类型
,都是可以赋给ArrayList<? extends Fruit> 类型
的引用的,反之则不可以。因此,我们可以认为ArrayList<? extends Fruit> 类型
是ArrayList<? extends Apple> 类型
的父类型,故 <? extends>
实现了泛型的协变。
<? super>
实现了泛型的逆变
,比如:
ArrayList<? super Apple> l1 = new ArrayList<>(); ArrayList<? super Fruit> l2 = new ArrayList<>(); l1 = l2;
对于 ArrayList<? super Fruit> 类型
,我们知道其表示某种具体类型(只是没有确定下来),但是无论其具体指的是ArrayList<Fruit> 类型
还是ArrayList<Object> 类型
,都是可以赋给ArrayList<? super Apple> 类型
的引用的,反之则不可以。因此,我们可以认为ArrayList<? super Apple> 类型
是ArrayList<? super Fruit> 类型
的父类型,故 <? super>
实现了泛型的逆变。
6、PECS 准则 (producer-extends, consumer-super)
我们知道 <?>
表示:我想使用 Java泛型 来编写代码,而不是用原生类型;但是在当前这种情况下,我并不能确定下泛型参数的具体类型,因此用?
表示任何某种类型
。因此,根据我们对通配符的了解,使用无界通配符的泛型类不能够写数据,而在读取数据时,所赋值的引用也只能是 Object 类型。那么,我们究竟如何向泛型类写入、读取数据呢?
《Effective Java2》给出了答案: PECS : producer(读取)-extends, consumer(写入)-super。换句话说,如果输入参数表示一个 T 的生产者,就使用 extends T>;如果输入参数表示一个 T 的消费者,就使用 super T>。总之,通配符类型可以保证方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数。 比如,一个简单的 Stack API :
public class Stack<E>{ public Stack(); public void push(E e); public E pop(); public boolean isEmpty(); }
现在要实现 pushAll(Iterable<E> src)
方法,将实现 Iterable 接口的 src 的元素逐一入栈:
public void pushAll(Iterable<E> src){ for(E e : src) push(e) }
那么问题就来了:假设有一个实例化 Stack<Number>
的对象 stack(类型参数被实例化为Number),显然, 我们向这个 stack 中加入 Integer型
或<a href="http://www.php.cn/wiki/919.html" target="_blank">Float</a>型
元素都是可以的,因为这些元素本来就是Number型
的。因此, src 就包括但不限于 Iterable<Integer>
与 Iterable<Float>
两种可能;这时,在调用上述 pushAll方法
时,编译器就会产生 type mismatch
错误。原因是显而易见的,因为Java中泛型是不变的,Iterable<Integer>
与 Iterable<Float>
都不是 Iterable<Number>
及其子类型中的一种。所以,我们对 pushAll方法
的设计就存在逻辑上的问题。因此,应改为
// Wildcard type for parameter that serves as an E producerpublic void pushAll(Iterable<? extends E> src) { for (E e : src) push(e); }
这样,我们就可以实现将 实现Iterable接口
的 E类型的容器
中的元素读取到我们的 Stack 中。
那么,如果现在要实现 popAll(Collection<E> dst)方法
,将 Stack 中的元素依次取出并添加到 dst 中,如果不用通配符实现:
// popAll method without wildcard type - deficient!public void popAll(Collection<E> dst) { while (!isEmpty()) dst.add(pop()); }
同样地,假设有一个实例化 Stack<Number>
的对象 stack , dst 为 Collection<Object>
,显然,这是合理的。但如果我们调用上述的 popAll(Collection<E> dst)方法
,编译器会报出 type mismatch
错误,编译器不允许我们进行这样的操作。原因是显而易见的,因为 Collection<Object>
不是 Collection<Number>
及其子类型的一种。所以,我们对 popAll方法
的设计就存在逻辑上的问题。因此,应改为
// Wildcard type for parameter that serves as an E consumerpublic void popAll(Collection<? super E> dst) { while (!isEmpty()) dst.add(pop()); }
这样,我们就可以实现将 Stack<E>
中的 元素读取到我们的 Collection 中 。
在上述例子中,在调用 pushAll方法时 src生产了 E实例(produces E instances),在调用 popAll方法时 dst消费了 E实例(consumes E instances)
。Naftalin与Wadler 将 PECS 称为 Get and Put Principle。
此外,我们再来学习一个例子: java.util.Collections 的 copy 方法(JDK1.7),它的目的是将所有元素从一个列表(src)复制到另一个列表(dest)中。显然,在这里,src 是生产者,它负责产生 T类型的实例;dest 是消费者,它负责消费 T类型的实例。这完美地诠释了 PECS :
// List<? extends T> 类型的 src 囊括了所有 T类型及其子类型 的列表 // List<? super T> 类型的 dest 囊括了所有可以将 src中的元素添加进去的 List种类 public static <T> void copy(List<? super T> dest, List<? extends T> src) { // 将 src 复制到 dest 中 int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } } }
故有PECS总结:
输入参数是生产者时,用 ? extends T ;
输入参数是消费者时,用 ? super T ;
输入参数既是生产者又是消费者时,那么通配符类型没什么用了:因为你需要的是严格类型匹配,这是不用任何通配符而得到的;
无界通配符<?>
既不能做生产者(读出来的是Object),又不能做消费者(写不进去);
通常情况下,一个编译器处理泛型有两种方式:
1、Code Specialization
在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要针对string,integer,float产生三份目标代码。
2、Code Sharing
对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。
C++中的模板(template)是典型的Code specialization实现
C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string list是两种不同的类型。这样会导致代码膨胀(code bloat),不过有经验的C++程序员可以有技巧的避免代码膨胀。 另外,在引用类型系统中,这种方式会造成空间的浪费。因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code sharing方式处理泛型的主要原因。
Java 是典型的Code sharing实现
Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。
1、要点
类型擦除: 通过移除泛型类定义的类型参数并将定义中每个类型变量替换成对应类型参数的非泛型上界(第一个边界),得到原生类型(raw type)
类型擦除是 Java 泛型实现的一种折中,以便在不破坏现有类库的情况下,将泛型融入Java,并且保证兼容性。(泛型出现前后的Java类库互相兼容)
类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码(Class 对象)上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键
在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。
擦除是在编译期
完成的。类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。泛型类型只有在静态类型检查期间
才会出现,在此之后,程序中的所有泛型类型都将被擦除,并替换为它们的非泛型上界
。因此,在泛型代码内部,无法获得任何有关泛型参数类型的信息。
2、编译器是如何配合类型擦除的?
3、类型擦除的主要过程
对于Pair<>
//代码示例 Aclass Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
Pair<>的原始类型为:
//代码示例 Bclass Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
以下类型擦除示例:
//代码示例 1interface Comparable <A> { public int compareTo( A that); }
//代码示例 2final class NumericValue implements Comparable <NumericValue> { priva byte value; public NumericValue (byte value) { this.value = value; } public byte getValue() { return value; } public int compareTo( NumericValue that) { return this.value - that.value; } }
//代码示例 3class Collections { public static <A extends Comparable<A>> A max(Collection <A> xs) { Iterator<A> xi = xs.iterator(); A w = xi.next(); while(xi.hasNext()) { A x = xi.next(); if(w.compareTo(x) < 0) w = x; } return w; } }
//代码示例 4final class Test { public static void main (String[] args) { LinkedList<NumericValue> numberList = new LinkedList<NumericValue> (); numberList.add(new NumericValue((byte)0)); numberList.add(new NumericValue((byte)1)); NumericValue y = Collections.max( numberList ); } }
类型擦除后:
//代码示例 1 interface Comparable { public int compareTo( Object that); }
//代码示例 2final class NumericValue implements java.lang.Comparable{ //域 private byte value; //构造器 public NumericValue(byte); //方法 public int compareTo(NumericValue); public volatile int compareTo(java.lang.Object); //桥方法 public byte getValue( ); }
//代码示例 3class Collections { public static Comparable max(Collection xs) { Iterator xi = xs.iterator(); Comparable w = (Comparable) xi.next(); while (xi.hasNext()) { Comparable x = (Comparable) xi.next(); if (w.compareTo(x) < 0) w = x; } return w; } }
//代码示例 4final class Test { public static void main (String[ ] args) { LinkedList numberList = new LinkedList(); numberList.add(new NumericValue((byte)0)); , numberList.add(new NumericValue((byte)1)); NumericValue y = (NumericValue) Collections.max( numberList ); } }
第一个泛型类被擦除后, A被替换为最左边界 Object。由于Comparable是一个泛型接口,所以Comparable的类型参数NumericValue被擦除掉并将相关参数置换为 Object,但是这直接导致 NumericValue 没有实现接口(重写)Comparable的compareTo(Object that)方法,于是编译器充当好人,添加了一个桥方法(由编译器在编译时自动添加)
。
第二个示例中限定了类型参数的边界,A必须为Comparable的子类,按照类型擦除的过程,先将所有的类型参数替换为最左边界Comparable,得到最终的擦除后结果。
1、以参数化类型与原始类型的兼容性说明引用是类型检查所针对的对象
public class Test10 { public static void main(String[] args) { ArrayList<String> arrayList1=new ArrayList(); arrayList1.add("1"); //编译通过 arrayList1.add(1); //编译错误 String str1=arrayList1.get(0); //返回类型就是 String ArrayList arrayList2=new ArrayList<String>(); arrayList2.add("1"); //编译通过 arrayList2.add(1); //编译通过 Object object=arrayList2.get(0); //返回类型就是 Object new ArrayList<String>().add("11"); //编译通过 new ArrayList<String>().add(22); //编译错误 String string=new ArrayList<String>().get(0); //返回类型就是 String } }
因此我们可以得出结论:类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
2、所有动作都发生在边界处(对传递进来的值,编译器进行额外的检查;对真正传递出去的值,编译器自动插入的转型)
因为类型擦除的问题,所以所有的泛型类型最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
先看下面非泛型示例:
// 代码片段1public class SimpleHolder { private Object obj; public void setObj(Object obj) { this.obj = obj; } public Object getObj() { return obj; } public static void main(String[] args) { SimpleHolder holder = new SimpleHolder(); holder.setObj("Item"); String s = (String)holder.getObj(); } }
反编译这个类,得到下面代码片段:
public void setObj(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2; //Field obj:Ljava/lang/Object; 5: returnpublic java.lang.Object getObj(); Code: 0: aload_0 1: getfield #2; //Field obj:Ljava/lang/Object; 4: areturnpublic static void main(java.lang.String[]); Code: 0: new #3; //class SimpleHolder 3: dup 4: invokespecial #4; //Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5; //String Item 11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V 14: aload_1 15: invokevirtual #7; //Method getObj:()Ljava/lang/Object; 18: checkcast #8; //class java/lang/String 21: astore_2 22: return
现将泛型应用到上述代码,如下:
// 代码片段 2public class GenericHolder<T> { private T obj; public void setObj(T obj) { this.obj = obj; } public T getObj() { return obj; } public static void main(String[] args) { GenericHolder<String> holder = new GenericHolder<String>(); holder.setObj("Item"); String s = holder.getObj(); } }
反编译这个类,得到下面代码片段:
public void setObj(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2; //Field obj:Ljava/lang/Object; 5: returnpublic java.lang.Object getObj(); Code: 0: aload_0 1: getfield #2; //Field obj:Ljava/lang/Object; 4: areturnpublic static void main(java.lang.String[]); Code: 0: new #3; //class GenericHolder 3: dup 4: invokespecial #4; //Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5; //String Item 11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V 14: aload_1 15: invokevirtual #7; //Method getObj:()Ljava/lang/Object; 18: checkcast #8; //class java/lang/String 21: astore_2 22: return
在上述应用泛型的代码中,将
String s = holder.getObj();
替换为
holder.getObj();
反编译后,有代码片段:
public void setObj(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #2; //Field obj:Ljava/lang/Object; 5: returnpublic java.lang.Object getObj(); Code: 0: aload_0 1: getfield #2; //Field obj:Ljava/lang/Object; 4: areturnpublic static void main(java.lang.String[]); Code: 0: new #3; //class GenericHolder 3: dup 4: invokespecial #4; //Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #5; //String Item 11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V 14: aload_1 15: invokevirtual #7; //Method getObj:()Ljava/lang/Object; 18: pop 19: return}
首先,代码片段 1 和代码片段 2 二者所产生的字节码是相同的。看第15,它调用的是getObj()方法,返回值是Object,说明类型擦除了。然后第18,它做了一个checkcast操作,即检查类型#8, 在上面找#8引用的类型,它是一个String类型,即作String类型的强转。所以不是在get方法里强转的,是在你调用的地方强转的。对进入setObj()的类型进行检查是不需要的,因为这将由编译器执行。而对从getObj()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的–此处它将由编译器自动插入。也就是说,在泛型中,所有动作都发生在边界处
– 对传递进来的值进行额外的编译器检查,并由编译器自动插入对传递出去的值的转型。
其次,在未将 getObj() 的值赋给String时,由代码片段可知,编译器并未自动插入转型代码,可见所谓编译器自动插入对传递出去的值的转型
的前提条件
是:其必须是真正传递出去,即必须赋值给引用.(当然,虽然 getObj() 的返回值的类型是 Object, 但是其实质上是一个 String, 因此直接进行操作 “ getObj() instanceof String ”时,返回值也是 true.)
再看一段代码:
public class GenericArray<T> { private T[] array; public GenericArray(int sz) { array = (T[]) new Object[sz]; } public void put(int index, T item) { array[index] = item; } public T get(int index) { return array[index]; } public T[] rep() { return array; } public static void main(String[] args) { GenericArray<Integer> gai = new GenericArray<Integer>(10); gai.put(0, new Integer(4)); gai.get(0); Integer i = gai.get(0); // This causes a ClassCastException: Integer[] ia = gai.rep(); // This is OK: Object[] oa = (Object[])gai.rep(); } }
反编译得代码段:
public class GenericArray extends java.lang.Object{public GenericArray(int); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: aload_0 5: iload_1 6: anewarray #2; //class java/lang/Object 9: checkcast #3; //class "[Ljava/lang/Object;" 12: putfield #4; //Field array:[Ljava/lang/Object; 15: returnpublic void put(int, java.lang.Object); Code: 0: aload_0 1: getfield #4; //Field array:[Ljava/lang/Object; 4: iload_1 5: aload_2 6: aastore 7: returnpublic java.lang.Object get(int); Code: 0: aload_0 1: getfield #4; //Field array:[Ljava/lang/Object; 4: iload_1 5: aaload 6: areturnpublic java.lang.Object[] rep(); Code: 0: aload_0 1: getfield #4; //Field array:[Ljava/lang/Object; 4: areturnpublic static void main(java.lang.String[]); Code: 0: new #5; //class GenericArray 3: dup 4: bipush 10 6: invokespecial #6; //Method "<init>":(I)V 9: astore_1 10: aload_1 11: iconst_0 12: new #7; //class java/lang/Integer 15: dup 16: iconst_4 17: invokespecial #8; //Method java/lang/Integer."<init>":(I)V 20: invokevirtual #9; //Method put:(ILjava/lang/Object;)V 23: aload_1 24: iconst_0 25: invokevirtual #10; //Method get:(I)Ljava/lang/Object; 28: pop 29: aload_1 30: iconst_0 31: invokevirtual #10; //Method get:(I)Ljava/lang/Object; 34: checkcast #7; //class java/lang/Integer 37: astore_2 38: aload_1 39: invokevirtual #11; //Method rep:()[Ljava/lang/Object; 42: checkcast #12; //class "[Ljava/lang/Integer;" 45: astore_3 46: aload_1 47: invokevirtual #11; //Method rep:()[Ljava/lang/Object; 50: checkcast #3; //class "[Ljava/lang/Object;" 53: astore 4 55: return}
结合上面的结论,仔细观察反编译后代码中 checkcast
都用在什么地方,加深对边界就是发生动作的地方
和自动转型发生在调用处(需要检验两种类型时)
的理解。
25显示调用后,直接pop,而31显示在调用处,还要进行 checkcast 操作;
由于类型擦除,操作39之后,进行 checkcast 操作,强转为 Ljava.lang.Integer ,但是由代码【 array = (T[]) new Object[sz]; 】可知,其 new 的是 Object 数组,是不可能成功强转到 Integer 数组的,就像 Object 对象不能成功强转到 Integer 对象一样,会在运行时抛出 ClassCastException 异常;
由于类型擦除,操作47之后,进行 checkcast 操作,由于 rep() 返回的即为 Object 数组,而其要赋给的引用也是 Object[] ,因此不会抛出任何异常。
3、类型擦除与多态的冲突及其解决办法
先看两段代码:
// 第一段代码public class Pair<T> { private T first; private T second; public Pair(T first, T second){ this.first = first; this.second = second; } public void setFirst(T first){ this.first = first; } public T getFirst(){ return first; } public void setSecond(T second){ this.second = second; } public T getSecond(){ return second; } }
// 第二段代码public class DateInterval extends Pair<Date> { // 时间间隔类 public DateInterval(Date first, Date second){ super(first, second); } @Override public void setSecond(Date second) { super.setSecond(second); } @Override public Date getSecond(){ return super.getSecond(); } public static void main(String[] args) { DateInterval interval = new DateInterval(new Date(), new Date()); Pair<Date> pair = interval; //超类,多态 Date date = new Date(2000, 1, 1); System.out.println("原来的日期:"+pair.getSecond()); System.out.println("set进新日期:"+date); pair.setSecond(date); System.out.println("执行pair.setSecond(date)后的日期:"+pair.getSecond()); } }
原本子类重写父类的方法,无可非议。但是泛型类的类型擦除造成了一个问题,Pair的原始类型
中存在方法:
public void setSecond(Object second);
DateInterval中的方法:
public void setSecond(Date second);
我们的本意是想重写父类Pair中的setSecond方法,但是从方法签名上看,这完全是两个不同的方法,类型擦除与多态产生了冲突
。而实际情况呢?运行DateInterval的main方法,我们看到
public void setSecond(Date second)的确重写了public void setSecond(Object second)方法。这是如何做到的呢?
使用Java类分析器对其进行分析,结果:
public class DateInterval extends Pair{ //构造器 public DateInterval(java.util.Date, java.util.Date); //方法 public void setSecond(java.util.Date); public volatile void setSecond(java.lang.Object); //方法 1 public java.util.Date getSecond( ); //方法 2 public volatile java.lang.Object getSecond( ); //方法 3,它难道不会和方法 2 冲突? public static void main(java.lang.String[]); }
方法1和方法3是我们在源码中不曾定义的,它肯定是由编译器生成的。这个方法称为 桥方法(bridge method),真正覆写超类方法的是它。语句pair.setSecond(date)实际上调用的是方法1[public volatile void setSecond(Object)],通过这个方法再去调用public void setSecond(Date)。这个桥方法的实际内容是:
public void setSecond(Object second){ this.setSecond((java.util.Date) second); }
这样的结果就符合面向对象中多态的特性了,实现了方法的动态绑定。但是,这样的做法给我们带来了一种错觉,就认为public void setSecond(Date)覆写了泛型类的public void setSecond(Object)【其实也不是重写,二者方法参数都不同】,如果我们在DateInterval中增加一个方法:
public void setSecond(Object obj){ System.out.println("覆写超类方法!"); }
编译器会报如下错误:Name clash: The method setSecond(Object) of type DateInter has the same erasure as setSecond(T) of type Pair<T> but doesn't override it.
即,同一个方法不能被重写两次。
为了实现多态,我们知道方法3也是由编译器生成的桥方法。方法擦除带来的第二个问题就是:由编译器生成的桥方法 public volatile java.lang.Object getSecond() 方法和 public java.util.Date getSecond() 方法,从方法签名的角度看是两个完全相同的方法,它们怎么可以共存呢? 如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情。
补充说明:
从JDK1.5开始,在一个方法覆盖另一个方法时可以指定一个更严格(窄)的返回类型,它的机制也是同样使用的桥方法。
例如,
public class A { public List getList(){ return null; } }public class ASub extends A{ @Override public ArrayList getList(){ return null; } }
使用Java 类分析器对ASub分析可得:
public class ASub extends A{ //域 //构造器 public ASub( ); //方法 public java.util.ArrayList getList( ); public volatile java.util.List getList( ); //桥方法}
4、泛型类型变量不能是基本数据类型
类型参数不能是基本类型。也就是说,没有ArrayList<double>
,只有ArrayList<Double>
。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值
,只能引用Double
的值。
解决之道: 使用基本类型的包装器类以及Java SE5的自动包装机制。
5、转型和警告
使用带有泛型类型参数的转型或 instanceof 不会有任何效果
,例如:
class FixedSizeStack<T> { private int index = 0; private Object[] storage; public FixedSizeStack(int size) { storage = new Object[size]; } public void push(T item) { storage[index++] = item; } public T pop() { //Warnning: Unchecked cast from Object to T return (T) storage[--index]; } }public class GenericCast { public static final int SIZE = 10; public static void main(String[] args) { FixedSizeStack<String> strings = new FixedSizeStack<String>(SIZE); for (String s : "A B C D E F G H I J".split(" ")) strings.push(s); for (int i = 0; i < SIZE; i++) { String s = strings.pop(); System.out.print(s + " "); } } }
由于擦除的原因,T 被擦除到它的第一个边界 Object,因此pop()实际上只是将Object转型为Object。换句话说,pop()方法实际上并没有执行任何转型。
6、任何在运行时需要知道确切类型信息的操作都将无法工作
instanceof操作
的右操作数不能带有泛型类型参数;
new 操作 :
可以 new 泛型类型(eg: ArrayList,…),但不能 new 泛型参数(T,…);
泛型数组 :
不可以创建带有泛型类型参数的数组(若需要收集参数化类型对象,可以直接使用 ArrayList:ArrayList<Pair<String>>最安全且有效。
);
转型 :
带有泛型类型参数的转型不会有任何效果;
例如:
关于由类型擦除引起的 instance of T,new T 和创建数组T 等问题,可以引入类型标签Class<T>
来解决,例如:
class Building {} class House extends Building {}public class ClassTypeCapture<T> { Class<T> kind; public ClassTypeCapture(Class<T> kind) { this.kind = kind; } public boolean f(Object arg) { return kind.isInstance(arg); } public static void main(String[] args) { ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println(ctt1.f(new Building())); // true System.out.println(ctt1.f(new House())); // true ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class); System.out.println(ctt2.f(new Building())); // true System.out.println(ctt2.f(new House())); // true } }
7、实现参数化接口
一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口,例如:
public Person implements Comparable<Person>{ ... } // OK class HonorPerson extends Person implements Comparable<HonorPerson>{ ... } // Error
HonorPerson 类不能编译,因为擦除会将Comparable<Person>
和Comparable<HonorPerson>
简化为相同的接口 Comparable, 上面的代码意味着重复实现相同的接口。但是,下面的代码可以通过编译:
public Person implements Comparable{ ... } // OK class HonorPerson extends Person implements Comparable{ ... } // OK
这种差别在于:编译器对泛型的特别处理方式。
8、异常中使用泛型的问题
由于类型擦除的原因,将泛型应用于异常是非常受限的。catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。
不能抛出也不能捕获泛型类的对象
事实上,泛型类扩展Throwable都不合法
(Exception是Throwable的子类)。例如:下面的定义将不会通过编译
public class Problem<T> extends Exception{......}
为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,再看下面的定义:
try{ }catch(Problem<Integer> e1){ ... }catch(Problem<Number> e2){ ... }
在运行时,类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就“相当于”下面的这样:
try{ }catch(Problem<Object> e1){ ... }catch(Problem<Object> e2){ ... }
这当然就是不行的, 就好像catch了两个一模一样的普通异常,编译器就不能通过编译一样。
不能再catch子句中使用泛型变量
例如:
public static <T extends Throwable> void doWork(Class<T> t){ try{ ... }catch(T e){ //编译错误 ... } }
因为泛型信息在编译的时候已经变为原始类型,也就是说上面的 T 会变为原始类型Throwable,那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢:
public static <T extends Throwable> void doWork(Class<T> t){ try{ ... }catch(T e){ //编译错误 ... }catch(IndexOutOfBounds e){ } }
根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。
类型变量可以使用在异常声明中
public static<T extends Throwable> void doWork(T t) throws T{ try{ ... }catch(Throwable realCause){ t.initCause(realCause); throw t; }
此时,虽然T也会被擦除为Throwable,但由于用在声明中,因此是合法的。
9、类型擦除后的冲突
当泛型类型被擦除后,创建条件不能产生冲突:
class Pair<T> { public boolean equals(T value) { return null; } }
考虑Pair<>:
public boolean equals(T value){}
擦除后变为
boolean equals(Object)
这与 Object.equals 方法是冲突的
!当然,补救的办法是重新命名引发错误的方法。
10、动态类型安全
先看以下代码:
public class CheckedList { @SuppressWarnings("unchecked") static void oldStyleMethod(List probablyDogs) { //原生List probablyDogs.add(new Cat()); } public static void main(String[] args) { List<Dog> dogs1 = new ArrayList<Dog>(); oldStyleMethod(dogs1); // Quietly accepts a Cat List<Dog> dogs2 = Collections.checkedList( new ArrayList<Dog>(), Dog.class); try { oldStyleMethod(dogs2); // Throws an exception } catch(Exception e) { System.out.println(e); } // Derived types work fine: List<Pet> pets = Collections.checkedList( new ArrayList<Pet>(), Pet.class); pets.add(new Dog()); pets.add(new Cat()); } } /* Output: java.lang.ClassCastException: Attempt to insert class typeinfo.pets.Cat element into collection with element type class typeinfo.pets.Dog
使用 Collections 的静态方法:checkedCollection( ), checkedList( ), checkedMap( ), checkedSet( ),
checkedSortedMap( ) 和 checkedSortedSet( ) 可以在运行时便知道罪魁祸首在哪里,而不必等到将对象从容器中取出时。
以上是Java 泛型(Generics) 綜述範例程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!