Java泛型

Java泛型

感觉java泛型设计得挺复杂,特别是泛型,泛型通配符,泛型上限下限等搅在一起时,实在不太好区分,容易出错。不管怎样,泛型对于设计可复用和扩展的代码支持还是很强大的,这里总结一下自己的理解。《疯狂java讲义》讲解的非常好,想深入理解可以参考该书,另外后面的代码也是参考该书。

泛型就是指在定义类,借口,方法使用类型形参,这个类型形参将在声明变量,创建对象,调用方法动态地指定,可以极强的增强代码的通用性。

理解泛型

定义泛型接口,类

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface List<E>
{
void add(E x);
Iterator<E> iterator();
}

public interface Iterator<E>
{
E next();
boolean hasNext();
}

public interface Map<K, V>
{
Set<K> keySet();
V put(K key, V value);
}
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
public class Apple<T>
{
// 使用T类型形参定义实例变量
private T info;
public Apple(){}
// 下面方法中使用T类型形参来定义构造器
public Apple(T info)
{
this.info = info;
}
public void setInfo(T info)
{
this.info = info;
}
public T getInfo()
{
return this.info;
}
public static void main(String[] args)
{
// 因为传给T形参的是String实际类型,
// 所以构造器的参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 因为传给T形参的是Double实际类型,
// 所以构造器的参数只能是Double或者double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}

从泛型类派生子类

为Apple派生子类时,不能在包含形参,下面代码是错误的:

1
public class A extends Apple<T>{}

必须要改为:

1
public class A extends Apple<String>{}

或者

1
public class A extends Apple{}

需要注意的是,这时系统会把Apple<T>类里的T当做Object类型来处理。

并不存在泛型类

ArrayList<String>只是类似于一种逻辑类,并不是真正产生了一个新的class文件或新的类。

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

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

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

1
2
Collection<String> cs = new ArrayList<>();
if(cs instanceof ArrayList<String>){...} //引发编译错误

类型通配符

List<Integer> 并不是List<Object>的子类型!
也就是:

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

并不能输入List<Integer> c作为参数。

使用类型通配 —(?)

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

也就是,在 public void test(List<?> c)的函数,可以输入List<Integer> cList<String> c,List<Character> c作为参数。

可以通过这种带通配符的List遍历元素,但是,不能这种带通配符的List将元素加入到其中:

1
2
3
4
5
6
List<Integer> list = new ArrayList<Integer>();
List<?> c = list;
//或者 List<?> c = new ArrayList<String>();

c.add(new Object()); // 应发编译错误
c.add(12); // 引发编译错误

原因是系统无法确定c集合中元素的类型,所以不能向其中添加对象。根绝前面List<E>接口的定义代码可以发现,add()方法需要有类型参数E作为集合元素的类型,传给add的参数必须是E类的对象或者其子类的对象,但在这中情况下,因为并不知道E是什么类型(?),所以程序无法判断尝试放入的对象是否合法,所以无法放入元素。唯一的例外是null。

另一方面程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值一个未知类的元素,但可以肯定的是,它总是一个Object对象,所以一定可以用Object接受,如果知道其类型,也可以使用类型转换转到所需要的对象类型。

设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但在某些情况,我们不希望这个List是任何List的父类,而是代表某一类泛型List的父类。

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}

// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}

// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}

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



public static void main(String[] args)
{
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
c.drawAll(circleList);
}
}

List<? extends Shape>是受限制通配符的例子,此处?同样代表一个未知的类型,但是此处的未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound).

这时drawAll(List<? extends Shape> shapes)的参数就限定了泛型参数必须为Shape或者其子类,而不能是其他,例如List c就不能作为drawAll的参数,否则会引发编译错误。

类似的,由于系统无法确定这个受限制的通配符的具体类型,所以不能不能通过shapes把对象添加到这个泛型中,即使是Shape的对象或子类对象:

1
2
3
4
public void addRectangle(List<? extends Shape> shapes)
{
shapes.add(new Rectangle()); //编译错误
}

设定类型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

1
2
3
4
5
public class Apple<T extends Number>
{
T col;
....
}

泛型方法

在定义类,接口时没有使用类型形参,但定义方法时想自己定义类型形参,这时可以使用泛型方法。

定义泛型方法

