定义泛型接口、类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//定义接口时指定了一个泛型形参,该形参名为E
public interface List<E> {
//在该接口里,E可作为类型使用
//下面方法可以使用E作为参数类型
void add(E x);
Iterator<E> iterator(); // ①
...
}

//定义接口时制定了一个泛型形参,该形参名为E
public interface Iterator<E> {
//在该接口里E完全可以作为类型使用
E next();
boolean hasNext();
...
}

//定义该接口时是定了两个泛型形参,其形参名为K、V
public interface Map<K , V> {
//在该接口里,K、V完全可以作为类型使用
Set<K> keySet(); // ②
V put(K key, V value);
...
}

①②处方法声明返回值类型是Iterator 、Set,这表明Set形式是一种特殊的数据类型,是一种与Set不同的数据类型——可以认为是Set类型的子类。

例如使用List类型时,如果为E形参传入String类型实参,则产生了一个新的类型:List类型,可以把List想象成E被全部替换成Stirng的特殊List子接口。

1
2
3
4
5
6
7
//List<String>等同于如下接口
public interface ListString extends List{
//原来的E形参全部变成String类型实参
void add(String x);
Iterator<String> iterator();
...
}

虽然程序只定义了一个List接口,但实际使用时可以产生无数个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。

必须指出:List 绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。

并不存在泛型类

1
2
3
4
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
//调用getClass()方法来比较l1和l2的类是否相等
System.out.println(l1.getClass() == l2.getClass());

运行上面代码输出为true。因为不管泛型的实际类型参数是什么,他们在运行时总有同样的类。

不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参。

例如:

1
2
3
4
5
6
7
8
public class R<T> {
//下面代码错误,不能在静态变量声明中使用泛型形参
static T info;
T age;
public void foo(T msg){}
//下面代码错误,不能在静态方法声明中使用泛型形参
public static void bar(T msg){}
}

由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。

例如下面代码是错误的

1
2
3
java.util.Collection<String> cs = new java.util.ArrayList<>();
//下面代码编译时引起错误:instanceof运算符后不能使用泛型
if (cs instanceof java.util.ArrayList<String>) {...}

类型通配符

如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型。

在Java的早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此Java在泛型设计时进行了改进,它不再允许把List对象赋值给List变量。

数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但G不是G的子类型。Foo[]自动向上转型为Bar[]的方式被称为型变。也就是说,Java的数组支持型变,但Java集合并不支持型变。

使用类型通配符

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号,将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号被称为通配符,它的元素类型可以匹配任何类型。

1
2
3
4
5
public void test(List<?> c){
for (int i = 0; i < c.size(); i++){
System.out.println(c.get(i));
}
}

现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是完全的,因为不管List的真实类型是什么,它包含的都是Object。

但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。

1
2
3
List<?> c = new ArrayList<String>();
//下面程序引起编译错误
c.add(new Object());

因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。

程序可以调用get()方法来返回List<?>集合执行索引处的元素,其返回值是一个未知类型,但可以肯定的是它总是一个Object。因此把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。

设定类型通配符的上限

定义三个形状类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义一个抽象类Shape
public abstract class Shape{
public abstract void draw(Canvas c);
}
//定义Shape的子类Circle
public class Circle extends Shape{
//实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c){
System.out.println("在画布" + c + "上画一个圆");
}
}
//定义Shape的子类Rectangle
public class Rectangle extends Shape{
//实现画图方法,以打印字符串来模拟画图方法实现
pubic void draw(Canvas c){
System.out.println("把一个矩形画在画布" + c + "上");
}
}

定义Canvas类:

1
2
3
4
5
6
7
8
public class Canvas{
//同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes){
for (Shape s : shapes){
s.draw(this);
}
}
}
1
2
3
4
List<Circle> circleList = new ArrayList();
Canvas c = new Canvas();
//不能把List<Circle> 当成 List<Shape> 使用,所以下面代码引起编译错误
c.drawAll(circleList);

List并不是List的子类型,所以不能把List对象当成List使用。

为了表示List的父类,可以考虑使用List<?>,但此时从List<?>集合中取出的元素只能被编译器当成Object处理。为了表示List集合的所有元素是Shape的子类,Java泛型提供了被限制的泛型通配符。

1
2
//它表示泛型形参必须是Shape子类的List
List<? extends Shape>

改写上面的Canvas程序:

1
2
3
4
5
6
7
8
public class Canvas{
//同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes){
for (Shape s : shapes){
s.draw(this);
}
}
}

