Java开发对泛型的认识和理解

转载请注明出处:http://blog.csdn.net/li0978/article/details/55193150

针对泛型大家并不陌生,主要是针对某些对象以及方法参数的限定,避免在代码编写的过程中由于类型的不匹配从而造成运行期发生异常,泛型只在编译阶段有用,运行期间则会进行泛型擦除。

什么是泛型

泛型即“参数化类型”,目的就是将参数类型像参数一样引入到类、方法、接口中来,进而来统一内部使用时的参数类型,JDK1.5引入的。
举个栗子

List list = new ArrayList<>();
list.add("test");
list.add(1);
for (int i = 0; i < list.size(); i++) {
    String name = (String) list.get(i); // 取出Integer时,运行时出现异常
    System.out.println("name:" + name);
}

以上异常的原因正是因为类型不匹配造成的,因此我们在定义的时候规定一种类型:

List<String> list = new ArrayList<String>();

这样在编译的时候就不允许使用添加integer类型了,取的时候也就大胆放心的去取了,这就是泛型的目的。

泛型给我们带来了不一样的编程体验,平时在使用泛型的时候不知道具体的泛型参数是什么,那么我们可以这样定义:

public class Cache<T> {
    T value;

    public Object getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

}

使用的时候我们只需将确定的泛型参数传进去即可:

Cache<String> cache1 = new Cache<String>();
cache1.setValue("123");
String value2 = cache1.getValue();

Cache<Integer> cache2 = new Cache<Integer>();
cache2.setValue(456);
int value3 = cache2.getValue();

尖括号 中的T被称作是类型参数,用于指代任何类型。出于规范的目的,Java建议我们用单个大写字母来代表类型参数。常见的如:

  1. T 代表一般的任何类。
  2. E 代表 Element 的意思,或者 Exception 异常的意思。
  3. K 代表 Key 的意思。
  4. V 代表 Value 的意思,通常与 K 一起配合使用。
  5. S 代表 Subtype 的意思,文章后面部分会讲解示意。

总结了一下泛型的优点:
(1)软件编程扩展
与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
(2)类型安全
通过知道使用泛型定义的变量的类型限制,编译器可以更有效地提高Java程序的类型安全。
(3)消除强制类型转换
消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。所有的强制转换都是自动和隐式的。
(4)提高性能
泛型使得在参数调用的时候事先判断了参数是否符合标准,避免了在运行时数据调用不规范所进行的一系列处理,保证程序稳定快速运行。

泛型的使用

在开发过程中泛型按照使用的情况大致分为三种:

  1. 泛型接口
  2. 泛型类
  3. 泛型方法

泛型接口

1.知道泛型参数的情况下。
具体泛型参数类:

public class Call {
    public Call(){
        System.out.println("this is call");
    }
}

定义接口:

public interface GenericInterface<T> {
    T next();
}

接口实现类:

public class GenericClass implements GenericInterface<Call> {
    public Call next() {
        Call c = new Call();
        return c;
    }
}

主函数功能实现:

public static void main(String[] arg){
    GenericClass genericClass = new GenericClass();
    genericClass.next();
}

运行结果:
this is call

2.大部分情况下是事先并不只泛型参数具体类型,此时需要泛型类来确定泛型接口中具体传入的参数类型。
定义接口:

public interface GenericInterface<T> {
    T next(T t);
}

接口实现类:

public class GenericClass<T> implements GenericInterface<T> {
    public T next(T t) {
        return t;
    }
}

主函数功能实现:

public static void main(String[] arg){
    GenericClass<Call> genericClass = new GenericClass<Call>();
    genericClass.next(new Call());
}

运行结果:
this is call

泛型类

和泛型接口定义一样:

public class Test<T> {
    T value;

    public T getValue(){
        return value;
    }
}

使用的时候将泛型参数传进去:

Test<String> test = new Test<>();

当然泛型参数也可以不止一个:

public class MultiType <E,T>{
    E value1;
    T value2;

