Java泛型
感觉java泛型设计得挺复杂,特别是泛型,泛型通配符,泛型上限下限等搅在一起时,实在不太好区分,容易出错。不管怎样,泛型对于设计可复用和扩展的代码支持还是很强大的,这里总结一下自己的理解。《疯狂java讲义》讲解的非常好,想深入理解可以参考该书,另外后面的代码也是参考该书。
泛型就是指在定义类,借口,方法使用类型形参,这个类型形参将在声明变量,创建对象,调用方法动态地指定,可以极强的增强代码的通用性。
理解泛型
定义泛型接口,类
Example
1 | public interface List<E> |
1 | public class Apple<T> |
从泛型类派生子类
为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 | public class R<T> |
另外由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类:
1 | Collection<String> cs = new ArrayList<>(); |
类型通配符
List<Integer>
并不是List<Object>
的子类型!
也就是:
1 | public void test(List<Object> c) { |
并不能输入List<Integer> c
作为参数。
使用类型通配 —(?)
为了表示各种泛型List的父类(并不是真正的父类,只是一种类似的逻辑关系),可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作List<?>
,意思是元素类型未知的List。这个?被称为通配符,它的元素类型可以匹配任何类型。
也就是,在 public void test(List<?> c)
的函数,可以输入List<Integer> c
,List<String> c
,List<Character> c
作为参数。
可以通过这种带通配符的List遍历元素,但是,不能这种带通配符的List将元素加入到其中:
1 | List<Integer> list = new ArrayList<Integer>(); |
原因是系统无法确定c集合中元素的类型,所以不能向其中添加对象。根绝前面List<E>
接口的定义代码可以发现,add()方法需要有类型参数E作为集合元素的类型,传给add的参数必须是E类的对象或者其子类的对象,但在这中情况下,因为并不知道E是什么类型(?),所以程序无法判断尝试放入的对象是否合法,所以无法放入元素。唯一的例外是null。
另一方面程序可以调用get()方法来返回List<?>
集合指定索引处的元素,其返回值一个未知类的元素,但可以肯定的是,它总是一个Object对象,所以一定可以用Object接受,如果知道其类型,也可以使用类型转换转到所需要的对象类型。
设定类型通配符的上限
当直接使用List<?>
这种形式时,即表明这个List集合可以是任何泛型List的父类。但在某些情况,我们不希望这个List是任何List的父类,而是代表某一类泛型List的父类。
1 | // 定义一个抽象类Shape |
List<? extends Shape>
是受限制通配符的例子,此处?同样代表一个未知的类型,但是此处的未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound).
这时drawAll(List<? extends Shape> shapes)
的参数就限定了泛型参数必须为Shape或者其子类,而不能是其他,例如List
类似的,由于系统无法确定这个受限制的通配符的具体类型,所以不能不能通过shapes把对象添加到这个泛型中,即使是Shape的对象或子类对象:
1 | public void addRectangle(List<? extends Shape> shapes) |
设定类型形参的上限
Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
1 | public class Apple<T extends Number> |
泛型方法
在定义类,接口时没有使用类型形参,但定义方法时想自己定义类型形参,这时可以使用泛型方法。
定义泛型方法
考虑实现这样一个方法,该方法将一个Object数组的所有元素添加到一个Collection集合中。
1 | fromArrayToCollection(Object[] a, Collection<Object> c) { |
问题,并不能使用Collection
1 | fromArrayToCollection(Object[] a, Collection<?> c) { |
使用泛型方法解决问题:
1 | 修饰符 <T, S> 返回值类型 方法明<形参列表> {} |
1 | public class GenericMethodTest |
与类,接口使用泛型参数不同的是,方法中的泛型参数,方法中的泛型参数无需显式传入实际类型参数,如上面所示,调用方法时,无需在调用方法前传入String,Object等类型,系统完成推断,但是如果传入的参数的类型不同,系统则无法准确判断该使用哪个类型,出现错误。
如果传入的参数有不同的形参,但是属于同一类型,可以借助带上限的通配符解决。否则需要声明多个泛型参数。
1 | public class RightTest |
泛型方法和类型通配符的区别
大多数时候都可以使用泛型方法来代替类型通配符,例如:
1 | public interface Collection<E> { |
上面两个方法类型形参T只使用了依次,产生的唯一效果是可以在不同的调用点传入不同的实际类型,对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来表示方法的一个或者多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系,另外还支持添加元素。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果有需要,也可以同时使用泛型方法和通配符,如Java的Collections.copy()
:
1 | public class Collections |
Java 7的”菱形”语法与泛型构造器
1 | class Foo |
1 | class MyClass<E> |
设定通配符下限
1 | public class MyUtils |
搽除和转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用泛型声明的类时不指定实际的类型参数。如果没有为这个泛型类指定实际的类型参数,则该类型参数被称作raw type,默认是声明该类型参数时制定的第一个个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量是,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>
类型被转换为List,则该List对集合元素的类型检查变成了类型参数的上限(即Object)。List<Integer>
则变成Number,下面程序示范了这种搽除。
1 | class Apple<T extends Number> |
1 | public class ErasureTest2 |
泛型与数组
Java不支持创建泛型数组,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素包含类型变量或者类型形参的数组(相当晕。。)。也就是说,可以声明List<String>[]
形式的数组,但不能创建ArrayList<String>[10]
这样的数组对象。另外特殊的是可以建立无上限的通配符泛型数组,例如new ArrayList<?>[10]
。在这种情况下,程序不得不进行强制类型转换。
创建元素类型是类型变量的数组对象也将导致编译错误:
1 | <T> T[] makeArray(Collection<T> coll) |