2018-04-06

Java范型

1)基本概念:

  泛型(Generic Type或Generics)是 对Java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。可以把类型参数看做是使用参数化类型时指定的类型的一个占位符,就像方法的形 式参数是运行时传递的占位符一样,泛型的体现主要是在集合框架里面可以看到,JCF里面应该是1.5里面使用泛型最多的地方。Java语言引入泛型是一个 较大的功能增强,不仅语言、类型系统和编译器有了大变化,以支持泛型,而且类库也进行了大翻修,所以许多重要的类,比如集合框架,都已经成为了泛型化的 了,使用泛型的优点为:

类型安全

​ 泛型的主要目标是提高Java程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就 只存在于程序员的脑海中。Java程序中的一种流行技术是定义这样的集合,即它的元素或键是功能类型的,比如“_列表”。通过在变量声明中捕获这一附加的 类型信息,泛型允许编译器实施这些附加的约束,类型错误现在就可以在编译时被捕获了,而不是在运行时才来进行检测操作。

消除强制类型转换

​ 泛型的一个附带的好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,而且减少了出错的机会。比较两段代码:
不使用泛型的代码段:

1
2
3
List li = new ArrayList(); 
li.add(new Integer(3));
Integer i = (Integer)li.get(0);

​ 使用泛型:

1
2
3
List<Integer> li = new ArrayList<Integer>(); 
li.add(new Integer(3));
Integer i = li.get(0);

潜在的性能收获:

​ 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将类型转换插入到各种字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的JVM也带来了优化可能。
泛型本质上是提供类型的“类型参数【外露参数】”,它们也被称为参数化类型(Parameterized Type)或参量多态(Parametric Polymorphism), 用GJ(Generic Java)编写的程序看起来和普通的Java基本相同,只不过多了一些参数化的类型同时少了一些类型转换。实际上,GJ程序也是首先被转化成一般的不带泛 型的Java程序后再进行处理的,编译器自动完成从GenericJava到普通Java的翻译,具体转化过程分为以下几个阶段:
* 将参数化类型中的类型参数“擦除”(erasure)掉;
* 将类型变量用“上限(Upper bound)”取代,通常情况下这些上限是Object。这里的类型变量是指实例域,本地方法域,方法参数以方法返回值用来标记类型信息的“变量”。
* 添加类型转换并插入“桥方法”(bridge method),以覆盖可以正常的工作
转化后的程序和没有引入的程序程序员不得不手工完成转换的程序是非常一致的,下边针对GJ的特点做一个简要的总结:
* 类型安全:泛型的一个主要目标是提高Java的类型安全,使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来保证,这显然不如带有泛型的安全性高。
* 消除强制类型转换:泛型可以消除源代码中的许多强制类型转换,这样使得代码更加可读,并且减少出错的机会
* 向后兼容:支持泛型的Java编译器可以用来编译经过泛型扩充的Java程序(GJ程序),但是现有的没有使用泛型扩充的Java程序仍然可以用这些编译器来编译
* 层次清晰,比较规范:无论被编译的源程序是否使用泛型扩充,编译生成的字节码均可以被虚拟机接受执行。就是说不论编译器输入的GJ程序还是一般的Java程序,经过编译后的字节码都遵循了Java虚拟机规范里面针对字节码的要求,也可以说:泛型对Java虚拟机是透明的
* 性能收益:目前来讲GJ编写的代码和一般的Java代码在效率上是很接近的,但是泛型可以进一步优化
API中的类型定义:
K——键、比如映射的键
V——值,比如的内容,或者 _中的值
E——异常类型
T——泛型
——[1]简单的泛型例子——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.susan.java.enumeration; 

class GenClass<T>{
T ob;
GenClass(T o){
this.ob = o;
}

T getOb(){
return this.ob;
}

void showType(){
System.out.println("Type of T is " + ob.getClass().getName());
}
}

public class GenDemo {

public static void main(String args[]){
GenClass<Integer> iObject = new GenClass<Integer>(88);
iObject.showType();
}
}

这段代码的输出为:
Type of T is java.lang.Integer
【*:上边定义了一个带泛型的类,在使用的时候T可以替换为所有我们需要的类型,这种定义方式类似C++里面的模板定义。】