    public E getValue1(){
        return value1;
    }

    public T getValue2(){
        return value2;
    }
}

泛型方法

这里的泛型方法特指由方法本身来确定将来传入的参数类型而非像上边引用泛型接口或泛型类的泛型参数。泛型方法可单独对方法定义方法的形参类型和方法的返回类型。
定义方法的形参类型:

public class Test {
    public <T> void testMethod(T t){

    }
}

定义方法的返回类型:

public class Test {
    public <T> T  testMethod(T t){
        return t;
    }
}

泛型方法与泛型类共存

public class Test<T>{

    public  void testMethod1(T t){
        System.out.println(t.getClass().getName());
    }
    public  <E> E testMethod2(E e){
        return e;
    }
}

这里testMethod1和testMethod2两个方法所接收的参数类型是不一样的。testMethod1属于普通方法,其参数来自于类传入的参数类型决定,testMethod2由将来自身方法的传入的参数来决定参数类型,这里为了代码的方便阅读和避免混淆所以采用T和E来进行区分。

从上边的一路走来可以看出无论是泛型接口还是泛型类或是泛型方法其将来的传入参数均来源于所在的位置,这里的是对T的声明定义。

通配符 ?

是通配符,目的希望泛型能够处理某一范围内的数据类型,也就是说让泛型所表示的参数类型更广一些从而满足泛型限定的对象之间能够正常匹配。

< ? > 表示类型的无界通配符,意思是目前并不知道参数类型是什么,是对类型的无界限定,使得参数范围更广阔。
例如:

List<?> list = new ArrayList<String>();

注意List< ? >和List是有区别的,就拿上句代码来说表示的意思是“生成一个集合对象只能放String类型,至于这个集合类型却是未知的”,这也是使用无界通配符不能往里面存任何类型数据 的原因,只能存null。而List list = new ArrayList<String>();表示的意思是“生成一个集合对象只能存放String类型,这个集合本身所限定的范围却是任意都可以”,看出来两者的区别了吧

List<Child> lsc = new ArrayList<Child>();
List<Parent> lsp = new ArrayList<Parent>();
lsp = lsc;  //编译器报错不通过

List lsc = new ArrayList<Child>();
List<Parent> lsp = new ArrayList<Parent>();
lsp = lsc;  //编译器可以运行

另外,通配符不能作为“参数类型”使用:

//error
class Test<?>{
    private ? item1;
    private ? item2;
}

通配符的边界
通配符常用的就是对边界的限定,有界通配符分为两种:

  • < ? extends T >:是指 “上界通配符(Upper Bounds Wildcards)”,限定的是所有的子类及其本身类型。
  • < ? super T >:是指 “下界通配符(Lower Bounds Wildcards)”,限定的是所有的父类及其本身类型。

通配符的边界限定能更好更准确的控制参数类型,便于数据顺利的传入和取出。
举个栗子:
这里有一个Fruit类和他的派生类Apple类

class Fruit {}
class Apple extends Fruit {}

接着我们设定一个容器,并设定他有存取功能

class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}

我们现在有一个想法就是生成一个用来装“苹果”的“水果盘子”,想像是美好的,现实却是不允许的。

Plate<Fruit> p=new Plate<Apple>(new Apple());    // error

编译器的想法是:苹果属于水果。但是装苹果的盘子却不属于装水果的盘子,其实想想也在理,这之间有一个关系存在,前者属于继承关系,后者仅仅是参数上存在着联系,对于Plate在没有一个参数限定的条件下是不能随意进行等价的。这就是通配符要有一个上下边界的范围限定的原因。

< ? extends T >

上界通配符,指的是所有的子类及其本身类型。还是上边的例子变换成Plate< ? extends Fruit >意思就可以是装所有水果的盘子,不一定是哪种水果,有可能是Fruit本身也有可能就是Apple.

Plate<?extends Fruit> p=new Plate<Apple>(new Apple());

