为什么使用泛型
泛型能使类型转换的错误在编译时被发现,从而增加程序的健壮性。看一个例子
public class Box{
private Object object;
public void set(Object object) {
this.object= object;
}
public Object get() {
return object;
}
}
其中set方法可以接受任何java对象作为参数,加入在某个地方使用该类,预期object属性是Integer类型,但是实际set的是String类型,就会抛出一个运行时错误,这个错误在编译阶段无法检测。如:
Box box = new Box();
box.set("abc");
Integer a = (Integer)box.get();//编译不报错,运行时报ClassCastException
使用泛型改造以上代码
public class Box<T>{
private T t;
public void set(T t) {
this.t= t;
}
public T get() {
return t;
}
}
当我们使用这个Box类时会指定T的类型,该类型参数可以是类,接口,数组等,但是不能是基本数据类型。比如:
Box<Integer> box = new Box<Integer>; //指定了类型类型为Integer
//box.set("abc"); 该句在编译时就会报错
box.set(new Integer(2));
Integer a = box.get(); //不用转换类型
可以看到,泛型还免除了我们手动进行类型转换。
在引入泛型机制前,要在方法中支持多个数据类型,需要对方法进行重载,在引入泛型后可以更简洁的解决问题,更进一步可以定义多个参数以及返回值之间的关系。例如:
public void write(Integer i, Integer[] ia);
public void write(Double d, Double[] da);
public void write(Long l, Long[] la);
的泛型版本为
public <T> void write(T t,T[] ta);
总体来说,泛型机制能够在定义类、接口、方法时把“类型”作为一个参数,有点类似方法中的形参,如此我们就能通过不同的输入参数来实现方法的重用。不同于形参的是,泛型“参数”的输入是类型。
命名规则
类型参数的命名有一套默认规则,为了提高代码的维护性和可读性,强烈建议遵循这些规则。JDK中随处可见这些命名规则的应用。
E-Element
K-Key
V-Value
N-Number
T-Type
S,U,V etc. - 第二个、第三个、第四个参数
泛型原理简述
java中的泛型是个语法糖,作用发生在编译阶段。在编译过程中,正确检验泛型结果后,会将其擦除,并在对象进入和离开边界处添加类型检查和类型转换的方法。因此,成功编译后的class文件是不包含任何泛型信息的。
可以用一个反射的例子来证明
ArrayList<Integer> list = new ArrayList<Integer>();
Class c = list.getClass();
try {
Method method = c.getMethod("add", Object.class);
method.invoke(list, "abc");
System.out.println(list.get(0));
} catch (Exception e){
}
能正确打印出“abc”
泛型类与泛型方法的使用
泛型类的基本写法
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
泛型类,是在实例化类的时候指明泛型的具体类型;而泛型方法是在调用方法的使用才指明泛型具体类型
泛型方法基本写法
/**
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
*
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
泛型的上下限和通配符
Integer是Number的子类,那么在Box<Number>作为形参的方法中能不能使用Box<Integer>呢?在上面的Box类中添加方法
public static void showValue(Box<Number> box){
System.out.println(box.getObject());
}
在该类中main方法编写代码
public static void main(String[] args) {
Box<Integer> box = new Box<Integer>();
box.setObject(3);
showValue(box);//出错
}
看来不行?但是又想只要是泛型具体类型只要是Number子类都能用这个方法要怎么办呢?总不能每个子类写一遍方法把。
这里可以用通配符“?”
将上面静态方法改成
public static void showValue(Box<? extends Number> box){
System.out.println(box.getObject());
}
问题解决,<? extends Number> 代表可以接受Number以及他的子类作为类型参数,这种声明方式称为上限通配符。<? super Number> 代表可以接受Number以及他的父类作为类型参数。称为下限通配符。
?单独使用时称作无限定通配符。通常一下两种情况会使用无限定通配符:
1.编写一个方法,可以使用Object类中提供的功能来实现;
2.代码实现的功能与类型参数无关,比如List.clear()、List.size()等方法,还有经常使用的Class<?>方法,他们实现的功能都与类型参数无关。
通配符可以看做类型参数的实参
泛型使用的几个注意点
(1)不能用基本类型实例化类型参数
例如
- class Pair<K,V> {
- private K key;
- private V value;
- public Pair(K key, V value) {
- this.key = key;
- this.value = value;
- }
- // ...
- }
当创建一个Pair类时,不能用基本类型来替代K,V两个类型参数。
- Pair<int,char> p = new Pair<>(8, 'a'); // 编译错误
- Pair<Integer,Character> p = new Pair<>(8, 'a'); //正确写法
(2)不可实例化类型参数
例如:
- public static <E> void append(List<E> list) {
- E elem = new E(); // 编译错误
- list.add(elem);
- }
但是,我们可以通过反射实例化带有类型参数的对象:
- public static <E> void append(List<E> list, Class<E> cls) throws Exception{
- E elem = cls.newInstance(); // 正确
- list.add(elem);
- }
- List<String> ls = new ArrayList<>();
- append(ls,String.class); //传入类型参数的Class对象
(3)不能在静态字段上使用泛型
通过一个反例来说明:
- public class MobileDevice <T> {
- private static T os; //假如我们定义了一个带泛型的静态字段
- // ...
- }
- MobileDevice<Smartphone> phone = new MobileDevice<>();
- MobileDevice<Pager> pager = new MobileDevice<>();
- MobileDevice<TabletPC> pc = new MobileDevice<>();
因为静态变量是类变量,被所有实例共享,此时,静态变量os的真实类型是什么呢?显然不能同时是Smartphone、Pager、TabletPC。
这就是为什么不能在静态字段上使用泛型的原因。
(4)不能对带有参数化类型的类使用cast或instanceof方法
- public static<E> void rtti(List<E> list) {
- if (list instanceof ArrayList<Integer>){ // 编译错误
- // ...
- }
- }
传给该方法的参数化类型集合为:
S = { ArrayList<Integer>,ArrayList<String> LinkedList<Character>, ... }
运行环境并不会跟踪类型参数,所以分辨不出ArrayList<Integer>与ArrayList<String>,我们能做的至多是使用无限定通配符来验证list是否为ArrayList:
- public static void rtti(List<?> list) {
- if (list instanceof ArrayList<?>){ // 正确
- // ...
- }
- }
同样,不能将参数转换成一个带参数化类型的对象,除非它的参数化类型为无限定通配符(<?>):
- List<Integer> li = new ArrayList<>();
- List<Number> ln = (List<Number>) li; // 编译错误
当然,如果编译器知道参数化类型肯定有效,是允许这种转换的:
- List<String> l1 = ...;
- ArrayList<String> l2 = (ArrayList<String>)l1; // 允许转变,类型参数没变化
(5)不能创建带有参数化类型的数组
例如:
- List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误
下面通过两段代码来解释为什么不行。先来看一个正常的操作:
- Object [] strings= new String[2];
- string s[0] ="hi"; // 插入正常
- string s[1] =100; //报错,因为100不是String类型
同样的操作,如果使用的是泛型数组,就会出问题:
- Object[] stringLists = new List<String>[]; // 该句代码实际上会报错,但是我们先假定它可以执行
- string Lists[0] =new ArrayList<String>(); // 插入正常
- string Lists[1] =new ArrayList<Integer>(); // 该句代码应该报ArrayStoreException的异常,但是运行环境探测不到
(6)不能创建、捕获泛型异常
泛型类不能直接或间接继承Throwable类
- class MathException<T> extends Exception { /* ... */ } //编译错误
- class QueueFullException<T> extends Throwable { /* ... */} // 编译错误
方法不能捕获泛型异常:
- public static<T extends Exception, J> void execute(List<J> jobs) {
- try {
- for (J job : jobs)
- // ...
- } catch (T e) { // 编译错误
- // ...
- }
- }
但是,我们可以在throw子句中使用类型参数:
- class Parser<T extends Exception> {
- public void parse(File file) throws T{ // 正确
- // ...
- }
- }
(7)不能重载经过类型擦除后形参转化为相同原始类型的方法
先来看一段代码:
- List<String> l1 = new ArrayList<String>();
- List<Integer> l2 = new ArrayList<Integer>();
- System.out.println(l1.getClass()== l2.getClass());
打印结果可能与我们猜测的不一样,打印出的是true,而非false,因为一个泛型类的所有实例在运行时具有相同的运行时类(class),而不管他们的实际类型参数。
事实上,泛型之所以叫泛型,就是因为它对所有其可能的类型参数,有同样的行为;同样的类可以被当作许多不同的类型。
认识到了这一点,再来看下面的例子:
- public class Example {
- public void print(Set<String> strSet){ } //编译错误
- public void print(Set<Integer> intSet) { } //编译错误
- }
因为Set<String>与Set<Integer>本质上属于同一个运行时类,在经过类型擦出以后,上面的两个方法会共享一个方法签名,相当于一个方法,所以重载出错。
2.