Java泛型是Java5推出的一个强大的特性,那什么是泛型?下面是从维基百科上摘下来的定义:java
泛型的定义主要有如下两种:程序员
不论使用哪一个定义,泛型的参数在真正使用泛型时都必须做出指明。数组
Java中的泛型适用于第一种定义,即:在程序编码中一些包含类型参数的类型,也就是说泛型的参数只能够表明类,不能表明个别对象。bash
什么是类型参数?假设你手上有两个彻底相同容器(自行想象,锅碗瓢盆什么的),如今俩都仍是空的,但你也不想什么乱七八糟的东西都往里面扔,因此搞了两个小纸条,上面写的“T恤”,一个写的“鞋子”,分别贴到两个容器上,之后贴有T恤的容器就只装T恤,贴有鞋子的容器就只装鞋子。在这个小例子中,小纸条上的内容就是所谓的“类型参数”。并发
上面的例子可能不太合适(实在是不太好举例),但不用担忧,到下面看到Java泛型的“样子”时,再回想这个例子,就会明白了。工具
Java泛型有三种使用方式,分别是:泛型类、泛型接口、泛型方法,下面将就这三种方式逐一介绍。学习
当泛型做用在类定义的时候,该类就是泛型类,JDK里(1.5以后)有不少泛型类,例如ArrayList,HashMap,ThreadLocal等,以下所示:测试
public class MyList<T> {
//.....
}
复制代码
中的T是泛型标识,能够是任意字符,不过通常会采用一些通用的单字符或者双字符,例如T、K、V、E等。在编写类定义的时候可使用T来代替类型,例如:编码
//用在方法参数上和返回值上
//合法的
public T method1(T val) {
//do something
return (一个T类型的对象);
}
//不合法,不能用在静态方法
public static T method1(T val) {
//do something
return (一个T类型的对象);
}
//用在字段声明
private T val; //ok
private static T staticVal; //不合法,不能用在静态字段上
复制代码
至于为何不能用在静态字段或者方法上,后面讲到泛型的实现时会讲到,这里先把这个问题放着。
JDK里也有不少泛型接口,例如List,Map,Set等,当泛型做用在接口定义的时候,这个接口就是一个泛型接口,例如:
public interface MyGenericInterface<T> {
//用在抽象方法上
T method1(T t);
//或者默认方法也是能够的
default T method2(T val) {
}
//但仍然不能做用在静态方法和静态字段上
//不合法
static T method3() {
}
//字段就很好理解了,怎么写都不像合法的
T message = "MESSAGE"; //语法规定了接口里的字段默认是static final的,因此必需要有初始化值,但T不表明某个具体的类型,因此泛型字段根本不合理。
}
复制代码
代码注释写的比较清楚了,很少作说明了,接下来看看泛型方法。
当泛型做用在方法上时,该方法就是一个泛型方法。注意,这里和以前在泛型类或者泛型接口中的方法里使用泛型是不一样的,咱们既能够在一个泛型类或者泛型接口中定义泛型方法,也能够在普通类或者接口中定义泛型方法。泛型方法较泛型类和泛型接口的定义稍微复杂一些,以下所示:
public <E> E method1(E val) {
return val;
}
//静态方法也是合法的
public static <E> E method2(E val) {
return val;
}
复制代码
这里的泛型标识要在修饰符以后,返回值以前的位置,不能放错,这里的泛型标识E的做用范围仅限于方法内部,便可以简单的将该泛型标识是一个局部变量(实际上不是)。但为何这时候泛型能够做用在静态方法上了呢?仍是和以前同样,留到后面解释。
上面三个小结介绍了泛型类,泛型接口和泛型方法,但仅仅是介绍了如何定义,没有介绍到如何使用泛型,在实践的过程当中,会接触到文章最开始说到的“类型参数”的概念,但愿能对读者有帮助。
public class Main {
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
for (Integer integer : integers) {
System.out.println(integer);
}
}
}
复制代码
代码很是很是简单,使用了List接口和ArrayList实现类,注意这一行:
List<Integer> integers = new ArrayList<>();
复制代码
Integer即所谓的“类型参数”,表示这个List容器只能存放Integer类以其子类对象实例,类型参数只能是引用类型,不能是基本类型(例如int,double,char等),JVM会在编译期会经过类型检查来保证这一点。赋值号后面的<>称做“菱形操做符”,是Java7提供的一个语法糖,用于简化泛型的使用,编译器会自动推断出类型参数,例如在这里,编译器会自动推断出类型参数是Integer,而不用在显式指明ArrayList的类型参数,在Java7以前,上面那一行语句不得不这样写:
List<Integer> integers = new ArrayList<Integer>();
复制代码
在声明并赋值完成以后,咱们往容器里“扔”了两个元素1和2,由于自动装箱的缘由,1和2会被包装成Integre类的实例,因此并不会发生类型安全问题,假设如今加入以下语句:
integers.add("yeonon");
复制代码
会发生什么状况?编译会报错,错误提示的意思大概是类型不匹配。为何呢?其实在刚刚已经说了,这个容器有一个类型参数Integer,这就代表了该容器只能存放Integer类以其子类对象实例,若是强行放入其余类型的实例,由于类型检查机制的存在,因此会发生类型匹配异常,这个就是泛型最重要的一个特性:保证类型安全。在没有泛型机制以前,咱们会这样使用容器类:
List integers = new ArrayList();
integers.add(1);
integers.add(2);
integers.add("yeonon");
复制代码
编译一下,发生编译经过,只不过有一些警告而已。这是有类型安全问题的,为何?例如如今我要从容器中提取元素,就不得不进行强制类型转换,以下所示:
Integer i1 = (Integer) integers.get(0);
Integer i2 = (Integer) integers.get(1);
String s1 = (String) integers.get(2);
复制代码
固然,彻底能够不作类型转换,直接使用Object类来接收元素,但那有什么意义呢?光有一个Object引用,几乎没什么操做空间,最终仍是要作类型转换的。
幸亏这里只有三个元素,并且都明确知道元素的顺序,第1,2个是Integer类型的,第3个是String类型的,因此能够准确的作出类型转换。那若是是下面这种状况呢?
public processList(List list) {
//如何处理元素?
}
复制代码
在processList方法中,List是从外部传进来的,彻底不知道这个List里是些什么东西,若是鲁莽的将元素强转成某种类型,就很是有可能出现强转异常,并且该异常仍是运行时异常,即不肯定何时会发生异常!可能你会说,那给方法写个文档说明,说明List里存的元素是Integer类型,而后要求客户端也必须传入元素全是Integer的List,这不就完事儿了?确实,这是一个解决方案,但这其实只是在制定“协议”,并且这个协议属于“君子协议”,客户端彻底可能会出于各类各样的缘由违反这个协议(例如客户端被入侵了,或者调用者没有注意到这个“协议”),因此,仍是有可能发生类型安全问题。
经过这个例子,我想读者已经能感觉到泛型带来的好处了,泛型能够在编译期发现类型错误,并发出错误报告,提示程序员!这使得类型安全问题不会出如今不可控的运行时,而是出如今可控的编译期,这个特性使Java语言的安全性大大提升。
那Java中的泛型是如何实现的呢?答案是经过“擦除”来实现的。
常常在论坛、社区里听到Java的泛型实现是伪泛型,而C#、C++的泛型实现才是真正的泛型。这么说是有缘由的,由于Java源码编译后的字节码里不存在什么类型参数。举个例子,现有以下代码:
public class Main {
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
for (Integer integer : integers) {
System.out.println(integer);
}
}
}
复制代码
使用Javac编译,编译后的.class文件内容以下(我使用的是IDEA来打开的,若是使用其余工具,可能会略有差异):
public class Main {
public Main() {
}
public static void main(String[] var0) {
ArrayList var1 = new ArrayList();
var1.add(1);
var1.add(2);
Iterator var2 = var1.iterator();
while(var2.hasNext()) {
Integer var3 = (Integer)var2.next();
System.out.println(var3);
}
}
}
复制代码
发现,确实没有相似的字符出现了,换句话说,类型参数被“擦除”了。取而代之的是,当有须要进行类型转换的时候,编译器帮咱们加上了强制类型转换的语法,例如这句:
Integer var3 = (Integer)var2.next();
复制代码
从这里能够看出,JVM是不知道类型参数的信息的(JVM只认字节码),知道了这一点以后就能够回答上面留下的两个问题了。
静态方法或者静态字段是属于类信息的一部分,存储在方法区且只有一份,可被类的多个不一样实例共享,所以即便编译器知道类型信息,能够作特殊处理,也没法为静态量肯定某一种类型。假设容许静态方法或者静态字段,以下代码所示:
public A<T> {
public static T val;
}
public static void main(String[] args) {
A<Integer> a1 = new A<>();
A<String> a2 = new A<>();
System.out.println(a1.val);
System.out.println(a2.val);
}
复制代码
这里的val到底应该是什么类型呢?若是该程序能正常运行,那么只有一种可能,就是有两份不一样类型的静态量,但虚拟机的知识告诉咱们,这显然是不符合规范的,因此这种使用方法是不被容许的。
反过来看一下普通实例方法和字段,由于普通实例方法和字段是能够有多份的(每一个对象一份),因此编译器彻底能够根据类型参数来肯定对象实例里的实例方法和字段的类型。须要注意的是,这里的类型信息是编译器知道的,虚拟机是不知道的,编译器能够为每一个不一样参数类型的实例对象作类型检查、类型转换等操做。例如上面的a1和a2对象,编译器知道他们的类型参数分别是Integre和String,因此在编译的时候能够对他们作类型检查、类型转换等。
其实这仍是编译器的“把戏”。来看个例子:
public class Main {
public static void main(String[] args) {
MyList<Integer> list1 = new MyList<>();
MyList<String> list2 = new MyList<>();
MyList.method2(1);
MyList.method2("String");
}
}
复制代码
用javac编译后,用javap来查看字节码信息,大体内容以下(省略了无关部分):
#21 = NameAndType #28:#29 // method2:(Ljava/lang/Object;)Ljava/lang/Object;
20: invokestatic #5 // Method top/yeonon/generic/MyList.method2:(Ljava/lang/Object;)Ljava/lang/Object;
23: pop
24: ldc #6 // String String
26: invokestatic #5 // Method top/yeonon/generic/MyList.method2:(Ljava/lang/Object;)Ljava/lang/Object;
复制代码
发如今序号20和26调用了method2方法,从常量池#21号能够看到,method2的参数是Object类型,说明在虚拟机中,泛型参数的类型实际只是Object类型,没有违背虚拟机规范。什么类型检查啊、自动类型推断、类型转换啊都是编译器本身加上去的。
更多关于泛型擦除的知识,建议多多参考资料,并结合javac、javap等工具进行研究。
在泛型系统中,大体有如下几种声明泛型的方式:
使用有界通配符能提高泛型的灵活性,使得泛型能够同时为多种类型而工做,从而使得咱们不须要为多种类型编写类似的代码,从另外一方面提供了代码的复用性。可是正确使用有界通配符会比较困难,其中最麻烦的是如何肯定使用有上界的通配符仍是有下界的通配符?《Effective Java》一书中给出了一个原则:PECS(producer-extends,consumer-super)。即对于生产者,使用有上界的通配符(extends,上界是T),对于消费者,使用有下界的通配符(super,下界是T)。
如今又有了新的问题,如何区分消费者和生产者。简单来讲,对于集合,消费者就是使用容器里的元素,例如List.sort(Comparator<? super E> c),sort须要使用到list内部的元素,因此这个方法是消费者,根据PECS原则,方法声明的参数应该是有下界的通配符。又例如List.addAll(Collection<? extends E> c)方法,addAll是将元素插入到容器中,属于生产者,根据PECS原则,方法参数应该使用有上界的通配符。
虽然有界通配符能提升API的灵活性,可是若是该方法不是消费者或是是生产者,那么就不要使用有界通配符了,直接使用便可,尽可能保持API的简单也是咱们的设计原则。
总之,使用有界通配符能够大大提供API的灵活性,不过在设计API时,应该尽可能保持简单,并且遵循PECS原则。
数组和泛型容器类是有很大区别的,JVM把A[]数组和B[]数组当作两种不一样的类型,而将List和List当作同一个类型List,假设能建立泛型数组,以下代码所示:
public class Main {
public static void main(String[] args) {
List<String>[] stringLists = new List<String>[1]; //1
List<Integer> integerList = new ArrayList<>(); //2
integerList.add(0); //3
Object[] objects = stringLists; //4
objects[0] = integerList; //5
String s = stringLists[0].get(0); //6
}
}
复制代码
代码有些绕,咱们一行一行分析:
这就是泛型数组带来的问题,最根本的缘由仍是由于泛型擦除的机制,虚拟机没法区分List和List,因此为了不这种难以发觉的问题,就干脆禁止建立泛型数组了。
虽然有一些办法能够绕开建立泛型数组的限制,但最好不要这样干,由于这样就失去了泛型带来的在编译期发现类型安全问题的好处,得不偿失。
本文简单介绍了泛型,也讲了一下泛型的实现方式:擦除。说实话,泛型是比较复杂难懂的知识点,想理解透彻,须要有必定的泛型使用经验,或者说是真真切切被坑过,不然会总以为泛型这玩意有点“虚无缥缈”。至于如何学习,个人经验是阅读JDK的源码,注意JDK是如何使用泛型的。
《Effective Java》第三版(英文版)