概述
Java中的泛型是一种比较常用且强大的功能,它是一种代码类型检验机制,使用泛型可以让你在代码编译期间检测到更多察觉不到的错误。
优点
- 在编译时进行更强的类型检测,Java编译器将强类型检查应用于通用代码里,如果有违反类型安全,则直接通过编译器暴露出来。
- 消除类型转换
- 使用泛型,可以实现不同类型的集合进行工作,可以实现自定义并且类型安全且易于阅读的泛型算法。
缺点
以下都是因为擦除机制而导致的不足
- 泛型类型变量不能使用基本数据类型
- 不能使用 instanceof 运算符
- 泛型类不能被定义成静态的
- 泛型类里同名函数会造成方法冲突
- 无法创建泛型实例(可以通过反射实现)
- 不存在泛型数组
分类
通用类型
通用类型是通过类型进行参数化的通用类或接口。
通用类的定义格式如下:
class name<T1, T2, ..., Tn> {} |
类型参数命名约定
- E - Element (Java Collections Framework广泛使用)
- K - Key
- V - Value
- N - Number
- T - Type
- S,U,V etc. - 2nd, 3nd, 4th types
调用和实例化泛型类型
泛型类型的调用通常称为参数化类型,要实例化此类,通常是使用new关键字:List<String> strList = new ArrayList<>();
多类型参数public interface Pair<K, V>{
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V>{
private K key;
private V value;
public OrderedPair(K key, V value){
this.key = key;
this.value = value;
}
public K getKey(){return K;}
public V getValue(){return value;}
}
通过以下语句创建OrderedPair的两个实例:Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p1 = new OrderedPair<String, String>("Hello", "World");
又由于菱形原因,Java编译器可以从上下文推断出类型参数,因此可以使用菱形表示法缩短实例化语句,如下:Pair<String, Integer> p1 = new OrderedPair<>("Even", 8);
Pair<String, String> p1 = new OrderedPair<>("Hello", "World");
参数化类型OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
原始类型
原始类型是没有任何类型参数的泛型或接口的名称。例如:public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
但是非泛型类或接口类型不是原始类型。
原始类型显示在旧代码中,因为在JDK 5.0之前,许多API类(例如Collections类)不是通用类型。使用原始类型时,实际上也会获得泛型行为(Box提供对象)。为了向后兼容,允许将参数化类型分配给其他原始类型。例如:Box<String> stringBox = new Box<>();
Box rawBox = stringBox; //这其实是OK的
但是反过来,将原始类型分配给参数化类型,就会提示警告,请注意:Box rawBox = new Box(); //原始类型
Box<Integer> intBox = rawBox; //WARNING: unchecked conversion
同样,如果使用原始类型来调用相应泛型类型中定义的泛型方法,也会提示警告,请注意:Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); //WARNING: unchecked invocation to set(T)
该警告表明原始类型会绕过通用类型检查,从而将不安全代码的捕获推迟到运行时。因此,应避免使用原始类型。
通用方法
指引入自己的类型参数的方法,类似于声明一个泛型方法,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态的泛型方法,也允许使用泛型类构造函数。
通用方法的语法包括:类型参数列表,在尖括号内,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。比如:
public class Util{ |
调用此方法的完成语法如下:Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "orange");
boolean same = Util.<Integer, String>compare(p1, p2);
该类型以明确提供,如上所示,通常可以忽略,编译器可推断出所需类型:Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "orange");
boolean same = Util.compare(p1, p2);
限定类型参数
限制参数化类型中用作类型参数的类型,比如对数字进行操作的方法只希望接受Number或其子类的实例。如下:public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get(){
return t;
}
public <U extends Number> void inspect (U u){
System.out.prinln("T: "+ t.getClass().getName());
System.out.prinln("U: "+ u.getClass().getName());
}
public static void main(String[] args){
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(20));
integerBox.inspect("do sth"); //error: this is an inconformity type
}
}
通用方法和限定类型参数
限定类型参数是实现通用算法的关键,考虑以下方法,该方法计算数组T[]中大于指定元素elem的元素数。如下:public static <T> int countGreaterThan(T[] array, T elem) {
int count = 0;
for (T e: array){
if(e > elem){ //compile error
++count;
}
return count;
}
}
该方法是无法编译的,因为大于运算符(>)仅仅适用于基本类型,例如short、int、double、long、float、byte和char。要解决上述编译错误问题,使用Comparable接口限定的类型参数即可。public interface comparable<T>{
public int comparaTo(T t);
}
上述错误代码可以改为:public static <T extends Comparable<T>> int countGreaterThan(T[] array, T elem) {
int count = 0;
for (T e: array){
if(comparaTo(elem) > 0){
++count;
}
return count;
}
}
泛型、继承和自类型
只要类型兼容,我们便可以将一种类型的对象分配给另一个种类型的对象。泛型也是如此,可以执行通用类型调用。
在使用泛型进行编程时,有一种常见的误解是,比如:
Integer是Number类型的子类,那么它的泛型包裹类Box
通用类和子类
可以通过扩展来实现泛型通用类或接口。一个类或者接口的类型参数与另一个类或接口的类型参数之间的关系由extends 和 implements 来确定。
类型推断
类型推断是Java编译器查看每个方法调用和相应声明以确定使用适用的类型参数的能力。
类型推断和通用方法:通常Java编译器可以推断出通用方法调用的类型参数。
泛型类的类型推断和实例化:可以使用一组空的类型参数(<>)替换调用通用类的构造函数所需的类型参数,记住只要编译器可以从上下文中推断出类型参数都可以使用。
泛型和非泛型类的类型推断和泛型构造函数:
考虑如下示例class MyClass<X>{
<T> MyClass(T t){
// ...
}
}
考虑类MyClass实例:new MyClass<Integer>("");
在此示例中,编译器为通用类MyClass的形式类型参数X推断类型Integer。它为该泛型类的构造函数的形式类型参数T推断为String。
通配符
在通用代码里,通配符(?)表示未知类型。通配符可以在多种情况下使用:
作为参数、字段或局部变量的类型
作为返回类型
请注意:通配符从不用作泛型方法调用、泛型类实例创建、超类型的类型参数。
上限通配符将未知类型限制为特定类型或该类型的子类型,并用extends关键字连接表示
下限通配符则是将未知类型限制为特定类型或该类型的超类型。
上限通配符
声明上限通配符,使用通配符(?),后跟 $extends$ 关键字,然后是其上限。示例如下:public static void process(List<? extends Foo> list){
}
无限通配符
无限通配符类型使用通配符(?)来指定,有两种情况,无界通配符是一种有效的方法:
- 如果你正在编写一个可以使用 $Object$ 类中提供的功能实现的方法;
- 当代码使用通用类中不依赖于类型参数的方法时。例如:List.size 或者 List.clear
下界通配符
下限通配符使用通配符(?)表示,后跟 $super$ 关键字,接着再跟下限。示例如下:public static void addNumbers(List<? super Integer> list){
/* ... */
}
通配符使用准则
首先明确两点概念,有关变量:
“输入”变量: 输入变量将数据提供给代码。例如一个包含两个参数的复制方法:copy(src, dest), src参数提供复制的数据,因此时输入参数;
“输出”变量: 输出变量保存要在其他地方使用的数据。在上面复制方法中,dest参数装载数据,因此它是输出参数。
根据上面的“输入”和“输出”原理,列出以下通配符准则:
通配符准则:
使用上限通配符定义输入变量,使用extends关键字 使用下限通配符定义输出变量,使用super关键字 如果可以使用Object类中定义的方法访问输入变量,则使用无界通配符(?) 如果代码需要同时使用输入和输出变量来访问变量,则不要使用通配符
类型擦除(Type Erasure)
Java泛型原理及泛型擦除机制
Java的泛型是JDK5新引入的特性,为了向下兼容,虚拟机其实是不支持泛型,所以Java实现的是一种伪泛型机制,也就是说Java在编译期擦除了所有的泛型信息,这样Java就不需要产生新的类型到字节码,所有的泛型类型最终都是一种原始类型,在Java运行时根本就不存在泛型信息。
Java编译器具体是如何擦除泛型
1、检查泛型类型,获取目标类型
2、擦除类型变量,并替换为限定类型
如果泛型类型的类型变量没有限定(<T>
),则用Object作为原始类型
如果有限定(<T extends XClass>
),则用XClass作为原始类型
如果有多个限定(<T extends XClass1&XClass2>
),则使用第一个边界XClass1作为原始类3、在必要时插入类型转换以保持类型安全
4、生成桥方法以在扩展时保持多态性
小结
1、Java泛型(generics)是JDK5中引入的一种参数化类型特性
2、使用泛型好处:
代码更健壮
代码更简洁
代码更灵活,可复用性高