考虑实现这样一个方法,该方法将一个Object数组的所有元素添加到一个Collection集合中。

1
2
3
4
5
fromArrayToCollection(Object[] a, Collection<Object> c) {
for(Object o : a ) {
c.add(o);
}
}

问题,并不能使用Collection, 同样不能使用:

1
2
3
4
5
fromArrayToCollection(Object[] a, Collection<?> c) {
for(Object o : a ) {
c.add(o); // 编译错误
}
}

使用泛型方法解决问题:

1
修饰符 <T, S> 返回值类型 方法明<形参列表> {}
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
35
36
37
38
39
40
public class GenericMethodTest
{
// 声明一个泛型方法,该泛型方法中带一个T类型形参,
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类型
<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);
}
}

与类,接口使用泛型参数不同的是,方法中的泛型参数,方法中的泛型参数无需显式传入实际类型参数,如上面所示,调用方法时,无需在调用方法前传入String,Object等类型,系统完成推断,但是如果传入的参数的类型不同,系统则无法准确判断该使用哪个类型,出现错误。

如果传入的参数有不同的形参,但是属于同一类型,可以借助带上限的通配符解决。否则需要声明多个泛型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RightTest
{
// 声明一个泛型方法,该泛型方法中带一个T形参
static <T> void test(Collection<? extends T> from , Collection<T> to)
{
for (T ele : from)
{
to.add(ele);
}
}
public static void main(String[] args)
{
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码完全正常
test(as , ao);
}
}

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

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

1
2
3
4
5
6
7
8
9
10
public interface Collection<E> {
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
}

public interface Collection<E>
{
<T> boolean containAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
}

上面两个方法类型形参T只使用了依次,产生的唯一效果是可以在不同的调用点传入不同的实际类型,对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

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

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

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

Java 7的”菱形”语法与泛型构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo
{
public <T> Foo(T t)
{
System.out.println(t);
}
}
public class GenericConstructor
{
public static void main(String[] args)
{
// 泛型构造器中的T参数为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T参数为Integer。
new Foo(200);
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String> Foo("疯狂Android讲义");
// 显式指定泛型构造器中的T参数为String,
// 传给Foo构造器的实参也是Double对象,下面代码出错
new <String> Foo(12.3);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass<E>
{
public <T> MyClass(T t)
{
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest
{
public static void main(String[] args)
{
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
//MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}

设定通配符下限

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
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);
}
}

搽除和转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用泛型声明的类时不指定实际的类型参数。如果没有为这个泛型类指定实际的类型参数,则该类型参数被称作raw type,默认是声明该类型参数时制定的第一个个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量是,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了类型参数的上限(即Object)。List<Integer>则变成Number,下面程序示范了这种搽除。

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
class Apple<T extends Number>
{
T size;
public Apple()
{
}
public Apple(T size)
{
this.size = size;
}
public void setSize(T size)
{
this.size = size;
}
public T getSize()
{
return this.size;
}
}
public class ErasureTest
{
public static void main(String[] args)
{
Apple<Integer> a = new Apple<>(6); //①
// a的getSize方法返回Integer对象
Integer as = a.getSize();
// 把a对象赋给Apple变量,丢失尖括号里的类型信息
Apple b = a; //②
// b只知道size的类型是Number
Number size1 = b.getSize();
// 下面代码引起编译错误
Integer size2 = b.getSize(); //③
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ErasureTest2
{
public static void main(String[] args)
{
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(9);
List list = li;
// 下面代码引起“未经检查的转换”的警告,编译、运行时完全正常
List<String> ls = list; // ①
// 但只要访问ls里的元素,如下面代码将引起运行时异常。
System.out.println(ls.get(0));
}
}

泛型与数组

Java不支持创建泛型数组,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素包含类型变量或者类型形参的数组(相当晕。。)。也就是说,可以声明List<String>[]形式的数组,但不能创建ArrayList<String>[10]这样的数组对象。另外特殊的是可以建立无上限的通配符泛型数组,例如new ArrayList<?>[10]。在这种情况下,程序不得不进行强制类型转换。

创建元素类型是类型变量的数组对象也将导致编译错误:

1
2
3
4
<T> T[] makeArray(Collection<T> coll)
{
return new T[coll.size()];
}