泛型程序设计
1、为什么使用泛型程序设计
泛型程序设计(Generic programming)意味着编写的代码
可以被很多不同类型的对象所重用
。例如,我们并不希望
为聚集String和File对象分别设计不同的类
;实际上,也不需要这样做,因为一个ArrayList类可以聚集任何类型的对象。这是一个泛型城西设计的实例
1.1、类型参数的好处
- 代码具有易读性
- 代码具有较高的安全性
下面看看演进的过程
在Java中增加泛型类之前,泛型程序设计使用继承实现的,ArrayList只维护了一个Object引用的数组:
public class ArrayList{
private Object[] elementData;
....
private Object get(int i){...}
private void add(Object o){....}
}
这种方法有两个问题:
-
当获取一个值时必须进行强制类型转换
ArrayList files = new ArrayList(); ..... String fileName = ( String)files.get(0);//强制类型转换
-
没有错误检查,可以像数组列表中添加任何类的对象
泛型提供了一个更好的解决方案类型参数(type parameters)。
ArrayList类有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<>();//使得程序具有更好的刻度性和安全性
2、定义简单泛型类
泛型类:具有一个或多个类型变量的类。
- 泛型类引入类型变量,用尖括号(<>)括起来,放在类名的后面。
- 泛型类型可以有多个类型变量,之间用逗号隔开
- 类定义中的类型变量指定方法的
返回类型
以及域和局部变量的类型
;private T first
类型变量使用大写形式,
在JAVa库中使用E
表示集合的元素类型,k
和V
分别表示表的关键字与值的类型;T(U或S)
表示任意类型
public class Pair<T> {
/**
* 类型变量使用大写形式,且比较短,这是很常见的。
* 在Java库中,使用变量E表示集合的元素类型,
* K和V分别表示关键字与之的类型。
* T(需要时还可以用邻近的字母U和S)表示"任意类型"。
*/
private T first; //use the type variable ;使用类型变量
private T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
//使用类型变量返回值
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
用具体的类型
替换类型变量就可以实例化泛型类型
Pair<String>
//可以将结果想象成
Pair<String>()
Pair<String>(String,String)
//和方法
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
换句话说,泛型类
可看做是普通类的工厂
3、简单泛型方法
语法:
修饰符 <T> 返回类型(参数)
- 类型变量放在修饰符的会面,返回类型的前面
- 泛型方法可以定义在普通类中,也可以定义在泛型类中
/*
* @param a
* @param <T>
* @return
* 这个方法是在普通类中定义的,而不是嘴啊反类型中定义的,
* 然而这是一个泛型方法,可以从尖括号和类型变量看出这一点。
* 注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面
*/
public static <T> T getMiddle(T...a){
return a[a.length/2];
}
3、类型变量的限定
有时需要对泛型变量进行限定,才能进行某些操作
语法:
<T extends BoundingType>
- 表示T应该是绑定类型的
子类型
T和绑定类型可以是类,也可以是接口。;- 选择关键字extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如sub)
- 一个
类型变量
或通配符
可以有多个限定;例如T extends Comparable & Serializable
- 可以拥有多个接口超类型,但限定中至多有一个类;如果用一个类作为限定,它必须是限定列表中的第一个
/* 类型变量的限定 通过extends实现对类型变量进行限定
* @param a
* @param <T>
* @return
*现在,泛型的min方法只能被实现了Comparable接口的类(如String,LocalDate等)的数组调用。
* 由于Rectangle类没有实现Comparable接口,所以调用min将产生一个差一位
*/
public static <T extends Comparable> T min(T[] a){
if(a==null||a.length==0){
T smallest = a[0];
for(int i =1;i<a.length;i++){
if(smallest.compareTo(a[i])>0) smallest=a[i];
}
return smallest;
}
return null;
}
4、泛型代码与虚拟机(虚拟机如何处理泛型)
虚拟机没有泛型类型对象----所有对象都属于普通类
4.1、类型擦除
无论何时定义了一个泛型类型,都自动的提供了一个相应的原始类型(raw type).
原始类型化的名字就是删除类型参数后的泛型类型名
擦除类型变量,并替换为
限定类型(无限定类型的变量用Object)
例如,Pair<T>
的原始类型如下所示:
因为T
是一个无限定的变量,所以直接用Object
替换
结果就是一个普通的类,就好像泛型引入Java语言之前已经实现的那样
在程序中可以包含不同类型的pair,例如,Pair<String>
或Pair<LocalDate>
,而擦除类型后就变成原始的Pair
类型
public class Pair {
/**
* 类型变量使用大写形式,且比较短,这是很常见的。
* 在Java库中,使用变量E表示集合的元素类型,
* K和V分别表示关键字与之的类型。
* T(需要时还可以用邻近的字母U和S)表示"任意类型"。
*/
private Object first; //use the type variable ;使用类型变量
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换
下面是类型变量有限定的情形:
为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾
4.2、翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
Pair<Employee> buddies =.....;
Employee buddy = buddies.getFirst();
//擦除之后的原始
public Object getFirst() {
return first;
}
擦除getFirst的返回类型将返回Object类型,编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法 Pair.getFirst的调用
- 对返回Object类型强制转换为Employee类型
4.3、翻译泛型方法
类型擦除也会出现在泛型方法中
例如:
public static <T extends Comparable> T min(T[] a) //是一个完整的方法族
类型擦除后
public static Comparable min(Comparable[] a) //类型参数T已经被擦除了,只留下了限定类型Comparable
Java泛型转换的事实:
- 虚拟机没有泛型,只有普通的类和方法
- 所有类型参数都用他们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插入强制类型转换‘
5、约束和局限性
下面介绍Java泛型时需要考虑的一些限制,
大多数限制都是由类型擦除引起的
5.1、不能用基本类型实例化类型参数
不能用
类型参数
代替基本类型
。===>基本数据类型不能作为类型参数
没有Pair<double>
,只有Pair<Double>
原因是类型参数
;擦除之后,Pair
类含有Object
类型的域,而Object类型不能存储double
值
5.2、运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。
因此,
所有的类型查询只产生原始类型
- 试图查询一个对象是否属于某个泛型类型时,倘若使用instanceof会得到一个编译期错误i
- 若使用强制类型转换会得到一个警告
getClass
方法总是返回原始类型
例如:
if(a instanceOf Pair<String>)
—> 实际上仅仅测试a是否是任意类型的一个Pair
if(a instanceOf Pair<T>)
----> 实际上仅仅测试a是否是任意类型的一个Pair
Pair<String> p = (Pair<String>)a;
----->Warning----->can only test that is a pair
Pair<String> stringPair = '';
Pair<Employee> employer='...';
if(StringPair.getClass==empliyee.getClass)//equal;这是因为两次调用getClass都将返回Pair.class
5.3、不能创建泛型类型的数组
不能实例化参数化类型的数组,例如
Pair<String> table = new Pair<String>[10];//error
这有什么问题?擦除之后,table类型是Pair[];可以把它转换为Object[]:
Object[] objarray = table;
数组会记住他的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException异常:
objarray[0]="Hello"//errot-Component is POair
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>
5.4、Varags警告
考虑下面代码
public static <T> void addAll(Collection<T> coll,T...ts){
for(t:ts) coll.add(t);
}
现在考虑调用下面:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ....;
Pair<String> pair2 = ....;
addAll(table,pair1,pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair<String>
数组,这就违反了 5.3
的规则。不过,对于这种情况,规则有所放松,会得到一个警告,而不是错误
可以采用两种方法来抑制警告。
- 为包含
addAll
调用的方法增加注解SuppressWarnings("unchecked")
- 用
@SafeVarargs
直接标注addAll
方法
5.5、不能实例化类型变量
不能使用像new T(...)
,new T[,,,]
或T.class
这样的表达式中的类型变量。
例如,下面的Pair<T>
构造器就是非法的:
public Pair(){
first = new T();
second = new T();//error
//类型擦除将T变成Object;而且本例不希望调用new Object()
}
在JavaSE 8之后,最好的解决办法是让调用者提供一个构造器表达式:
public<String> p = Pair.makePair(String::new);
makePair
方法接收一个Supplier<T>,
这是一个函数式接口,表示一个无参数而且返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(),constr.get());
}
5.6、不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。
不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。
不过数组本身也有类型,用来存储在虚拟机中的数组。这个类型会被擦除
public static<T extends Comparable> T[] minmax(T[] a){ T[] mm = new T[2];}//ERROT
类型擦除会让这个方法永远构造Comparable[2]数组
如果数组仅仅作为一个类的私有实例域,就可以将这个数组声明为Object[],
并且在获取元素时进行类型转换
例如,ArrayList就可以这样实现:
public class ArrayList<E>{
private Object[] elements;
@SuppressWarnings("unchecked")
public E get(int n){
return (E)elements[n];
}
public void set(int n,E e){
elements[n]=e;
}
}
6、通配符类型
固定的泛型类型系统使用起来并没有那么令人愉快,类型系统的研究人员知道这一点已经有一段时间了
Java的设计者发明了一种巧妙的(仍然是安全的)“解决方案”:通配符类型
6.1、通配符概念
通配符类型中,允许类型参数变化
例如:
Pair <? extends Employee>
表示任何泛型Pair
类型,它的类型参数是Employee
的子类,如Pair<Manager>
,但不是Pair<String>
类型Pair<Manager>
是Pair<? extends Employee>
的子类型
6.2、通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定
? super Manager
`这个通配符限制为Manager的所有超类型
上一篇: Javascript面向对象编程--闭包
推荐阅读