目录
一、什么是泛型?
二、为什么要使用泛型?
三、泛型的规则
三、泛型的使用场景
四、泛型的使用方式
五、泛型的通配符(边界)
六、泛型的类型擦除
七、泛型的阴暗角落
八、总结
泛型是Java SE 1.5 的新特性,《Java 核心技术》中对泛型的定义是: “泛型” 意味着编写的代码可以被不同类型的对象所重用。可见泛型的提出是为了编写重用性更好的代码。而泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
public class Paly{
T play(){}
}
其中T就是作为一个类型参数在Play被实例化的时候所传递来的参数,比如:
Play playInteger=new Play<>();
这里T就会被实例化为Integer。
1、使用泛型能写出更加灵活通用的代码
泛型的设计主要参照了C++的模板,旨在能让人写出更加通用化,更加灵活的代码。模板/泛型代码,就好像做雕塑时的模板,有了模板,需要生产的时候就只管向里面注入具体的材料就行,不同的材料可以产生不同的效果,这便是泛型最初的设计宗旨。
2、泛型将代码安全性检查提前到编译期并能够省去类型强制转换。
泛型被加入Java语法中,有一个最大的原因:解决容器的类型安全,使用泛型后,能让编译器在编译的时候借助传入的类型参数检查对容器的插入,获取操作是否合法,编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常,符合越早出错代价越小原则。比如:
List dogs =new ArrayList();
dogs.add(new Cat());
在没有泛型之前,这种代码除非运行,否则你永远找不到它的错误。但是加入泛型后会在编译的时候就检查出来。
List dogs=new ArrayList<>();
dogs.add(new Cat());//Error Compile
并在引入泛型之前,要想实现一个通用的、可以处理不同类型的方法,你需要使用 Object 作为属性和方法参数。然而由于 Object 是所有类的父类,所有的类都可以作为成员被添加到上述类中;当需要使用的时候,必须进行强制转换。
Dog dog=(Dog)dogs.get(1);
加入泛型后,由于编译器知道了具体的类型,因此编译期会自动进行强制转换,使得代码更加优雅。
3、潜在的性能收益
由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已。
泛型的参数类型只能是类(包括自定义类),不能是简单类型。
同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
泛型的类型参数可以有多个。
泛型的参数类型可以使用 extends 语句,习惯上称为“有界类型”
泛型的参数类型还可以是通配符类型,例如 Class。
当类中要操作的引用数据类型不确定的时候,过去使用 Object 来完成扩展,JDK 1.5后推荐使用泛型来完成扩展,同时保证安全性。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
类型参数的意义是告诉编译器这个集合中要存放实例的类型,从而在添加其他类型时做出提示,在编译时就为类型安全做了保证。
这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
/**
*
* Description: 泛型类
*
*
* Author: shixinzhang
*/
public class GenericClass {
private F mContent;
public GenericClass(F content){
mContent = content;
}
/**
* 泛型方法
* @return
*/
public F getContent() {
return mContent;
}
public void setContent(F content) {
mContent = content;
}
/**
* 泛型接口
* @param
*/
public interface GenericInterface{
void doSomething(T t);
}
}
1、泛型类
2、泛型接口
和泛型类一样,泛型接口在接口名后添加类型参数,比如上面的 GenericInterface
未指明类型的实现类,默认是 Object 类型:
public class Generic implements GenericInterface{
@Override
public void doSomething(Object o) {
//...
}
}
指明了类型的实现:
public class Generic implements GenericInterface{
@Override
public void doSomething(String s) {
//...
}
}
泛型接口比较实用的使用场景就是用作策略模式的公共策略,比如 Java 解惑:Comparable 和 Comparator 的区别 中介绍的 Comparator,它就是一个泛型接口:
public interface Comparator {
public int compare(T lhs, T rhs);
public boolean equals(Object object);
}
泛型接口定义基本的规则,然后作为引用传递给客户端,这样在运行时就能传入不同的策略实现类。
3、泛型方法
泛型方法是指使用泛型的方法,如果它所在的类是个泛型类,那就很简单了,直接使用类声明的参数。如果一个方法所在的类不是泛型类,或者他想要处理不同于泛型类声明类型的数据,那它就需要自己声明类型,举个例子:
**
* 传统的方法,会有 unchecked ... raw type 的警告
* @param s1
* @param s2
* @return
*/
public Set union(Set s1, Set s2){
Set result = new HashSet(s1);
result.addAll(s2);
return result;
}
/**
* 泛型方法,介于方法修饰符和返回值之间的称作 类型参数列表 (可以有多个)
* 类型参数列表 指定参数、返回值中泛型参数的类型范围,命名惯例与泛型相同
* @param s1
* @param s2
* @param
* @return
*/
public Set union2(Set s1, Set s2){
Set result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
注意上述代码在返回值前面也有个
有时候希望传入的类型有一个指定的范围,从而可以进行一些特定的操作,这时候就是通配符边界登场的时候了。泛型中有三种通配符形式:
1、> 无限制通配符
要使用泛型,但是不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 > ),表示可以持有任何类型。大部分情况下,这种限制是好的,但这使得一些理应正确的基本操作都无法完成,比如交换两个元素的位置,看代码:
private void swap(List> list, int i, int j){
Object o = list.get(i);
list.set(j,o);
}
这个代码看上去应该是正确的,但 Java 编译器会提示编译错误,set 语句是非法的。编译器提示我们把方法中的 List> 改成 List借助带类型参数的泛型方法,这个问题可以这样解决:
private void swapInternal(List list, int i, int j) {
//...
list.set(i, list.set(j, list.get(i)));
}
private void swap(List> list, int i, int j){
swapInternal(list, i, j);
}
swap 可以调用 swapInternal,而带类型参数的 swapInternal 可以写入。Java容器类中就有类似这样的用法,公共的 API 是通配符形式,形式更简单,但内部调用带类型参数的方法。
(这个例子引自: http://mp.weixin.qq.com/s/te9K3alu8P8jRUUU2AkO3g )
2、上界通配符 < ? extends E>
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
举个例子:
/**
* 有限制的通配符之 extends (有上限),表示参数类型 必须是 BookBean 及其子类,更灵活
* @param arg1
* @param arg2
* @param
* @return
*/
private E test2(K arg1, E arg2){
E result = arg2;
arg2.compareTo(arg1);
//.....
return result;
}
可以看到,类型参数列表中如果有多个类型参数上限,用逗号分开。
3、下界通配符 < ? super E>
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
根据代码介绍吧:
private void add(List super E> dst, List src){
for (E e : src) {
dst.add(e);
}
}
4、通配符比较
通过上面的例子我们可以知道。
用《Effective Java》 中的一个短语来加深理解:
为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限:PECS: producer-extends, costumer-super
因此使用通配符的基本原则:
小总结一下:
举个例子:
private > E max(List extends E> e1){
if (e1 == null){
return null;
}
//迭代器返回的元素属于 E 的某个子类型
Iterator extends E> iterator = e1.iterator();
E result = iterator.next();
while (iterator.hasNext()){
E next = iterator.next();
if (next.compareTo(result) > 0){
result = next;
}
}
return result;
}
上述代码中的类型参数 E 的范围是
1、擦除的概念
Java 中的泛型和 C++ 中的模板有一个很大的不同:
在 Java 中,泛型是 Java 编译器的概念,用泛型编写的 Java 程序和普通的 Java 程序基本相同,只是多了一些参数化的类型同时少了一些类型转换。实际上泛型程序也是首先被转化成一般的、不带泛型的 Java 程序后再进行处理的,编译器自动完成了从 Generic Java 到普通 Java 的翻译,Java 虚拟机运行时对泛型基本一无所知。当编译器对带有泛型的java代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种普通的字节码可以被一般的 Java 虚拟机接收并执行,这在就叫做 类型擦除(type erasure)。
实际上无论你是否使用泛型,集合框架中存放对象的数据类型都是 Object,这一点不仅仅从源码中可以看到,通过反射也可以看到。
List strings = new ArrayList<>();
List integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());//true
上面代码输出结果并不是预期的 false,而是 true。其原因就是泛型的擦除。
2、擦除的原理
一直有个疑问,Java 编译器在编译期间擦除了泛型的信息,那运行中怎么保证添加、取出的类型就是擦除前声明的呢?
Java 编辑器会将泛型代码中的类型完全擦除,使其变成原始类型。当然,这时的代码类型和我们想要的还有距离,接着 Java 编译器会在这些代码中加入类型转换,将原始类型转换成想要的类型。这些操作都是编译器后台进行,可以保证类型安全。总之泛型就是一个语法糖,它运行时没有存储任何类型信息。擦除导致的泛型不可变性。泛型中没有逻辑上的父子关系,如 List 并不是 List 的父类。两者擦除之后都是List,所以形如下面的代码,编译器会报错:
/**
* 两者并不是方法的重载。擦除之后都是同一方法,所以编译不会通过。
* 擦除之后:
*
* void m(List numbers){}
* void m(List strings){} //编译不通过,已经存在相同方法签名
*/
void method(List numbers) {
}
void method(List strings) {
}
泛型的这种情况称为 不可变性,与之对应的概念是 协变、逆变:
1)、协变
Java 中数组是协变的,泛型是不可变的。如果想要让某个泛型类具有协变性,就需要用到边界。
对于协变,我们见得最多的就是多态,而逆变常见于强制类型转换。这好像没什么奇怪的。但是看以下代码:
public static void error(){
Object[] nums=new Integer[3];
nums[0]=3.2;
nums[1]="string"; //运行时报错,nums运行时类型是Integer[]
nums[2]='2';
}
因为数组是协变的,因此Integer[]可以转换为Object[],在编译阶段编译器只知道nums是Object[]类型,而运行时nums则为Integer[]类型,因此上述代码能够编译,但是运行会报错。
这就是常见的人们所说的数组是协变的。这里带来一个问题,为什么数组要设计为协变的呢?既然不让运行,那么通过编译有什么用?
答案是在泛型还没出现之前,数组协变能够解决一些通用的问题:
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
/**
* 摘自JDK 1.8 Arrays.equals()
*/
public static boolean equals(Object[] a, Object[] a2) {
//...
for (int i=0; i
可以看到,只操作数组本身,而关心数组中具体保存的原始,或则是不管什么元素,取出来就作为一个Object存储的时候,只用编写一个Object[]就能写出通用的数组参数方法。比如:
Arrays.sort(new Student[]{...})Arrays.sort(new Apple[]{...})
等,但是这样的设计留下来的诟病就是偶尔会出现对数组元素有具体的操作的代码,比如上面的error()方法。
泛型的出现,是为了保证类型安全的问题,如果将泛型也设计为协变的话,那也就违背了泛型最初设计的初衷,因此在Java中,泛型是不变的,什么意思呢?
List
public static void test(List
方法,是无法传递一个List
2)、逆变
逆变一般常见于强制类型转换。
Object obj="test";String str=(String)obj;
原理便是Java 反射机制能够记住变量obj的实际类型,在强制类型转换的时候发现obj实际上是一个String类型,于是就正常的通过了运行。泛型与向上转型的实现。
前面说了这么多,应该关心的问题在于,如何解决既能使用数组协变带来的方便性,又能得到泛型不变带来的类型安全?答案依然是extend,super关键字与通配符?泛型重载了extend,super关键字来解决通用泛型的表示。
注意:这句话可能比较熟悉,没错,前面说过extend还被用来指定擦除到的具体类型,比如
概念麻烦,直接看代码:
协变泛型:
public static void playFruit(List < ? extends Fruit> list){
//do somthing
}
public static void main(String[] args) {
List apples=new ArrayList<>();
List oranges=new ArrayList<>();
List foods =new ArrayList<>();
playFruit(apples);
playFruit(oranges);
//playFruit(foods); 编译错误
}
可以看到,参数List < ? extend Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是继承自Fruit的。这样便解决了泛型无法向上转型的问题,前面说过,数组也能向上转型,但是存取元素有问题啊,这里继续深入,看看泛型是怎么解决这一问题的。
public static void playFruit(List < ? extends Fruit> list){
list.add(new Apple());
}
向传入的list添加元素,你会发现编译器直接会报错。
逆变泛型:
public static void playFruitBase(List < ? super Fruit> list){
//..
}
public static void main(String[] args) {
List apples=new ArrayList<>();
List foods =new ArrayList<>();
List objects=new ArrayList<>();
playFruitBase(foods);
playFruitBase(objects);
//playFruitBase(apples); 编译错误
}
同理,参数List < ? super Fruit>所表示是需要一个List<>,其中尖括号所指定的具体类型必须是Fruit的父类类型。
public static void playFruitBase(List < ? super Fruit> list){
Object obj=list.get(0);
}
取出list的元素,你会发现编译器直接会报错。
为什么要这么麻烦要区分开到底是xxx的父类还是子类,不能直接使用一个关键字表示么?
前面说过,数组的协变之所以会有问题是因为在对数组中的元素进行存取的时候出现的问题,只要不对数组元素进行操作,就不会有什么问题,因此可以使用通配符?达到此效果:
public static void playEveryList(List < ?> list){
//..
}
对于playEveryList方法,传递任何类型的List都没有问题,但是你会发现对于list参数,你无法对里面的元素存和取。这样便达到了上面所说的安全类型的协变数组的效果。但是觉得多数时候,我们还是希望对元素进行操作的,这就是extend和super的功能。
extend Fruit>表示传入的泛型具体类型必须是继承自Fruit,那么我们可以里面的元素一定能向上转型为Fruit。但是也仅仅能确定里面的元素一定能向上转型为Fruit
public static void playFruit(List < ? extends Fruit> list){
Fruit fruit=list.get(0);
//list.add(new Apple());
}
比如上面这段代码,可以正确的取出元素,因为我们知道所传入的参数一定是继承自Fruit的,比如:
List apples=new ArrayList<>();
List oranges=new ArrayList<>();
都能正确的转换为Fruit。但是我们并不知道里面的元素具体是什么,有可能是Orange,也有可能是Apple,因此,在list.add()的时候,就会出现问题,有可能将Apple放入了Orange里面,因此,为了不出错,编译器会禁止向里面加入任何元素。这也就解释了协变中使用add会出错的原因。
同理: super Fruit>表示传入的泛型具体类型必须是Fruit的父类,那么我们可以确定只要元素是Fruit以及能转型为Fruit的,一定能向上转型为对应的此类型,比如:
public static void playFruitBase(List < ? super Fruit> list){
list.add(new Apple());
}
因为Apple继承自Fruit,而参数list最终被指定的类型一定是Fruit的父类,那么Apple一定能向上转型为对应的父类,因此可以向里面存元素。但是我们只能确定他是Furit的父类,并不知道具体的“上限”。因此无法将取出来的元素统一的类型(当然可以用Object)。比如:
List eatables=new ArrayList<>();
List foods=new ArrayList<>();
除了
obj=eataObject obj;
bles.get(0);
obj=foods.get(0);
之外,
没有确定类型可以修饰obj以达到类似的效果。
针对上述情况。我们可以总结为:PECS原则,Producer-Extend,Customer-Super,也就是泛型代码是生产者,使用Extend,泛型代码作为消费者。
通过擦除而实现的泛型,有些时候会有很多让人难以理解的规则,但是了解了泛型的真正实现又会觉得这样做还是比较合情合理。下面分析一下关于泛型在应用中有哪些奇怪的现象:
1、擦除的地点---边界
static T[] toArray(T... args) {
return args;
}
static T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // Can't get here
}
public static void main(String[] args) {
String[] attributes = pickTwo("Good", "Fast", "Cheap");
}
这是在《Effective Java》中看到的例子,编译此代码没有问题,但是运行的时候却会类型转换错误:Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
当时对泛型并没有一个很好的认识,一直不明白为什么会有Object[]转换到String[]的错误。现在我们来分析一下:
static T[] toArray(T... args) {
return args;
}
//和
static Object[] toArray(Object... args){
return args;
}
生成的二进制文件是一致的。进而剥开可变数组的语法糖:
static Object[] toArray(Object[] args){
return args;
}
static T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(a, c);
case 2: return toArray(b, c);
}
throw new AssertionError(); // Can't get here
}
//和
static Object[] pickTwo(Object a, Object b, Object c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(new Object[]{a,b});//可变参数会根据调用类型转换为对应的数组,这里a,b,c都是Object
case 1: return toArray(new Object[]{a,b});
case 2: return toArray(new Object[]{a,b});
}
throw new AssertionError(); // Can't get here
}
是一致的。
那么调用pickTwo方法实际编译器会帮我进行类型转换
public static void main(String[] args) {
String[] attributes =(String[])pickTwo("Good", "Fast", "Cheap");
}
可以看到,问题就在于可变参数那里,使用可变参数编译器会自动把我们的参数包装为一个数组传递给对应的方法,而这个数组的包装在泛型中,会最终翻译为new Object,那么toArray接受的实际类型是一个Object[],当然不能强制转换为String[]。
上面代码出错的关键点就在于泛型经过擦除后,类型变为了Object导致可变参数直接包装出了一个Object数组产生的类型转换失败。
2、基类劫持
public interface Playable {
T play();
}
public class Base implements Playable {
@Override
public Integer play() {
return 4;
}
}
public class Derived extend Base implements Playable{
...
}
可以发现在定义Derived类的时候编译器会报错。
观察Derived的定义可以看到,它继承自Base。那么它就拥有一个Integer play()和方法,继而实现了Playable
public static void main(String[] args){
new Derived().play();
}
编译器并不知道应该调用哪一个play()方法。
3、自定义类型
自限定类型简单点说就是将泛型的类型限制为自己以及自己的子类。最常见的在于实现Compareable接口的时候:
public class Student implements Comparable{
}
这样就成功的限制了能与Student相比较的类型只能是Student,这很好理解。
但是正如Java 中返回类型是协变的:
public class father{
public Number test(){
return nll;
}
}
public class Son extend father{
@Override
public Interger test(){
return null;
}
}
有些时候对于一些专门用来被继承的类需要参数也是协变的。比如实现一个Enum:
public abstract class Enum implements Comparable,Serializable{
@Override
public int compareTo(Enum o) {
return 0;
}
}
这样是没有问题的,但是正如常规所说,假如Pen和Cup都继承于Enum,但是按道理来说笔和杯子之间相互比较是没有意义的,也就是说在Enum中compareTo(Enum o)方法中的Enum这个限定词太宽泛,这个时候有两种思路:
而更好的解决方案便是使用泛型的自限定类型:
public abstract class Enum> implements Comparable,Serializable{
@Override
public int compareTo(E o) {
return 0;
}
}
泛型的自限定类型比起传统的自限定类型有个更大的优点就是它能使泛型的参数也变成协变的。
这样每个子类只用在集成的时候指定类型
public class Pen extends Enum{}
public class Cup extends Cup{}
便能够在定义的时候指定想要与那种类型进行比较,这样达到的效果便相当于每个子类都分别自己实现了一个自定义的Comparable接口。
自限定类型一般用在继承体系中,需要参数协变的时候。
1.上面说到使用 Object 来达到复用,会失去泛型在安全性和直观表达性上的优势,那为什么 ArrayList 等源码中的还能看到使用 Object 作为类型?
根据《Effective Java》中所述,这里涉及到一个 “移植兼容性”:泛型出现时,Java 平台即将进入它的第二个十年,在此之前已经存在了大量没有使用泛型的 Java 代码。人们认为让这些代码全部保持合法,并且能够与使用泛型的新代码互用,非常重要。
这样都是为了兼容,新代码里要使用泛型而不是原始类型。
2.泛型是通过擦除来实现的。因此泛型只在编译时强化它的类型信息,而在运行时丢弃(或者擦除)它的元素类型信息。擦除使得使用泛型的代码可以和没有使用泛型的代码随意互用。
3.如果类型参数在方法声明中只出现一次,可以用通配符代替它。
比如下面的 swap 方法,用于交换指定 List 中的两个位置的元素:
private void swap(List list, int i, int j) {
//...
}
只出现了一次 类型参数,没有必要声明,完全可以用通配符代替:
private void swap(List> list, int i, int j){
//...
}
对比一下,第二种更加简单清晰。
4.数组中不能使用泛型
这可能是 Java 泛型面试题中最简单的一个了,当然前提是你要知道 Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。
5.Java 中 List
原始类型和带参数类型 之间的主要区别是:
在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查。通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String 或 Integer。你可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误。
这道题的考察点在于对泛型中原始类型的正确理解。
参考:
深入理解 Java 泛型
Java 干货之深入理解Java泛型