
Java核心知识1:泛型机制详解
1 理解泛型的本质
JDK 1.5开始引入Java泛型(generics)这个特性,该特性提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型)。
2 泛型的作用
泛型有四个作用:类型安全、自动转换、性能提升、可复用性。即在编译的时候检查类型安全,将所有的强制转换都自动和隐式进行,同时提高代码的可复用性。
2.1 泛型如何保证类型安全
在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
比如:没有泛型的情况下使用集合:
有泛型的情况下使用集合:
有了泛型后,会对类型进行验证,所以集合arr在编译的时候add(1)、add('a') 都会编译不通过。
这个过程相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。
2.2 类型自动转换,消除强转
泛型的另一个好处是消除源代码中的强制类型转换,这样代码可读性更强,且减少了转换类型出错的可能性。
以下面的代码为例子,以下代码段需要强制转换,否则编译会通不过:
当重写为使用泛型时,代码不需要强制转换:
2.3 避免装箱、拆箱,提高性能
在非泛型编程中,将简单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。
使用泛型后
2.4 提升程序可复用性
引入泛型的另一个意义在于:适用于多种数据类型执行相同的代码(代码复用)
我们通过下面的例子来说明,代码如下:
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
3 泛型的使用
3.1 泛型类
泛型类是指把泛型定义在类上,具体的定义格式如下:
注意事项:泛型类型必须是引用类型,非基本数据类型
定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔:
当然,这个后面的参数类型也是有规范的,不能像上面一样随意,通常类型参数我们都使用大写的单个字母表,可以任意指定,但是还是建议使用有字面含义的,让人通俗易懂,下面的字母可以参考使用:
T:任意类型 type
E:集合中元素的类型 element
K:key-value形式 key
V:key-value形式 value
N:Number(数值类型)
?:表示不确定的java类型
这边举个例子,假设我们写一个通用的返回对象,对象中的某个字段的类型不定:
做成泛型类,他的通用性就很强了,这时候他返回的情况可能如下:
先定义一个用户信息对象
尝试返回不同的数据类型:
输出结果如下:
3.2 泛型接口
泛型方法概述:把泛型定义在接口上,他的格式如下
注意点1:方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
注意点2:使用泛型的时候,前后定义的泛型类型必须保持一致,否则会出现编译异常:
3.3 泛型方法
泛型方法,是在调用方法的时候指明泛型的具体类型 。定义格式如下:
举例说明,下面是一个典型的泛型方法,根据传入的对象,打印它的值和类型:
输出结果如下:
从上面可以看出,泛型方法随着我们的传入参数类型不同,执行的效果不同,拿到的结果也不一样。泛型方法能使方法独立于类而产生变化。
3.4 泛型通配符(上下界)
Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:
无边界的通配符,使用精确的参数类型
关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
结构如下:
上界示例:
下界示例:
4 泛型实现原理
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),
将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。
4.1 泛型的类型擦除原则
消除类型参数声明,即删除<>及其包围的部分。
根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
为了保证类型安全,必要时插入强制类型转换代码。
自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
4.2 擦除的方式
擦除类定义中的类型参数 - 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object。
擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
