泛型的定义
什么是泛型呢?从字面意思理解,泛型就是广泛的类型,不确定的类型。这种不确定的数据类型需要在使用这个类的时候才能够确定出来。
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
泛型机制的作用
泛型的作用是一种安全机制,将运行时期会发生的某些异常提前到编译时期了。它是一种书写规范,和接口的作用有着一定的类似,都是在制定规则。同时也可以提高代码的复用性。
如何理解泛型是一种安全机制呢?举例来说:
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add("大头");
collection.add(200);
for (Object col : collection) {
int length = ((String) col).length();
System.out.println(length);
}
}
上面的这个例子在编译的时候是没有问题的,因为集合本来就可以存放多种数据类型 ;但是在运行的时候就会报错,这是因为集合中有个integer类型的数据,无法强转为String,所以报错了。
泛型类
泛型类就是有一个或多个类型变量的类。泛型类可以有多个类型变量。类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。常见的做法是类型变量使用大写字母,而且很简短。Java库使用变量E表示集合的元素类型,K和V分别表示表的键和值类型。T(必要时还可以用相邻的字母U和S)表示“任意类型”。
可以用具体的类型替换类型变量来实例化泛型类型。
泛型方法
下面就是一个泛型方法的定义格式:
public static <T> T getMiddle(T arg){
...
}
注意,类型变量放在修饰符的后面,并在返回类型的前面。泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle =ArrayAlg.<String>getMiddle("Jhon","Q","Public");
在这种情况下(实际也是大多数情况下),方法调用中可以省略类型参数。编译器有足够的信息推断出我们想要的方法。几乎在所有情况下,泛型方法的类型推导都能正常工作。但在某些特殊的情况下,还是需要我们显示的写出具体的类型。
但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,有如下几种情况:
class GenerateTest<T>{
//此泛型方法比较特殊,在方法中并没有声明泛型类型,但这仍然是一个泛型方法,只是有一个隐藏的限制:在使用时,方法中的T和泛型类中声明的T必须一致
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
泛型参数
泛型参数比较简单,这里不再详解。
泛型接口
泛型接口与泛型类的定义及使用基本相同。
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
当实现泛型接口的类,未传入泛型实参时:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
类型变量的限定
这里涉及到泛型类型的上边界和下边界:
泛型类型上下边界的限定都是通过extends 关键字实现的,具体如下:
- 限制上边界。,即传入的类型实参必须是指定类型的子类型。
- 限制下边界。,即传入的类型实参必须是ManagerEntity 的父类型。
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
通配符
通配符“?”同样可以对类型进行限定。可以分为子类型限定、超类型限定和无限定。通配符不是类型变量,因此不能在代码中使用"?"作为一种类型。
通配符限定
通配符上界限限定:
? extends T
通配符下界限限定:
? super T
首先需要明确的是泛型上限和下限是定义在方法的参数或者用来声明对象时定义的,如果参数包含一个带有泛型的类,可以用上限或者下限给这个类做相应的限制,如果直接定义在类上会编译错误。
静态方法与泛型
需要注意,在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。例如:
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
泛型代码和虚拟机
虚拟机没有泛型类型对象----所有对象都属于普通类。这就意味着编译器在编译java文件时需要对泛型类型进行替换(也即是类型擦除)。下面我们详细讲解什么是类型擦除,以及类型擦除对Java程序员有什么影响?
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其限定类型(原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object)。举例说明:
这是一个泛型类:
public class Pair<T>{
private T first;
private T second;
public Pair(T first,T second){
this.first=first;
this.second=second;
}
//省略get和set方法
}
因为T是一个无限定的类型,所以在类型擦除后,就变成如下:
public class Pair{
private Object first;
private Object second;
public Pair(Object first,Object second){
this.first=first;
this.second=second;
}
//省略get和set方法
}
这样一来,在进行类型擦除后,就变成了一个普通类,就好像Java语言引入泛型之前实现的类一样。
在程序中,可包含不同类型的Pair,例如,Pair或 Pair。不过擦除类型后,他们都会变成原始的Pair类型。
泛型类型可以有多个限定类型,不同限定类型之间用&连接,例如: T extends Comparable&Serializable。这里需要注意的是当有多个限定类型时,需要将标签接口放在限定列表的末尾,以提高效率。
对于Java泛型的转换,需要注意几点:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型泛型都会替换为它们的限定类型
- 会合成桥方法来保持多态
- 编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
- 当访问一个泛型字段时也要插入强制类型转换。
泛型数组
- 不能实例化参数化类型的数组
查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的 。例如:
//这些写法是不被允许的
List<String>[] ls = new ArrayList<String>[10];
List<String>[] lsas = new List<String>[10];
//这些写法是可以
List<?>[] lsa = new List<?>[10];
List<?>[] ls = new ArrayList<?>[10];
List<String>[] ls = new ArrayList[10];
为什么不允许创建参数化类型的数组呢?
擦除之后,变量的类型就变成限制类型。可以把它转换为Object[],这样数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出异常。
- 不能实例化泛型数组 ·
public static <T extends Comparable> T[] minmax(T arg){
T[] mm=new T[2];//这个是错误的写法
}
有关泛型需要注意的问题
- 不能在类型new T(…)的表达式中使用类型变量。因为在进行类型擦除后,T变成了Object,你肯定不希望调用new Object()。
- 表达式T.class是不合法的。Class类本身是泛型的。
- 不能在静态字段或方法中引用类的泛型变量。
- 既不能抛出也不能捕获泛型类对象。
- 就像不能实例化泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组可以填充null值,看上去好像可以安全的构造。不过数组本身也带有类型,用来监控虚拟机中的数组存储。这个类型会被擦除。如果数组仅仅作为一个类的私有实例字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换。