2)深入理解泛型:

  [1]数据类型转换:

  在前边已经说过了,Java里面存在类的向下转型和向上转型,而且Java语言里面有时候比较容易因为类型的检查引发转型的问题,因为很多时候需要不断地向下转型,这种方式往往增加了JVM的一部分运行时的开销。实际上程序中每个向下转型针对ClassCastException都是潜在的危险, 应当尽量避免它们,但是在写程序的过程,这种转型往往没有办法避免,即使特别优良的设计也是会存在的。其实JDK 1.4到JDK 1.5的升级过程泛型是一个大的跨度,这种跨度使得编写程序更加规范,其实在前边讲解集合的时候已经使用了很多泛型的编程格式了,提供的很多代码Demo 都是泛型的。这里再提供一段简单的泛型使用代码:

  ——[$]使用泛型的 List——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package org.susan.java.generic; 

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
*泛型List的使用,结合了JDK 1.4和JDK 1.5的两种用法,但是在JDK 1.4编译器中下边的方式编译不通过
**/
public class ListGenericDemo {

public static void main(String args[]){
List list = new ArrayList();
List<String> genList = new ArrayList<String>();
list.add("List 1");
list.add("List 2");
genList.add("Generic List 1");
genList.add("Generic List 2");
System.out.println("No Generic:");
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.print("[" + (String)iterator.next() + "],");
}

System.out.println("\nGeneric:");
Iterator<String> genIterator = genList.iterator();
while(genIterator.hasNext()){
System.out.print("[" + genIterator.next() + "],");
}
}
}

上边这段代码的输出为:

1
2
3
4
No Generic:
[List 1],[List 2],
Generic:
[Generic List 1],[Generic List 2],

  这里使用了两种不同的方式【*: 在最原始的定义的List参数里面,不使用泛型的时候都是直接使用Object类型,在传入String的时候会进行向下转型,一般情况不会出现转型失败 的情况,但是使用了泛型过后,就上边的genList代码段里面,只能添加String对象,如果使用了其他类型的元素这里编译器就会直接报错,这种做法消除了JVM本身的类型转换。】

  [2]基本类型限制

  Tiger【Java 5.0的版本号】中 的类型变量的限制之一就是:必须使用引用类型进行实例化,基本类型不起作用,就是说不能使用:List list = new ArrayList();这种定义方式。也就是说在泛型使用的过程里面,如果要针对基本类型进行泛型使用,必须要进行包装,就是 Boxing操作,比如把int包装成为Integer。

  这里参考以下泛型的类型限制:JSR-14中:

​ * 不应在静态成员中引用封闭类型参数
* 不能用基本类型实例化泛型类型参数
* 不能在数据类型转换或instanceof操作中使用“外露”类型参数
* 不能在new操作符中使用“外露”的类型参数
* 不能在类定义的implements或extends字句中使用 “外露”类型参数
这里简单透露一点JVM的原理:JVM 本身不支持泛型,在编译器进行泛型代码的编译的时候,其实是使用了“擦除”功能,就是JVM在编译带泛型的代码的时候,实际上对带泛型的代码进行了类型检 查,然后“擦除”泛型代码中的类型支持,转换为普通类型进行编译。这里有一个新概念成为“外露”类型——单独出现而不是位于某个类型中的类型参数如 (List中的T)针对T类型而言,T的上界就是Object。这一项技术的功能极其强大,我们可以使几乎所有泛型类型的精度增强,但 是与JVM兼容。
——[$]静态成员中的封闭类型参数——

1
2
3
4
5
6
7
8
9
package org.susan.java.generic; 
public class StaticListGenericDemo<T> {
static void metho1(){
//T t;
}
static class StaticClass{
//StaticListGenericDemo<T> t;
}
}

这里被注释掉的代码是不能通过JVM编译的,因为编译器完全禁止在静态方法和静态内部类中引用封闭类型参数,下边几种情况这里就不做解释了,T在整个过程里面是不应该作为外露类型来使用。
——[$]提供一个 instanceof操作中的“外露参数”——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.susan.java.generic; 
import java.util.Hashtable;
interface Registry{
public void register(Object o);
}

class C<T> implements Registry{
int counter = 0;
Hashtable<Integer,T> values;
public C(){
values = new Hashtable<Integer,T>();
}

public void register(Object o){
values.put(new Integer(counter), (T)o);
counter++;
}
}