这样就说的过去了,总有一个是属于我(苹果)的。接下来就可以调用盘子里的方法了。但是并不是所有的方法都能调用,设定上届通配符可以取数据但是不允许存入数据的,除了null。编译器看到Plate< ? extends Fruit>只知道盘子里将来放的是Fruit或者他的孩子,而具体是什么并不知道,所以编译器在这里仅仅给以一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,然后无论是想往里插入Apple或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。另外,读取数据时也只能存放在Fruit或者Fruit基类中,因为不能确定到底是哪种类型,否则不得不进行类型判断或者强转了。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
//不能存入任何元素
p.set(new Fruit());    //Error
p.set(new Apple());    //Error
p.set(null);    
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get();    //Error

< ? super T >

下届通配符,指的是Fruit本身以及所有Fruit的基类,有可能是Foot或者Object。但是要明白一点Plate< ? super Fruit >的基类不是Plate< Apple >的基类。

Plate<? super Fruit> p=new Plate<Apple>(new Apple());  //Error

使用下界< ? super Fruit >会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里,还是因为不能确定到底是哪种类型,只能基类来接收。set( )方法正常。

Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get();    //Error
Fruit newFruit1=p.get();    //Error
Object newFruit2=p.get();

extends、super选取

1.频繁往外读取内容的,适合用上界extends。
2.经常往里插入的,适合用下界super。

类型擦除

泛型只在编译阶段有用,在进入JVM之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。

java1.5版本之前是没有泛型的,1.5版本引入泛型概念,为了和之前的版本能够很好兼容,这里采用了类型擦数的方式。

看下面的代码:

List<String> a = new ArrayList<String>();  
List b = new ArrayList();  
Class c1 = a.getClass();  
Class c2 = b.getClass();  
System.out.println(c1 == c2); //true  

上面程序的输出结果为true。所有反射的操作都是在运行时的,既然为true,就证明了编译之后,程序会采取去泛型化的措施,也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

上述结论可通过下面反射的例子来印证:

ArrayList<String> a = new ArrayList<String>();  
a.add("CSDN");  
Class c = a.getClass();  
try{  
    Method method = c.getMethod("add",Object.class);  
    method.invoke(a,100);  
    System.out.println(a);  
}catch(Exception e){  
    e.printStackTrace();  
}  

因为绕过了编译阶段也就绕过了泛型,输出结果为:
[CSDN, 100]

泛型擦除规则

并不是所有的泛型参数类型擦除后都是Object类型,类型擦除是有规则的:

泛型擦除就是类型变量用第一个限定来替换,如果没有给定限定就用Object替换,例如类Pair中的类型变量没有限定所以用Object替换。

看个例子:

public class Interval<T extends Comparable & Serializable> {
    private T lower;
    private T upper;

    public Interval(T first, T second) {
        if (first.compareTo(second) > 0){
            lower = second;
            upper = first;
        }else{
            lower = first;
            upper = second;
        }
    }
}

擦除后的原始类:

public class Interval{
  private Comparable lower;
  private Comparable upper;

  public Interval(Comparable first, Comparable second) {

  }
}

此时把限定改为Interval

翻译泛型方法

你可能会有疑问,泛型擦除后非限定的类型转换成Object,编译前进行引用时并没有进行类型转换,还能够正常运行,这是怎么回事呢?

Pair<Manager> pair = ...;
Manager manager = pair.getFirst();

这里引入”翻译泛型表达式”概念:在程序调用泛型方法的时候,如果返回值被擦除,编译器会插入强制的类型转换。
擦除后getFirst返回值为Object,编译器会自动插入Manager强制类型转换,所以getFirst()方法会执行如下两个指令:

  • 对原始方法调用getFirst()
  • 把返回值Object强转成Manager

类型擦除带来的问题

因为种种原因,Java不能实现真正的泛型,只能使用类型擦除来实现伪泛型,这样虽然不会有类型膨胀的问题,但是也引起了许多新的问题。所以,Sun对这些问题作出了许多限制,避免我们犯各种错误。