List<? extends Shape>可以表示List、List的父类,Shape称为这个通配符的上限。

类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。例如:

1
2
3
4
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes.add(0, new Rectangle());
}

这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。

对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A就相当于A<? extends Foo>的子类,可以将A赋值给A<? extends Foo>类型的变量,这种型变方式被称为协变

对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。

口诀是:协变只出不进!

对于指定通配符上限的泛型类,相当于通配符上限是Object

设定类型通配符的下限

通配符的下限用<? super 类型>的方式来指定。

比如Foo是Bar的子类,当程序需要一个A<? super Bar>变量时,程序可以将A、A赋值给A<? super Bar>类型的变量,这种形变方式被称为逆变。

对于逆变的泛型集合来说,编译器只知道集合元素的下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyUtils{
//下面dest集合元素的类型必须与src集合元素的类型相同,或者是其父类
public static <T> T copy(Collection<? super T> dest, Collection<T> src){
T last = null;
for (T ele : src) {
last = ele;
//逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}
public static void main(String[] args){
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
li.add(5);
//此处可准确地知道最后一个被复制的元素是Integer类型
//与src集合元素的类型相同
Integer last = copy(ln , li);
System.out.println(ln);
}
}

设定泛型形参的上限

1
2
3
4
5
6
7
8
9
10
public class Apple<T extends Number>{
T col;
public static void main(String[] args){
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
//下面代码将引发编译异常,下面代码试图把String类型传给T形参
//但String不是Number的子类
//Apple<String> as = new Apple<>();
}
}

定义泛型方法

定义一个方法,将一个Object数组的所有元素添加到一个Collection集合中。

1
2
3
4
5
static void fromArrayToCollection(Object[] a, Collection<Object> c){
for (Object o : a){
c.add(o);
}
}
1
2
3
4
String[] strArr = {"a", "b"};
List<String> strList = new ArrayList<>();
//Collection<String>对象不能当成Collection<Object>使用,下面代码出现编译错误
fromArrayToCollection(strArr, strList);

上面方法的参数类型不可以使用Collection,那使用通配符Collection<?>是否可行呢?

显然也不行,因为Java不允许把对象放进一个未知类型的集合中。

采用支持泛型的方法,就可以将上面的fromArrayToCollection方法改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class GenericMethodTest{
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for(T o : a){
c.add(o);
}
}
public static void main(String[] args){
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
//下面代码中T代表Object类型
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
//下面代码中T代表String类型
fromArrayToCollection(sa, cs);
//下面代码中T代表Object类型
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
//下面代码中T代表Number类型
fromArrayToCollection(ia, cn);
//下面代码中T代表Number类型
fromArrayToCollection(fa, cn);
//下面代码中T代表Number类型
fromArrayToCollection(na, cn);
//下面代码中T代表Object类型
fromArrayToCollection(na, co);
//下面代码中T代表String类型,但na是一个Number数组
//因为Number既不是String类型,也不是它的子类,所以出现编译错误
//fromArrayToCollection(na, cs);
}
}

泛型方法和类型通配符的区别

大多数时候都可以使用泛型方法来代替类型通配符。

类型通配符形式:

1
2
3
4
5
public interface Collection<E>{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
...
}

泛型方法形式:

1
2
3
4
5
public interface Collection<E>{
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}

上面两个方法中泛型形参T只使用了一次,T产生的唯一效果是可以在不同的调用点传入不同的实际类型。

对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的依赖关系,就不应该使用泛型方法。

如果有需要 ,也可以同时使用泛型方法和通配符,如Java的Collections.copy()方法。

1
2
3
4
public class Collections{
public static <T> void copy(List<T> dest, List<? extends T> src){...}
...
}

上面copy方法中dest和src存在明显的依赖关系,所以src元素的类型只能是dest元素的类型的子类型或者它本身。

但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。因为该方法无须向src集合中添加元素,也无须修改src集合的元素,所以可以使用类型通配符,无须使用泛型方法。

简而言之,指定上限的类型通配符支持协变,因此这种协变的集合可以安全地取出元素(协变只出不进),因此无须使用泛型方法。

当然也可以将上面的方法签名改为使用泛型方法,不使用类型通配符。

1
2
3
4
public class Collections{
public static <T , S extends T> void copy(List<T> dest, List<S> src){...}
...
}

但泛型形参S仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那泛型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法更加清晰明确,因此Java设计该方法时采用了通配符。

类型通配符与泛型方法还有一个显著的区别:

  • 类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型。
  • 泛型方法中的泛型形参必须在对应方法中显式声明。