【*:这段代码编译没有任何问题,但是在(T)o地方会有一个警告,虽然这些警告本身没有什么,事实上,它们会使得诊断代码变得极为困难。在以前的代码中,我们认为如果对实例 C 调用 register(“test”) ,会发出 ClassCastException 。但并非如此;计算将继续,就仿佛数据类型转换成功了一样,然后在进一步进行计算时发出错误,或者更糟:用遭破坏的数据完成计算,但不向外发出任何错误信号。同样,对“外露”类型参数的 instanceof 检查将在编译时产生 “unchecked”警告,而且检查将不会如期在运行时进行。】

[3]泛型的构造函数:

在定义泛型的构造函数的时候,要解决这一个问题,需要一定的操作:
* 要求类型参数的所有实例化都包括不带参数的构造函数
* 只要泛型类的运行时实例化没有包括所需要的构造函数,就抛异常
* 修改语言的语法以包括更加详细的类型参数界限
——[$]定义一个带泛型构造函数——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.susan.java.generic; 

class GenGons{

private double val;
<T extends Number> GenGons(T arg){
val = arg.doubleValue();
}

void showVal(){
System.out.println("Val: " + val);
}

}

public class GenGonsDemo {

public static void main(String args[]){
GenGons genOne = new GenGons(100);
GenGons genTwo = new GenGons(123.5F);
genOne.showVal();
genTwo.showVal();
}
}

这段程序输出为:
Val: 100.0
Val: 123.5

[4]泛型中通配符的使用:

通配符——使 用一个?标识类型参数,是一种表示未知类型的约束方法。通配符并不包含在最初的泛型设计中,从形成JSR14到发布其最终版本之间的五年时间内完成了设计 添加到泛型中。通配符在泛型的使用中具有重要的意义,它们为一个泛型类所指定的类型集合提供了一个有用的类型范围。对泛型的ArrayList而言,对于 任意类型T,ArrayList类型是ArrayList的超类型,但是这些超类型在执行类型推断方面是不起作用的。通配符类型List、原始List和具体List都不相同。如果说变量X具有List<?>类型,标识存在一些T类型,其中x是List类型,x具有相同的结构,尽管我们不知道其元素的具体类型。这并不代表它具有任意内容,而是指我们并不了解内容的类型限制是什么 — 但我们知道存在 某种限制。另一方面,原始类型 List 是异构的,我们不能对其元素有任何类型限制,具体类型 List 表示我们明确地知道它能包含任何对象(当然,泛型的类型系统没有 “列表内容” 的概念,但可以从 List 之类的集合类型轻松地理解泛型)。 通配符在类型系统中的作用部分来自其不会发生协变(covariant)这一特性。数组是协变的,因为 Integer 是 Number 的子类型,数组类型 Integer[] 是 Number[] 的子类型,因此在任何需要 Number[] 值的地方都可以提供一个 Integer[] 值。另一方面,泛型不是协变的, List 不是 List 的子类型,试图在要求 List 的位置提供 List 是一个类型错误。这不算很严重的问题,也不是所有人都认为的错误,但泛型和数组的不同行为的确引起了许多混乱。
——[$]通配符的使用 ——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.susan.java.generic; 

class Status<T extends Number>{

T[] nums;
Status(T[] o){
nums = o;
}

double average(){
double sum = 0.0;
for( int i = 0; i < nums.length; i++)
sum += nums[i].doubleValue();
return sum / nums.length;
}

boolean sameAvg(Status<?> obj){
if( average() == obj.average())
return true;
return false;
}
}

public class WildcardDemo {

public static void main(String args[]){
Integer inums[] = {1,2,3,4,5};
Status<Integer> iobj = new Status<Integer>(inums);
System.out.println("iob average is "+ iobj.average());
Double dnums[] = {1.1,2.2,3.3,4.4,5.5};
Status<Double> dobj = new Status<Double>(dnums);
System.out.println("dob average is "+ dobj.average());
Float fnums[] = {1.1F,2.2F,3.3F,4.4F,5.5F};
Status<Float> fobj = new Status<Float>(fnums);
System.out.println("fob average is "+ fobj.average());
}
}
  • 这段程序的输出为:

1
2
3
iob average is 3.0 
dob average is 3.3
fob average is 3.300000023841858

——[$]返回泛型值的方法——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.susan.java.generic; 

