在Java语言处于尚未出现泛型的版本时,只能经过Object是全部类型的父类和类型强制转换两个特色的配合来实现类型泛化。例如在哈希表的存取中,JDK1.5以前使用HashMap的get()方法,返回值就是一个Object对象,因为Java语言里面全部的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。可是也由于有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object究竟是个什么类型的对象。在编译期间,编译器没法检查这个Object的强制转型是否成功,若是仅仅依赖程序员去保障这项操做的正确性,许多ClassCastException的风险就会被转嫁到程序运行期之中。java
泛型是JDK1.5的一项新特性,它的本质是将类型参数化,简单的说就是将所操做的数据类型指定为一个参数,在用到的时候经过传参来指定具体的类型。在Java中,这种参数类型能够用在类、接口和方法的建立中,分别称为泛型类、泛型接口和泛型方法。程序员
一个泛型类的例子以下:segmentfault
//将要操做的数据类型指定为参数T public class Box<T> { private T t; public void add(T t) { this.t = t; } public T get() { return this.t; } }
//使用的时候指定具体的类型为Integer //那么Box类里面的全部T都至关于Integer了 Box<Integer> integerBox = new Box<Integer>();
泛型接口和泛型方法的定义和使用示例以下:数组
//泛型接口 interface Show<T,U> { void show(T t,U u); } class ShowTest implements Show<String,Date> { @Override public void show(String str,Date date) { System.out.println(str); System.out.println(date); } } public static void main(String[] args) { ShowTest showTest = new ShowTest(); showTest.show("Hello",new Date()); }
//泛型方法 public <T, U> T get(T t, U u) { if (u != null) return t; else return null; } String str = get("Hello", "World");
从上面的例子能够看出,用尖括号<>来声明泛型变量,能够有多个类型变量,例如Show<T, U>,可是类型变量名不能重复,例如Show<T, T>是错误的。另外,类型变量名通常使用大写形式,且比较短(不强制,只是一种命名规约),下面是一些经常使用的类型变量:框架
类型限定就是使用extends关键字对类型变量加以约束。好比限定泛型参数只接受Number类或者子类Integer、Float等,能够这样限定
//定义一个水果类 //里面有一个示例方法getWeight()能够获取水果重量 public class Fruit { public int getWeight() { return 10; //这里假设全部水果重量都是10 } } public class Apple extends Fruit {} -------------------------------------------------------------------------------------------- //定义泛型类Box,并限定类型参数为Fruit public class Box<T extends Fruit> {} -------------------------------------------------------------------------------------------- //因为Box限定了类型参数,实际类型参数只能是Fruit或者Fruit的子类 Box<Fruit> integerBox = new Box<Fruit>();//编译经过 Box<Apple> integerBox = new Box<Apple>();//编译经过 Box<Integer> integerBox = new Box<Integer>();//编译器报错
上面代码用虚线分为三个部分,第一个部分是举例用的,定义一个水果类Fruit和它的子类Apple;第二部分定义一个泛型类Box,而且限定了泛型参数为Fruit,限定以后,实际类型只能是Fruit或者Fruit的子类,因此第三部分,实际泛型参数是Integer就会报错。学习
经过限定,箱子Box就只能装水果了,这是有好处的,举个例子,好比Box里面有一个getBigFruit()方法能够比较两个水果大小,而后返回大的水果,代码以下:ui
public class Box<T extends Fruit>{ public T getBigFruit(T t1, T t2) { // if (!(t1 instanceof Fruit) || !(t2 instanceof Fruit)) { // throw new RuntimeException("T不是水果"); // } if (t1.getWeight() > t2.getWeight()) { return t1; } return t2; } }
代码中须要注意两个地方:一个是注释的三行,参数限定以后,不必判断t1和t2的类型了,若是类型不对,在Box实例化的时候就报错了;另外一个是t1.getWeight(),在Box类里面,t1是T类型,T类型限定为Fruit,因此这里能够直接调用Fruit里面的方法getWeight()(确切的说是能够调用Fruit里面能够被子类继承的方法,由于限定以后,实参也能够是Fruit的子类),若是不加限定,那么T就默认是Object类型,t1.getWeight()就会报错由于Object里面没有这个方法(调用Object里面的方法是能够的)。这就是是类型限定的两个好处。this
类型也可使用接口限定,好比
先看三行代码
Fruit f = new Apple(); Fruit[] farray = new Apple[10]; ArrayList<Fruit> flist = new ArrayList<Apple>();
第一行的写法是很常见的,父类引用指向子类对象,这是java多态的表现。相似的,第二行父类数组的引用指向子类数组对象在java中也是能够的,这称为数组的协变。Java把数组设计为协变的,对此是有争议的,有人认为这是一种缺陷。
虽然Apple[]能够“向上转型”为Fruit[],但数组元素的实际类型仍是Apple,因此只能向数组中放入Apple或者Apple的子类。在上面的代码中,向数组中放入了Fruit对象和Orange对象,对于编译器来讲,这是能够经过编译的,可是在运行时期,JVM可以知道数组的实际类型是Apple[],因此当其它对象加入数组的时候在运行期会抛出异常。
由上可知,协变的缺陷在于可能的异常发生在运行期,而编译期间没法检查,泛型设计的目的之一就是避免这种问题,因此泛型是不支持协变的,也就是说,上面的第三行代码是编译不经过的。可是,有时候是须要创建这种“向上转型”的关系的,好比定义一个方法,打印出任意类型的List中的全部数据,示例以下:
public void printCollection(List<Object> collection) { for (Object obj : collection) { System.out.println(obj); } } ------------------------------------ List<Integer> listInteger =new ArrayList<Integer>(); List<String> listString =new ArrayList<String>(); printCollection(listInteger); //编译错误 printCollection(listString); //编译错误
由于泛型不支持协变,即List<Object> collection = new ArrayList<Integer>();
没法经过编译,因此printCollection(listInteger)
就会报错。
这时就须要使用通配符来解决,通配符<?>,用来表示某种特定的类型,可是不知道这个类型究竟是什么。例以下面的例子都是合法的:
List<?> collection1 = new ArrayList<Fruit>(); List<?> collection2 = new ArrayList<Number>(); List<?> collection3 = new ArrayList<String>(); List<?> collection4 = new ArrayList<任意类型>(); // 对比不合法的 List<Fruit> flist = new ArrayList<Apple>();
因此printCollection()方法改为下面这样便可:
public void printCollection(List<?> collection) { for (Object obj : collection) { System.out.println(obj); } }
这就是通配符的简单用法。须要注意的是,由于不知道 "?" 类型究竟是什么,因此List<?> collection
中的collection不能调用带泛型参数的方法,可是能够调用与泛型参数类型无关的方法,以下:
collection.add("a"); //错误,由于add方法参数是泛型E collection.size(); //正确,由于无参即与泛型参数类型E无关 collection.contains("a"); //正确,由于contains参数是Object类型,与泛型参数类型E无关
注:collection.add(null);是能够的,除了null其余任何类型都不能够,Object也不行。
通配符可使用extends和super关键字来限制:
注意区分 泛型变量的类型限定 和 通配符的边界限定:
泛型变量的类型限定只能使用extends关键字,通配符的边界限定可使用extends或super来限定上边界或下边界。
Java中的泛型是经过类型擦除来实现的伪泛型。类型擦除指的是从泛型类型中清除类型参数的相关信息,而且在必要的时候添加类型检查和类型转换的方法。类型擦除能够简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通的java字节码,看下面的例子:
泛型的Java代码以下:
class Pair<T> { private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
泛型Java代码,通过编译器编译后,会擦除泛型信息,将泛型代码转换为以下的普通Java代码:
class Pair { private Object value; public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
由上面的例子可知,泛型擦除的结果就是用Object替换T,最终生成一个普通的类。上面的例子替换成Obejct是由于在Pair
至此能够知道,类型擦除的过程:
泛型只存在于代码中,泛型信息在编译时都会被擦除,因此虚拟机中没有泛型,只有普通类和普通方法。
使用泛型时会有一些问题和限制,大部分是由类型擦除引发的,因此只要记住:泛型擦除后留下的只有原始类型。那么大部分问题都是很容易理解的。好比下面的例子:
public void test(List<String> list){ } public void test(List<Integer> list){ }
两个方法通过泛型擦除后,都只留下原始类型List,因此它们是同一个方法而不是方法的重载,若是这两个方法在同一个类中同时存在,编译器是会报错的。
其余更多问题以下:
这些问题的答案在:java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题,本文也是学习这篇博客和一些其余博客后的一个总结。
参考文章:
Java泛型-类型擦除
java泛型(一)、泛型的基本介绍和使用
java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
Java 泛型总结(三):通配符的使用