在Java SE1.5中。添加了一个新的特性:泛型(日本语中的总称型)。何谓泛型呢?通俗的说。就是泛泛的指定对象所操作的类型。而不像常规方式一样使用某种固定的类型去指定。
泛型的本质就是将所操作的数据类型參数化,也就是说,该数据类型被指定为一个參数。这样的參数类型能够使用在类、接口以及方法定义中。
一、为什么使用泛型呢?
在以往的J2SE中,没有泛型的情况下,一般是使用Object类型来进行多种类型数据的操作。这个时候操作最多的就是针对该Object进行数据的强制转换。而这样的转换是基于开发人员对该数据类型明白的情况下进行的(比方将Object型转换为String型)。倘若类型不一致。编译器在编译过程中不会报错,但在执行时会出错。
使用泛型的优点在于,它在编译的时候进行类型安全检查,而且在执行时全部的转换都是强制的,隐式的,大大提高了代码的重用率。
二、泛型的简单样例:
首先。我们来看看以下两个普通的class定义
public class getString { private String myStr; public String getStr() { return myStr; }
public void setStr(str) { myStr = str; } } public class getDouble { private Double myDou; public Double getDou() { return myDou; }
public void setDou(dou) { myDou = dou; } }
这两个class除了所操作的数据类型不一致,其它机能都是同样的。如今,我们能够使用泛型来将上面两个class合并为一个。从而提高代码利用率。降低代码量。
public class getObj<T> {
private T myObj ;
public T getObj() {
return myObj;
}
public void setObj<T obj> {
myObj = obj;
}
}
那么,使用了泛型后。怎样生成这个class的实例来进行操作呢?请看以下的代码:
getObj<String> strObj = new getObj<String>();
strObj.setObj(“Hello Nissay”);
System.out.println(strObj.getObj());
getObj<Double> douObj = new getObj<Double>();
douObj.setObj(new Double(“116023”));
System.out.println(douObj.getObj());
三、样例分析
如今我们来分析上面那段代码:
1、<T>是泛型的标记,当然能够使用别的名字,比方。使用<T>声明一个泛型的引用,从而能够在class、方法及接口中使用它进行数据定义,參数传递。
2、<T>在声明的时候相当于一个有意义的数据类型,编译过程中不会错误发生。在实例化时,将其用一个详细的数据类型进行替代,从而就能够满足不用需求。
四、泛型的规则和限制
通过上述的样例,我们简单理解了泛型的含义。在使用泛型时,请注意其使用规则和限制,例如以下:
1、泛型的參数类型仅仅能是引用类型,而不能是简单类型。
比方。<int>是不可使用的。
2、能够声明多个泛型參数类型,比方<T, P,Q…>。同一时候还能够嵌套泛型。比如:<List<String>>
3、泛型的參数类型能够使用extends语句,比如<T extends superclass>。
4、泛型的參数类型能够使用super语句。比如< T super childclass>。
5、泛型还能够使用通配符,比如<? extends ArrayList>
五、扩展
1、extends语句
使用extends语句将限制泛型參数的适用范围。比如:
<T extends collection> ,则表示该泛型參数的使用范围是全部实现了collection接口的calss。假设传入一个<String>则程序编译出错。
2、super语句
super语句的作用与extends一样,都是限制泛型參数的适用范围。
差别在于,super是限制泛型參数仅仅能是指定该class的上层父类。
比如<T super List>。表示该泛型參数仅仅能是List和List的上层父类。
3、通配符
使用通配符的目的是为了解决泛型參数被限制死了不能动态依据实例来确定的缺点。
举个样例:public class SampleClass < T extends S> {…}
假如A。B,C。…Z这26个class都实现了S接口。我们使用时须要使用到这26个class类型的泛型參数。
那实例化的时候怎么办呢?依次写下
SampleClass<A> a = new SampleClass();
SampleClass<B> a = new SampleClass();
…
SampleClass<Z> a = new SampleClass();
这显然非常冗余,还不如使用Object而不使用泛型,呵呵,是吧?
别着急,咱们使用通配符,就OK了。
SampleClass<? Extends S> sc = new SampleClass();
仅仅须要声明一个sc变量。非常方便把!
通配符进阶
泛型最复杂的部分是对通配符的理解。我们将讨论三种类型的通配符以及它们的用途。
首先让我们了解一下数组是怎样工作的。能够从一个Integer[]为一个Number[]赋值。
假设尝试把一个Float写到Number[]中,那么能够编译。但在执行时会失败。出现一个ArrayStoreException:
Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5; // compiles, but fails at runtime
假设试图把该例直接转换成泛型。那么会在编译时失败,由于赋值是不被同意的:
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = iList; // not allowed
nList.add(0.5);
假设使用泛型,仅仅要代码在编译时没有出现警告,就不会遇到执行时ClassCastException。
上限通配符
我们想要的是一个确切元素类型未知的列表,这一点与数组是不同的。
List<Number>是一个列表,其元素类型是详细类型Number。
List<? extends Number>是一个确切元素类型未知的列表。它是Number或其子类型。
上限
假设我们更新初始的样例。并赋值给List<? extends Number>,那么如今赋值就会成功了:
List<Integer> iList = new ArrayList<Integer>();
List<? extends Number> nList = iList;
Number n = nList.get(0);
nList.add(0.5); // Not allowed
我们能够从列表中得到Number,由于不管列表的确切元素类型是什么(Float、Integer或Number)。我们都能够把它赋值给Number。
我们仍然不能把浮点类型插入列表中。
这会在编译时失败,由于我们不能证明这是安全的。假设我们想要向列表中加入浮点类型。它将破坏iList的初始类型安全——它仅仅存储Integer。
通配符给了我们比数组很多其它的表达能力。
为什么使用通配符
在以下这个样例中,通配符用于向API的用户隐藏类型信息。
在内部。Set被存储为CustomerImpl。而API的用户仅仅知道他们正在获取一个Set。从中能够读取Customer。
此处通配符是必需的。由于无法从Set<CustomerImpl>向Set<Customer>赋值:
public class CustomerFactory {
private Set<CustomerImpl> _customers;
public Set<? extends Customer> getCustomers() {
return _customers;
}
}
*通配符
最后。List<?
>列表的内容能够是不论什么类型。并且它与List<? extends Object>差点儿同样。能够随时读取Object。可是不能向列表中写入内容。
公共API中的通配符
总之,正如前面所说,通配符在向调用程序隐藏实现细节方面是很重要的。但即使下限通配符看起来是提供仅仅读訪问,因为remove(int position)之类的非泛型方法,它们也并不是如此。假设您想要一个真正不变的集合,能够使用java.util.Collection上的方法。比方unmodifiableList()。
编写API的时候要记得通配符。通常,在传递泛型类型时,应该尝试使用通配符。它使很多其它的调用程序能够訪问API。
通过接收List<? extends Number>而不是List<Number>,以下的方法能够由很多不同类型的列表调用:
void removeNegatives(List<? extends Number> list);
构造泛型类型
如今我们将讨论构造自己的泛型类型。我们将展示一些样例,当中通过使用泛型能够提高类型安全性,我们还将讨论一些实现泛型类型时的常见问题。
集合风格(Collection-like)的函数
第一个泛型类的样例是一个集合风格的样例。
Pair有两个类型參数,并且字段是类型的实例:
public final class Pair<A,B> {
public final A first;
public final B second;
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
}
这使从方法返回两个项而无需为每一个两种类型的组合编写专用的类成为可能。还有一种方法是返回Object[]。而这样是类型不安全或者不整洁的。
在以下的使用方法中,我们从方法返回一个File和一个Boolean。
方法的client能够直接使用字段而无需类型强制转换:
public Pair<File,Boolean> getFileAndWriteStatus(String path){
// create file and status
return new Pair<File,Boolean>(file, status);
}
Pair<File,Boolean> result = getFileAndWriteStatus("...");
File f = result.first;
boolean writeable = result.second;
集合之外
在以下这个样例中,泛型被用于附加的编译时安全性。通过把DBFactory类參数化为所创建的Peer类型。您实际上是在强制Factory子类返回一个Peer的特定子类型:
public abstract class DBFactory<T extends DBPeer> {
protected abstract T createEmptyPeer();
public List<T> get(String constraint) {
List<T> peers = new ArrayList<T>();
// database magic
return peers;
}
}
通过实现DBFactory<Customer>,CustomerFactory必须从createEmptyPeer()返回一个Customer:
public class CustomerFactory extends DBFactory<Customer>{
public Customer createEmptyPeer() {
return new Customer();
}
}
泛型方法
无论想要对參数之间还是參数与返回类型之间的泛型类型施加约束,都能够使用泛型方法:
比如,假设编写的反转函数是在位置上反转。那么可能不须要泛型方法。
然而,假设希望反转返回一个新的List,那么可能会希望新List的元素类型与传入的List的类型同样。在这样的情况下,就须要一个泛型方法:
<T> List<T> reverse(List<T> list)
详细化
当实现一个泛型类时,您可能想要构造一个数组T[]。由于泛型是通过擦除(erasure)实现的,所以这是不同意的。
您能够尝试把Object[]强制转换为T[]。但这是不安全的。
详细化解决方式
依照泛型教程的惯例,解决方式使用的是“类型令牌”,通过向构造函数加入一个Class<T>參数,能够强制client为类的类型參数提供正确的类对象:
public class ArrayExample<T> {
private Class<T> clazz;
public ArrayExample(Class<T> clazz) {
this.clazz = clazz;
}
public T[] getArray(int size) {
return (T[])Array.newInstance(clazz, size);
}
}
为了构造ArrayExample<String>,client必须把String.class传递给构造函数,由于String.class的类型是Class<String>。
拥有类对象使构造一个具有正确元素类型的数组成为可能。