import java.io.Serializable;
class Base{}
class SubClass extends Base implements Serializable{}
class SubClassTwo extends Base implements Serializable{}
public class TypeInference {
public static <T extends Base> T Method(T t1,T t2){
return null;
}

public static void main(String args[]){
Base base = Method(new SubClass(), new SubClassTwo());
Serializable run = Method(new SubClass(), new SubClassTwo());
}
}
  • 注意上边返回值的写法
    ——[$]?通配符——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.susan.java.generic; 
import java.util.ArrayList;
import java.util.List;

/**
*问号通配符
**/
public class QuestionDemo {
private static void testMethod(List<? extends Number> list){}
public static void main(String args[]){
List<Object> oList = new ArrayList<Object>();
List<Integer> iList = new ArrayList<Integer>();
List<Number> nList = new ArrayList<Number>();
//testMethod(oList); //这里会出现编译错误
testMethod(iList);
testMethod(nList);
}
}
  • 从上边的的代码可以知道,?一般和extends以及super关键字进行使用,其含义在于传入泛型的类型定义为extends后边的类型的子类,所以上边的注释掉的代码是没有办法通过编译的。
    【*:泛型真正在开发过程程序员需要掌握的是用法,上边讲到的四点都比较深入,而且都是讨论的与JVM处理泛型原理相关的内容,如果没有弄懂没有关系,下边讲“泛型类型捕获”的时候主要提及应用层的相关内容。】
    ——[$]定义一个泛型接口——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package org.susan.java.generic; 

interface MinMan<T extends Comparable<T>>{

T min();
T max();

}

class MyDemo<T extends Comparable<T>> implements MinMan<T>{

T[] tValues;
MyDemo(T[] o){ tValues = o;}

public T min(){

T vT = tValues[0];

for( int i = 1; i < tValues.length; i++ )
if(tValues[i].compareTo(vT) < 0)
vT = tValues[i];
return vT;
}

public T max(){

T vT = tValues[0];

for( int i = 1; i < tValues.length; i++ )
if(tValues[i].compareTo(vT) > 0)
vT = tValues[i];
return vT;
}
}

public class GenericInterface {

public static void main(String args[]){
Integer inums[] = {3,6,13,11,45,22,33,21};
MyDemo<Integer> iob = new MyDemo<Integer>(inums);
System.out.println("iob Max:" + iob.max());
System.out.println("iob Min:" + iob.min());
}
}
  • 该输出为:
    iob Max:45
    iob Min:3
    ——[$]泛型的方法重载——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.susan.java.generic; 

/**
*一段错误的代码段
**/

class MyGenDemo<T,V>{
T ob1;
V ob2;

void set(T o){
this.ob1 = o;
}

void set(V o){
this.ob2 = o;
}
}

class GenDemo<T>{
T ob;

GenDemo(){
this.ob = new T();
}
}

class Wrong<T>{
static T ob;

static T getOb(){
return ob;
}
}
  • 分析上边的代码段就可以发现很多问题:

    • 首先set方法并不能通过这种方式重载,原因很简单,虽然这里使用了通配符T、V,但是这两个“外露” 类型在编译器里面为默认为相同的,因为这种情况下两个都属于占位符类型,按照这样的方式就会使得set方法的方法签名对JVM而言是一模一样的
    • this.ob = new T() 有问题,因为通配符是不能通过这种方式构造的,通配符目前还不能成为Java类型,所以通配符目前是不能实例化的,哪怕是一个类类型。
    • 最后一段代码问题也很严重:因为通配符属于非静态类型,所以不能使用在static的方法或者类定义里面,这就是上边代码段的代码。
      ——[$]泛型Java 类——
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.susan.java.generic; 
class GenType<T>{

T ob;

GenType(T ob){
this.ob = ob;
}

T getOb(){
return this.ob;
}
}

class GenStr<T extends Number>{

T str;

GenStr(T o){
this.str = o;
}

T getStr(){
return this.str;
}
}

public class GenTypeDemo {

public static void main(String args[]){
GenType<Integer> iObGenType = new GenType<Integer>(99);
GenStr<Float> fOb = new GenStr<Float>(102.2F);
System.out.println(iObGenType.getClass().getName());
System.out.println(fOb.getClass().getName());
}
}
  • 根据输出结果可以知道该类型就为我们定义的类型:
    org.susan.java.generic.GenType
    org.susan.java.generic.GenStr

