java泛型梳理
概述
- 泛型,即参数化类型,是在JDK1.5之后才开始引入的。
- 所谓参数化类型是指所操作的数据类型在定义时被定义为一个参数,然后在使用时传入具体的类型。
- 这种参数类型可以用在类,接口,方法的创建中,分别被称为泛型类、泛型接口和泛型方法。
泛型值存在于java的编译期,编译后生成字节码文件泛型是被擦除的;
Java泛型的底层原理
- 泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想。虽然思想一致,但是他们存在着本质性的不同。
- C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译成对应不同的目标代码,ClassName和ClassName是两种不同的类型,这种泛型被称为真正泛型。这种泛型实现方式,会导致类型膨胀,因为要为不同具体参数生成不同的类。
- Java中ClassName和ClassName虽然在源代码中属于不同的类,但是编译后的字节码中,他们都被替换成原始类型(ClassName),而两者的原始类型的一样的,所以在运行时环境中,ClassName和ClassName就是同一个类。Java中的泛型是一种特殊的语法糖,通过类型擦除实现(后面介绍),这种泛型称为伪泛型。由于Java中有这么一个障眼法,如果没有进行深入研究,就会在产生莫名其妙的问题。值得一提的是,不少大牛对Java的泛型的实现方式很不满意。
- JVM类型擦除
- Java中的泛型是通过类型擦除来实现的。所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。
实际上,Java的泛型除了类型擦除之外,还会自动生成checkcast指令进行强制类型转换。
泛型解决的问题
// 定义一个List,add()可以存放Object及其子类实例
List list = new ArrayList();
list.add(123); // 合法
list.add("123"); // 合法
// 我们在编译时无法知晓list到底存放的什么数据,于是在进行强制转换时发生异常
int i = (Integer) list.get(1); // 抛出ClassCastException异常
- 上面的代码首先实例化一个ArrayList对象,它可以存放所有Object及其子类实例。分别add一个Integer类型对象和String类型对象,我们原本以为list中存放的全部是Integer类型对象,于是在使用get()方法获取对象后进行强制转换。从代码中可以看到,索引值为1的位置放置的String类型,很显然在进行强制转换时会抛出ClassCastException(类型转换异常)。由于这种异常只会发生在运行时,我们在开发时稍有不慎,就会直接掉到坑里,还很难排查出问题。
- 为什么会出现这种问题呢?
- 集合本身无法对其存放的对象类型进行限定,可以涵盖Java中的所有类型。缺口太大,导致各种蛇、蚁、虫、鼠通通都可以进来。
- 由于我们要使用的实际存放类型的方法,所以不可避免地要进行类型转换。小对象转大对象很容易,大对象转小对象则有很大的风险,因为在编译时,我们无从得知对象真正的类型。
- 泛型就是为了解决这类问题而诞生的
泛型类的定义和使用
- 一个泛型类(generic class)就是具有一个或多个类型变量的类。上面的例子中的List就是一个典型的泛型类。泛型类的定义结构类似下面的代码
- 注意,在Java编码规范中,类型变量通常使用较短的大写字母,并且最好与其作用相匹配。譬如:List中的变量使用E,对应单词Element,Map中的K,V变量对应单词Key和Value。当然这些都是约定性质的东西,其实类型变量的命名规则与Java中的普通变量命名规则是一致的。
public class ClassName<T1, T2> { // 可以任意多个类型变量
public void doSomething(T1 t1) {
System.out.println(t1);
}
}
ClassName<String, String> a = new ClassName<String, String>();
a.doSomething("hello world");
泛型接口的定义和使用
- 接口本质上来说就是一种特殊的类,所以泛型接口的定义和使用与泛型类相差无几。
public interface InterfaceName<T1, T2> { // 可以任意多个类型变量
public void doSomething(T1 t1);
}
public class ConcreteName<T2> implements InterfaceName<String, T2> {
public void doSomething(String t1) {
System.out.println(t1);
}
}
InterfaceName<String, String> a = new ConcreteName<String>();
a.doSomething("hello world");
泛型方法的定义和使用
- 泛型类和泛型接口的类型变量都是定义在类型级别,其作用域可覆盖成员变量和成员方法。泛型方法的类型参数定义在方法签名中.
/**
* 创建一个指定类型的无参构造的对象实例。
* @param <T> 待创建对象的类型。
* @param t 指定类型所对应的Class对象。
* @return 返回创建的对象。
* @throws Exception
*/
public <T> T getObject(Class<T> t) throws Exception {
return t.newInstance();
}
String newStr = generic.getObject(String.class);
泛型变量的类型限定
public <T> T getMax(T t1, T t2) {
if (t1.compareTo(t2) > 1) { // 编译错误
return t1;
} else {
return t2;
}
}
- 在上面的代码无法通过编译,由于我们都没有对类型变量对任何的约束限制,那么实际上这个类型可以是任意Object及其子类。那么在使用这个类型变量时,只能调用Object类中的方法。而Object本身就是Java中对顶层的类,没有实现Comparable接口,所以无法调用compareTo方法来比较对象的大小。这时候可以通过限定类型变量来达到目的。
public <T extends Comparable<T>> T getMax(T t1, T t2) {
if (t1.compareTo(t2) > 1) {
return t1;
} else {
return t2;
}
}
- 注意到上面的代码使用extends关键字限定了类型变量T必须继承自Comparable,于是变量t1和t2就可以使用Comparable接口中的compareTo方法了。
- 不管是泛型类、泛型接口还是泛型方法,都可以进行类型限定。类型限定的特点如下:
- 不管该限定是类还是接口,统一都使用extends关键字。
- 使用&符号进行多个限定,那么传入的具体类型必须同时是这些类型的子类。
- 由于Java中不支持多继承,所以不存在一个同时继承两个以上的类的类。所以,在泛型的限定中,&连接的类型最多只能有一个类,而接口数量则没有限制。同时,如果同时限定类和接口,则必须将类写在最前面。
public <T extends Serializable&Cloneable&Comparable> T getMax(T t1, T t2) {
...
}
public <T extends Object&Serializable&Cloneable&Comparable> T getMax(T t1, T t2) { // 合法
...
}
public <T extends Object&ArrayList> T getMax(T t1, T t2) { // 同时限定两个类,不合法
...
}
public <T extends Serializable&Cloneable&Comparable&Object> T getMax(T t1, T t2) { // 将类写在最后面,不合法
...
}
泛型通配符
- 我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过
Generic<Ingeter>
与Generic<Number>
实际上是相同的一种基本类型。那么问题来了,在使用Generic<Number>
作为形参的方法中,能否使用Generic<Ingeter>
的实例传入呢?在逻辑上类似于Generic<Number>
和Generic<Ingeter>
是否可以看成具有父子关系的泛型类型呢?
//为了弄清楚这个问题,我们使用Generic<T>这个泛型类继续看下面的例子:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
- 通过提示信息我们可以看到Generic不能被看作为`Generic的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic和Generic父类的引用类型。由此类型通配符应运而生。
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
- 类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
静态方法与泛型
- 静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
泛型的好处
- 1,类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2,消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3,潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。 - Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
参考
- https://blog.csdn.net/xialei199023/article/details/63251311
- https://www.cnblogs.com/coprince/p/8603492.html