参考资料:http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html
oracle官网介绍:https://docs.oracle.com/javase/tutorial/extra/generics/intro.html
同一系列:
Java范型那些事(二)
Java范型那些事(三)
在JDK1.5 加入了范型,范型可以增加代码的稳定性,使得在编译时能检查出更多的潜在bug,尤其是为集合类增加了编译时的类型安全,并减少了类型转换的苦差事。
目录
1. 简介
2. 定义简单的范型
3. 泛型和子类型
4. 通配符
5. 范型方法
泛型允许您抽象类型。最常见的示例是容器类型,例如Collections层次结构中的容器类型。
以下是此类的典型用法:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
第3行的类型转换有点烦人。通常,程序员知道将哪种数据放入特定列表中。但是,类型转换是必不可少的。编译器只能保证迭代器返回一个Object。为确保赋值为Integer类型的变量是类型安全的,需要强制转换。
当然,类型强转不仅引入了混乱,它还引入了运行时错误的可能性,因为程序员可能会弄错。
如果程序员实际上可以表达他们的意图,并将列表标记为限制包含特定的数据类型,该怎么办?这是范型背后的核心理念。以下是使用泛型给出的上述程序片段的一个版本:
List myIntList = new LinkedList(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
注意变量myIntList的类型声明,它指定这不是一个任意List,而是一个数据类型是Integer的List,写成List
通过这种方式,编译器现在可以在编译时检查程序的类型正确性。当我们说使用类型List
以下是java.util包中接口List和Iterator的定义的一小段摘录:
public interface List {
void add(E x);
Iterator iterator();
}
public interface Iterator {
E next();
boolean hasNext();
}
在简介中,我们看到了泛型类型声明List的调用,例如List
⚠️注意:范型的声明类似于接口和类一样,都只有一个文件,不会增加多个代码副本。可以将类型参数理解成方法或者构造函数中的普通形式参数,在调用范型方法时,实际的类型参数将替换形式类型参数。
关于类型参数的命名,建议使用简洁的单个大写字符,在集合类中通常使用 E 来表示元素的类型,List
通常,如果Foo是Bar的子类型(子类或子接口),并且G是一些泛型类型声明,则G
所以如果将子类型的范型对象赋值给父类型的范型对象,将会引起编译错误。
官网举了以下这个例子:
List ls = new ArrayList(); // 1
List
编译器会在其中第二行报错,解决办法见下一节 通配符
Java中会遇到类似List< ? extends CustomClass>的上界通配符和List super CustomClass>的下界通配符,用以限定类型参数的范围,以下介绍以下为什么会要这样来做。
在JDK 1.5以前的集合遍历代码中,大概是以下样式:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
在JDK1.5引入范型后,可以这样来写:
void printCollection(Collection
但这并没有什么比旧版本好到哪去,因为旧版本中,可以使用任意的集合类型作为参数,新版本中,只能使用Collection
那么各种集合类的超类型是什么?它写成Collection <?>(发音为“Collection of unknown”),即元素类型与任何东西匹配的集合。由于显而易见的原因,它被称为通配符类型。我们可以写:
void printCollection(Collection <?> c){
for(Object e:c){
System.out.println(E);
}
}
现在,我们可以用任何类型的集合来调用它。请注意,在printCollection()内部,我们仍然可以从c中读取元素并为它们指定类型Object。这总是安全的,因为无论集合的实际类型如何,它都包含对象。然而,向它添加任意对象是不安全的:
Collection> c = new ArrayList();
c.add(new Object()); // Compile time error
由于我们不知道c的元素类型代表什么,我们无法向其添加对象。 add()方法接受类型为E的参数,即集合的元素类型。当实际类型参数是?时,它代表某种未知类型。我们传递给add的任何参数都必须是这种未知类型的子类型。因为我们不知道它是什么类型,所以我们无法传递任何内容。唯一的例外是null,它是每种类型的成员。
另一方面,给定List <?>,我们可以调用get()并使用结果。结果类型是未知类型,但我们始终知道它是一个对象。因此,可以安全地将get()的结果赋给Object类型的变量,或者将其作为期望Object类型的参数传递。
但是在我们的项目中,经常需要限定该集合中可以存放哪些有某一共同特征的类的对象,但又不能使用List
其中的?代表一种未知的类型,就像我们之前看到的通配符一样。但是,在这种情况下,我们知道这种未知类型实际上是Shape的子类型。 (注意:它可能是Shape本身,或者是某些子类;它不需要字面上扩展Shape。)我们说Shape是通配符的上限
但是这里使用通配符后,在灵活性上却付出了代价,就是在方法体中添加这样的代码是非法的:
public void addRectangle(List <?extends Shape> shapes){
//编译时错误!
shapes.add(0,new Rectangle());
}
⚠️注意:您应该能够弄清楚为什么不允许上面的代码。 shapes.add()的第二个参数的类型是? extends Shape-- Shape的未知子类型。由于我们不知道它是什么类型,我们不知道它是否是Rectangle的超类型;它可能是也可能不是这样的超类型,所以在那里传递一个Rectangle是不安全的,但有界通配符却是可以解决传递进来的参数被限定为某些类型的解决方案。
先从一个例子说起,从一个数组中读取对象,存放到集合中,第一版代码如下:
static voidstatic void fromArrayToCollection(Object[] a, Collection> c) {
for (Object o : a) {
c.add(o); // compile-time error
}
}
现在你应该不会像初学者一样,再使用 Collection
Collection>同样也不行,如之前所说,unknown类型的集合,无法向其中添加元素。
这时候就可以使用范型方法来解决该问题,同范型类的声明一样,方法声明也可以是范型的,即使用一个或多个参数化的类型:
static void fromArrayToCollection(T[] a, Collection c) {
for (T o : a) {
c.add(o); // Correct
}
}
按照上面的范型方法,然后我们可以使用任何元素类型是数组元素类型的超类型的集合来调用该方法。
Object[] oa = new Object[100];
Collection
⚠️注意:我们不必将实际类型参数传递给泛型方法。编译器根据实际参数的类型为我们推断类型参数。它通常会推断出使得调用时类型安全的最具体的类型参数。
出现的一个问题是:何时应该使用泛型方法,何时应该使用通配符类型?为了理解答案,我们来看一下Collection库中的一些方法。
interface Collection {
public boolean containsAll(Collection> c);
public boolean addAll(Collection extends E> c);
}
我们也可以使用范型方法替代上面的代码:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
// 嘿,类型变量也可以有界限!
}
但是,在containsAll和addAll中,类型参数T仅使用一次。返回类型不依赖于类型参数,也不依赖于方法的任何其他参数(在上述情况下,只有一个参数)。这告诉我们类型参数用于多态;它唯一的作用是允许在不同的调用站点使用各种实际的参数类型。如果是这种情况,则应使用通配符。通配符旨在支持灵活的子类型,这是我们在此尝试表达的内容。
范型方法允许使用类型参数来表示方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。如果没有这种依赖关系,则不应使用范型方法。
可以串联使用范型方法和通配符。这是方法Collections.copy():
class Collections {
public static void copy(List dest, List extends T> src) {
...
}
注意两个参数类型之间的依赖关系。从源列表src复制的任何对象必须可分配给目标列表的元素类型T,dst。所以src的元素类型可以是T的任何子类型 - 我们不关心哪个。copy方法使用类型参数表示依赖关系,但对第二个参数的元素类型使用了通配符。
我们可以用另一种方式为这种方法编写签名,而不使用通配符:
class Collections {
public static void copy(List dest, List src) {
...
}
这很好,但是第一个类型参数T既在dst的类型中使用,也在第二个类型参数S的边界中使用,且S本身只使用一次(即在src的类型中 - 没有别的依赖它)。这表明我们可以用通配符替换S。使用通配符比声明明确的类型参数更清晰,更简洁,因此应尽可能优先使用。
通配符还具有以下优点:它们可以在方法签名之外使用,如字段类型,局部变量和数组。如:
static List> history = new ArrayList>();
public void drawAll(List extends Shape> shapes) {
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最后,再次让我们注意用于类型参数的命名约定。我们使用T作为类型,只要没有更具体的类型来区分它。范型方法通常就是这种情况。如果有多个类型参数,我们可能会使用字母表中与T相邻的字母,例如S。如果泛型方法出现在泛型类中,最好避免对方法的类型参数使用相同的名称,以避免混淆。这同样适用于嵌套泛型类。