3)泛型“类型捕获”

协变概念【extends 和super】

讲了这么多泛型的内容,相信读者对泛型有一点点了解了,接下来针对泛型里面比较费解的地方进行比较通俗的讲解,泛型的类型捕获是从编译器的级别来说的,当我们定义了泛型的时候,如果使用了? extends T这 种格式,编译器遇到一个这样带有通配符的变量的时候,它如何来进行识别操作呢,它会觉得遇到T了过后,一定会有T定义的变量,而对这些T而言一定有一个 Class类型。但是它不知道T代表什么,但是JVM会为T定制一个占位符来代替T的类型。占位符被称为特殊通配符的捕获。这种情况 下,编译器会为通配符提供一个名字,每个变量声明中出现的一个通配符都会活得JVM的一个捕获,,因此在泛型声明中如果用了public void method(Pointer pointer)的话,JVM就会获取两个通配符名称,因为这个时候?也好,T也好,类型是未知的,任意未知的类型的参数在使用的时候相互之间是没有任何关系的。
【*:泛型的通配符的使用其实在JDK 1.5无疑是最复杂的部分,而且Java编译器产生的一些令人困惑的错误以及很多消息都可能和通配符有关。】
? extends Number:这种语法的意思就是传入的泛型的“外露”类型必须是Number的子类,这里重复一下“外露类型”,其实在遇到泛型的时候往往会遇到两个类型,直接定义的类型,一般为Type格式的,然后是外露类型T,这里T就代表了外露类型,这里需要区分这种情况下Type和Type不属于同一个类型。这里需要了解的是泛型的“协变”。
注意:泛型本身不是协变的
比 如有类型Integer和Number,因为Integer类是Number类的子类,也就是说Integer是一个Number,Integer[]也 是一个Number[],按照这样的逻辑,我们也许会觉得泛型也是遵循了这样原理List也会是 List的子类,但是这种做法在泛型里面是错误的。这种做法在传入的时候会被编译器定义为类型错误。看一段程序:
——[$]协变的概念代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.susan.java.generic; 
import java.util.ArrayList;
import java.util.List;
/**
*关于泛型协变的概念代码
**/
public class ChangeGenerice {
private static void methodOne(List<Number> number){
System.out.println("One:" + number);
}

private static void methodTwo(List<Integer> integer){
System.out.println("Two:" + integer);
}

private static void methodThree(List<? extends Number> number){

System.out.println("Three:" + number);

}

private static void methodFour(List<? super Number> integer){
System.out.println("Four:" + integer);
}

public static void main(String args[]){
List<Number> nList = new ArrayList<Number>();
List<Integer> iList = new ArrayList<Integer>();
List<Object> oList = new ArrayList<Object>();
methodOne(nList);
//这里编译报错,因为nList是List<Number>,但是方法接受参数是 List<Integer>,它们不存在继承关系,这里也证明了协变的简单
//methodTwo(nList);
methodThree(nList);
methodFour(nList);
//这里编译报错,iList是List<Integer>,不是 List<Number>
//methodOne(iList);
methodTwo(iList);
methodThree(iList);
//这里编译报错,iList不满足条件List<? super Number>,因为List<Integer>中外露类型Integer不满足Integer super Number
//methodFour(iList);
//最后三个编译错误留给读者自己去分析
//methodOne(oList);
//methodTwo(oList);
//methodThree(oList);
methodFour(oList);
}
}

如果需要在1.5的JDK环境里面进行1.4的写法,如果不去掉类型检测可能会报警告:
【*:上边这段概念代码很好说明了协变过程,同样也能够很好理解泛型里面的extends和super关键字的使用了。关于通配符的使用可能还需要写更多的代码,最好办法是直接去参考集合部分的源代码,是一个不错的学习途径。】
针对Java泛型做一个简单的总结:
* 泛型的“外露”类型可以用在类、接口和方法中,一般称为泛型类、泛型接口、泛型方法
* 泛型的“外露”类型只可以是类类型(包括自定义类),不能是简单类型
* 泛型的“外露”类型可以使用extends和super关键字,该关键字标识了“外露”类型的界限
* 泛型不支持直接的类型协变,在协变过程注意泛型的“外露”类型满足的界限的条件
* 泛型还可以使用通配符进行操作,这种情况可以用来作为类模版 推荐