1.类型擦除与多态冲突
假设有一个超类:

public class Parent<T>
{
    public void sayHello(T value)
    {
        System.out.println("This is Parent Class, value is " + value);
    }
}

以及一个子类:

public class Child extends Parent<String>
{
    public void sayHello(String value)
    {
        System.out.println("This is Child class, value is " + value);
    }
}

最后有以下测试代码,企图实现多态:

public class MainApp
{
    public static void main(String[] args)
    {
        Child child = new Child();
        Parent<String> parent = child;

        parent.sayHello("This is a string");
    }
}

按照泛型类型擦除原则,父类中是sayHello(Object value),子类中还是sayHello(String value),这两个根本不是重写关系,而是重载,这不是多态的形式,所以你会感觉冲突了吧,但是结果是可以正常运行,这是怎么回事?

原因是编译器在Child类中自动生成了一个桥方法

public void sayHello(Object value)
{
    sayHello((String) value);
}

可以看出,这个桥方法实际上就是对超类中sayHello(Obejct)的重写。这样做的原因是,当程序员在子类中写下以下这段代码的时候,本意是对超类中的同名方法进行重写,但因为超类发生了类型擦除,所以实际上并没有重写成功,因此加入了桥方法的机制来避免类型擦除与多态发生冲突。

public class Child extends Parent<String>
{
    public void sayHello(String value)
    {
        System.out.println("This is Child class, value is " + value);
    }
}

桥方法并不需要自己手动生成,一切都是编译器自动完成的。

2.泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArrayList,只有ArrayList。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

3.使用泛型不能在运行时进行类型匹配查询
例如:若定义ArrayList<String> arrayList=new ArrayList<String>();则不能使用if( arrayList instanceof ArrayList<String>),此时运行时会报错。因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。

4.不能抛出也不能捕获泛型类的对象
例如下面的定义将不会通过编译:

public class Problem<T> extends Exception{......} 

假如上述代码成立,则:

try{  
}catch(Problem<Integer> e1){  
。。  
}catch(Problem<Number> e2){  
...  
}   

根据类型擦除性质,泛型类型擦除后两个catch都变为原始类型Object类型,也就是说两个catch一模一样,这不符合常规,当然不能通过。

不能在catch条件中使用泛型变量:

public static <T extends Throwable> void doWork(Class<T> t){  
    try{  
        ...  
    }catch(T e){ //编译错误  
        ...  
    }catch(IndexOutOfBounds e){  
    }                           
}  

根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。

5.泛型类型不能直接实例化
不能实例化泛型类型:

T first = new T(); //ERROR

类型擦除会使这个操作做成new Object()。

不能实例化数组

public<T> T[] minMax(T[] a){  
    T[] mm = new T[2]; //ERROR  
    ...  
}  

擦除会使这个方法总是构靠一个Object[2]数组

6.条件不能与擦除后的原型类产生冲突

class Pair<T>   {  
    public boolean equals(T value) {  
        return null;  
    }  
}  

考虑一个Pair。从概念上,它有两个equals方法:
booleanequals(String); //在Pair中定义
boolean equals(Object); //从object中继承
擦除后方法变成了:
boolean equals(Object)
这与Object.equals方法发生了冲突,补救的办法就是重新命名引发错误的方法。

7.泛型在静态方法和静态类中的问题
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。
例如:

public class Test2<T> {    
    public static T one;   //编译错误    
    public static  T show(T one){ //编译错误    
        return null;    
    }    
}    

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

静态泛型方法是可以使用泛型参数的:

public class Test2<T> {    
    public static <T >T show(T one){//这是正确的    
        return null;    
    }    
}    

因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T。

对于泛型擦除带来的问题,我这这里仅仅举例出一部分典型的认为容易出错和犯迷糊的点,感谢大牛的充分总结,更多请转移参考。

猜你喜欢

转载自blog.csdn.net/li0978/article/details/55193150