一、泛型详解
1.1 泛型的定义以及存在意义
☞ 泛型的定义:
泛型(Generic type或 generics),即参数化类型,是JavaSE 1.5新特性,对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
☞ 为什么使用泛型?
为了解决数据在装入集合时的类型都被当做Object对待,从而失去本身特有的类型,从集合读取时,还要强制转换,java是所谓的静态类型语言,意思是在运行前,或者在编译期间,就能够确定一个对象的类型,这样做的好处是减少运行时由于类型不对引发的错误。但是强制类型转换是钻了一个空子,在编译期间不会有问题,而在运行期间,就有可能由于错误的强制类型转换,导致错误。这个编译器无法检查到。有了泛型,就可以用不着强制类型转换,在编译期间,编译器就能对类型进行检查,杜绝运行时由于强制类型转换导致的错误。
☞泛型的好处:
Java语言引入泛型是一个较大的功能增强。不仅语言、类型系统和编译器有了较大的变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经称为泛型化的了。
1.类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2.消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3.潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
简单地说,泛型简单易用、消除强制类型转换、保证类型安全。
1.2 泛型类、泛型方法、泛型接口的使用
1.2.1 泛型类的定义与使用
定义格式:修饰符 class 类名<代表泛型的变量> { }
如:class ArrayList < Iteger > { }
///在创建对象时确定泛型
ArrayList <String> list = new ArrayList<String>();
class ArrayList<String>{
public boolean add(String e){ }
public String get(int index){ }
....
}
///自定义泛型
public class MyGenericsClass<MVP>{
//没有MVP类型,在这里代表未知地一种数据类型未来传递什么就是什么类型
}
class ArrayList<E>{
public boolean add(E e){ }
public E get(int index){ }
....
}
1.2.2 泛型方法的定义与使用
定义格式:修饰符 <代表泛型地变量> 返回值类型 方法名(参数) { }
如:public < MVP > void Method( ){…}
class MyGenericsMethod{
public <MVP> void show(MVP mvp){
System.out.println(mvp.getClass());///getClass()方法是获得调用该方法的对象的类
}
}
public class Main{
public static void main(String [] args){
MyGenericsMethod my = new MyGenericsMethod();
my.show("珠穆朗玛峰");
my.show(8844.43);
}
}
控制台结果输出:
1.2.3 泛型接口的定义与使用
定义格式:修饰符 interface 接口名 <代表泛型的变量> {…}
public interface MyGenericsInterrface < E >{…}
public interface MyGenericsInterface <E>{
public abstract void add(E e);
public abstract E getE();
}
///定义接口实现类时确定泛型的类型
public class Main implements MyGenericsInterface <String>{
public void add (String e){
...
}
public String getE(){
return null;
}
}
///始终不确定泛型的类型
public class Main <E> implements MyGenericsInterface <E>{
public void add (E e){
///省略...
}
public E getE(){
return null;
}
}
///创建对象时,确定泛型的类型
public class Main{
public static void main(String [] args){
Main<String> my = new Main<String>();
my.add("华山");
}
}
1.3 泛型通配符类型和通配符的使用
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过**<?>** 表示。但是一旦使用通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
1.3.1 通配符基本使用(不受限泛型)
不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。此时只能接受数据,不能往集合中存储数据。
import java.util.Collection;
import java.util.ArrayList;
public class Main{
public static void main(String [] args){
Collection<Integer> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
}
public static void getElement(Collection <?> collection){
///?代表可以接收任意类型
}
}
1.3.2 通配符高级使用(受限泛型)
设置泛型的时候,是可以任意设置的,只要是类就可以设置。但是在Java的泛型中可以指定一个泛型的上限和下限。
在引用传递中,泛型操作中也可以设置一个泛型对象的范围上限和范围下限。范围上限使用extends关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类,而范围下限使用super关键字进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object类。
☞ 泛型的上限格式:类型名称 <? extends 类> 对象名称 (只能接收该类型及其子类)
☞ 泛型的下限格式:类型名称<? super 类> 对象名称(只能接收该类型及其父类型)
import java.util.Collection;
import java.util.ArrayList;
public class Main{
public static void main(String [] args){
Collection<Integer> list1 = new ArrayList<Integer>();///Integer类是Number类的子类
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<>();///所有类的父类
///泛型上限的实例
getElement1(list1);
///getElement1(list2);报错,不是Number类或其子类
getElement1(list3);
///getElement1(list4);报错,不是Number类或其子类
///泛型下限的实例
///getElement2(list1);报错,不是Number类或其父类
///getElement2(list2);报错,Number类或其父类
getElement2(list3);
getElement2(list4);
}
public static void getElement1(Collection <? extends Number> coll){
///泛型上限:此时的泛型?,必须是Number类型或Number类的子类
}
public static void getElement2(Collection <? super Number> coll){
///泛型下限:此时的泛型?,必须是Number类型或Number类的父类
}
}
1.4 泛型的实现方法:类型擦除
Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候去掉,这个过程称为类型擦除。
import java.util.ArrayList;
public class Main{
public static void main(String [] args) {
ArrayList <String> arrayList1 = new ArrayList<String>();
arrayList1.add("华为");
ArrayList <Integer> arrayList2 = new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass()==arrayList2.getClass());///获取类的信息true
///说明String和Integer类型都擦除掉了,只剩下了原始类型
}
}
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删除类型参数后的泛型类型名。擦除类型变量,并替换为限定类型(无限定变量用Object)。
类型擦除后保留的原始类型:
class Pair<T>{
private T first;
private T second;
public T getFirst(){
return first;
}
public void setFirst(T first){
this.first=first;
}
}
///Pair<T>的原始类型
class Pair{
private Object first;
private Object second;
public Object getFirst(){
return first;
}
public void setFirst(Object first){
this.first=first;
}
...
}
如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换:
public class Pair <T extends Comparable&Serializable> {
}
///原始类型为Comparable
1.5 泛型中的约束及其局限性
这一小节将阐述Java泛型时需要考虑的一些限制。大多数限制都是由类型擦除引起的。
1.5.1 不能用基本类型实例化类型参数
不能用类型参数代替基本类型。因此,没有Pair< double >,只有Pair< Double >
1.5.2 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
查询一个对象是否属于某个泛型类型时:
if(a instanceof Pair<String>)///编译错误
if(a instanceof Pair<T>)///编译错误
Pair<String> p = (Pair<String>)a;///warning,强制类型转换
Pair<String> stringPair = ...;
Pair<E> ePair = ...;
if(stringPair.getClass()==ePair.getClass())///编译通过,getClass()方法总是返回原始类型
1.5.3 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10];///编译错误
擦除之后,table的类型是Pair[ ],需要说明的是,只是不允许创建这些数组,而声明类型Pair< String >[ ]的变量仍是合法的,不过不能用new Pair< String >[10]初始化这个变量。
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList:ArrayList<Pair< String >>。
1.5.4 Varargs警告
向参数个数可变的方法传递一个泛型类型的实例。它的参数个数是可变的。
public static <T> void addAll(Collection<T> coll,T...ts){
for(t: ts) ///枚举
coll.add(t);///ts是一个数组,包含提供的所有实参。
}
///调用
Collection<pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table,pair1,pair2);
这种情况会得到一个警告。可以用以下两种方法抑制这个警告。
☞为包含addAll调用的方法增加注释 @Suppress Warns(“unchecked”)。
☞ 在Java SE 7中,可以用 @SafeVarargs 直接标注addAll方法。
@SafeVarargs
public static <T> void addAll(Collection<T> coll,T...ts){
...
}
1.5.5 不能实例化类型变量
不能使用像 new T(…),new T[…]或T.class这样的表达式中的类型变量。下面的Pair构造器就是非法的 :
public Pair(){
first = new T();
second = new T();
}
类型擦除将T改变成Object,本意肯定不希望调用new Object()。在JavaSE8后,最好的解决办法是然调用者提供一个构造器表达方式:
Pair<String> p = Pair.makePair(String::new);
makePair方法接收一个Supplier,这是一个函数式接口,表示一个无参而且返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier<T> const){
return new Pair<>(const.get(),constr.get());
}
1.5.6 不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。不过原因有所不同,毕竟数组会填充null值,构造时看上去是安全的。不过,数组本身也有类型,用来监控在虚拟机中的数组。这个类型会被擦除。
1.5.7 泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。
public class Pair<T>{
private static T first;///编译错误
public static T getFirst(){///编译错误
if(first==null) return first;
}
}
1.5.8 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型对象。实际上,甚至泛型扩展Throwable都是不合法的。
1.5.9 可以消除对受查异常的检查
Java异常处理的一个基本原则是,必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。通过使用泛型类、擦除和@Suppress Warning注解,就能消除Java类型系统的部分基本限制。
1.5.10 注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。
1.6 泛型类型的继承规则
在使用泛型类时,需要了解一些有关继承和子类型的准则。
泛型类型的继承原则:要么同时擦除,要么子类大于等于父类类型
☞属性类型: 在父类中,随父类而定;在子类中,随子类而定。
☞方法重写: 随父类而定。
1.子类声明为具体类型(具体类型大于泛型):
public abstract class Father<T>{
T name;
public abstract void test(T name);
}
class Child extends Father<Integer>{
public void test(Integer name){
}
}
2. 子类声明为泛型(子类有两个,大于父类一个):
class Child<T,T1> extends Father<T>{
public void test(T name){///重写的方法随父类
}
}
3. 子类为泛型,父类不指定,使用Object替代(子类一个,大于父类没有):
class Child<T> extends Father{
public void test(Object name){
}
}
4.子类与父类同时擦除,用Object替代:
class Child extends Father{
public void test(Object name){
}
}
有待更新…
二、参考资料
1.Java泛型详解
2.全网Java泛型最详解
3.猿问
4.Java中泛型的作用和意义
5.受限泛型
6.《Java核心技术 卷Ⅰ 基础知识》–泛型程序设计
7.类型擦除以及类型擦除带来的问题
8.3.泛型类型的继承规则