浅析Java泛型

什么是泛型?

泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操做的数据类型被指定为一个参数,在用到的时候在指定具体的类型。这种参数类型能够用在类、接口和方法的建立中,分别称为 泛型类泛型接口泛型方法

基本术语介绍html

以ArrayList<E>和ArrayList<Integer>为例
整个ArrayList<E>称为泛型类型
ArrayList<E>中的E称为类型变量或者类型形参
整个ArrayList<Integer>称为参数化的类型
ArrayList<Integer>中的Integer称为类型参数的实例或者类型实参
ArrayList<Integer>中的<Integer>念为typeof Integer
ArrayList称为原始类型

为何使用泛型?

泛型使类型(类和接口)在定义类、接口和方法时成为参数,好处在于:java

  • 强化类型安全,因为泛型在编译期进行类型检查,从而保证类型安全,减小运行期的类型转换异常。
  • 提升代码复用,泛型能减小重复逻辑,编写更简洁的代码。
  • 类型依赖关系更加明确,接口定义更加优好,加强了代码和文档的易读性。

一个简单的例子数组

public class Test1 {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("kiwen1");
        list.add("kiwen2");
        list.add(123);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); 
            System.out.println("name:" + name);
        }
        
    }
}
//输出结果
name:kiwen1
name:kiwen2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at DateTest.Test1.main(Test.java:17)

从上面例子能够看出,定义了一个List类型的集合,向其中加入了两个字符串类型的值和 一个Integer类型的值。此时list默认的类型为Object类型。但这里有两个问题,在循环中,一是当获取一个值时必须进行强制类型转换,二是没有错误检查。因为定义了name为String类型,运行时将Integer转成String会产生错误。即编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。所以,致使此类错误编码过程当中不易被发现。安全

public class Test2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("kiwen1");
        list.add("kiwen2");
        //list.add(123);   //提示编译错误

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i);
            System.out.println("name:" + name);
        }
    }
}
//输出结果
name:kiwen1
name:kiwen2

该段代码采用泛型写法后,向list加入一个Integer类型的对象时会出现编译错误,经过List<String>,直接限定了list集合中只能含有String类型的元素,从而在输出时处无须进行强制类型转换。由于此时,集合可以记住元素的类型信息,编译器已经可以确认它是String类型了。app

经过上面的例子能够证实,在编译以后程序会采起去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程当中,正确检验泛型结果后,会将泛型的相关信息擦出,而且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以当作是多个不一样的类型,实际上都是相同的基本类型。dom

泛型类

在类的申明时指定参数,即构成了泛型类。泛型类的类型参数部分能够有一个或多个类型参数,它们之间用逗号分隔。这些类称为参数化类或参数化类型,由于它们接受一个或多个参数。ide

定义一个简单的泛型类

//在实例化泛型类时,必须指定T的具体类型
public class Test<T>{
    //在类中声明的泛型整个类里面均可以用,除了静态部分,由于泛型是实例化时声明的。
    //静态区域的代码在编译时就已经肯定,只与类相关
    class A <E>{
        T t;
    }
    //类里面的方法或类中再次声明同名泛型是容许的,而且该泛型会覆盖掉父类的同名泛型T
    class B <T>{
        T t;
    }
    //静态内部类也可使用泛型,实例化时赋予泛型实际类型
    static class C <T> {
        T t;
    }
    public static void main(String[] args) {
        //报错,不能使用T泛型,由于泛型T属于实例不属于类
//        T t = null;
    }

    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Test(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

在使用泛型的时候若是传入泛型实参,则会根据传入的泛型实参作相应的限制,此时泛型才会起到本应起到的限制做用,可是,泛型的类型参数只能是类类型,不能是简单类型。若是不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型能够为任何的类型。换句话说,泛型类能够当作普通类的工厂。函数

//不传入泛型类型实参
List list = new List();
list.add(123);
list.add("hello");
//传入的泛型实参
List list<String> = new List<String>();
list.add("hello");

如何继承一个泛型类

若是不传入具体的类型,则子类也须要指定类型参数,ui

class Son<T> extends Test<T>{}

若是传入具体参数,则子类不须要指定类型参数this

class Son extends Test<String>{}

泛型接口

泛型接口与泛型类的定义基本一致

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

如何实现一个泛型接口

一个简单的例子

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然咱们只建立了一个泛型接口Generator<T>
 * 可是咱们能够为T传入无数个实参,造成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则全部使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */

public class FruitGenerator implements Generator<String> {
    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

泛型通配符

咱们知道,Box<Number>Box<Integer>实际上都是Box类型,如今须要继续探讨一个问题,那么在逻辑上,相似于Box<Number>Box<Integer>是否能够当作具备父子关系的泛型类型呢?

为了弄清楚这个问题,咱们使用Box这个泛型类继续看下面的例子:

package DateTest;
public class GenericTest {
    class Box<T> {
        private T data;
        public Box() {
        }
        public Box(T data) {
            this.data = data;
        }
        public T getData() {
            return data;
        }
        public void setData(T data) {
        this.data = data;
        }
    }
    public static void main(String[] args) {
        Box<Number> name = new Box<Number>(99);
        Box<Integer> age = new Box<Integer>(712);
        getData(name);
        //The method getData(Box<Number>) in the type GenericTest is 
        //not applicable for the arguments (Box<Integer>)
        getData(age);   // 1
    }
    public static void getData(Box<Number> data){
        System.out.println("data :" + data.getData());
    }
}

经过提示信息咱们能够看到Box<Number>不能被看做为Box<Integer>的子类。由此能够看出:同一种泛型能够对应多个版本(由于参数类型是不肯定的),不一样版本的泛型类实例是不兼容的。

回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic类型的类,这显然与java中的多态理念相违背。所以咱们须要一个在逻辑上能够表示同时是Box<Integer>Box<Number>的父类的引用类型。由此类型通配符应运而生。

咱们能够将上面的方法改一下:

public static void getData(Box<?> data) {
    System.out.println("data :" + data.getData());
}

类型通配符通常是使用?代替具体的类型实参,注意, 此处的?和Number、String、Integer同样都是一种实际的类型,能够把?当作全部类型的父类。是一种真实的类型。

能够解决当具体类型不肯定的时候,这个通配符就是 ? ;当操做类型时,不须要使用类型的具体功能时,只使用Object类中的功能。那么能够用 ? 通配符来表未知类型。

泛型无限定通配符

无限定通配符使用<?>的格式,表明未知类型的泛型。 当可使用Object类中提供的功能或当代码独立于类型参数来实现方法时,这样的参数可使用任何对象。

public void showKeyValue1(List<?> list) {
    for (Object item : list) {  
        System.out.print(item + " ");   
    } 
}

泛型上限通配符

通配符上界使用<? extends T>的格式,意思是须要一个T类型或者T类型的子类,通常T类型都是一个具体的类型,例以下面的代码。

//只能传入number的子类或者number
public void showKeyValue2(List<? extends Number> list) {  
    for (Number number : list) {  
        System.out.print(number.intValue()+" ");   
    }  
}
//假如传入String类型,list.add("hello");会提示
//The method add(Number) in the type List<Number> is not applicable for the arguments (String)

不管传入的是何种类型的集合,咱们均可以使用其父类的方法统一处理。

泛型下限通配符

通配符下界使用<? super T>的格式,意思是须要一个T类型或者T类型的父类,通常T类型都是一个具体的类型,例以下面的代码。

//只能传入Integer的父类或者Integer
public void showKeyValue3(List<? super Integer> obj){
    System.out.println(obj);
}
//假如传入String类型,list.add("hello");会提示
//The method add(Number) in the type List<Number> is not applicable for the arguments (String)

泛型方法

在java中,泛型类的定义很是简单,可是泛型方法就比较复杂了。
尤为是咱们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中很是容易将泛型方法理解错了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。

定义泛型方法以下

图片描述

调用泛型方法以下

图片描述

定义泛型方法时,必须在返回值前边加一个 <T>,来声明这是一个泛型方法,持有一个泛型T,而后才能够用泛型T做为方法的返回值。
Class<T>的做用就是指明泛型的具体类型,而 Class<T>类型的变量c,能够用来建立泛型类的对象。
为何要用变量c来建立对象呢?既然是泛型方法,就表明着咱们不知道具体的类型是什么,也不知道构造方法如何,所以没有办法去new一个对象,但能够利用变量c的 newInstance方法去建立对象,也就是利用反射建立对象。
泛型方法要求的参数是 Class<T>类型,而 Class.forName()方法的返回值也是 Class<T>,所以能够用 Class.forName()做为参数。其中, forName()方法中的参数是何种类型,返回的 Class<T>就是何种类型。在本例中, forName()方法中传入的是User类的完整路径,所以返回的是 Class<User>类型的对象,所以调用泛型方法时,变量c的类型就是 Class<User>,所以泛型方法中的泛型T就被指明为User,所以变量obj的类型为User。
/**
 * 泛型方法的基本介绍
 * 说明:
 *     1)public 与 返回值中间<T>很是重要,能够理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并非泛型方法。
 *     3)<T>代表该方法将使用泛型类型T,此时才能够在方法中使用泛型类型T。
 *     4)与泛型类的定义同样,此处T能够随便写为任意标识,常见的如T、E、K、V等形式的参数经常使用于表示泛型。
 */
public class Generic<T> {
    public T name;
    public  Generic(){}
    public Generic(T param){
        name=param;
    }
    public T m(){
        return name;
    }
    public <E> void m1(E e){ }
    public <T> T m2(T e){ }
}

上面代码中,m()方法不是泛型方法,m1()和m2()都是泛型方法。

泛型方法与可变参数

public class Test{
    @Test
    public void test () {
        printMsg("hello1",1,"hello2",2.0,false);
        print("hello1","hello2", "hello3");
    }
    //普通可变参数只能适配一种类型
    public void print(String ... args) {
        for(String t : args){
            System.out.println(t);
        }
    }
    //泛型的可变参数能够匹配全部类型的参数。。有点无敌
    public <T> void printMsg( T... args){
        for(T t : args){
            System.out.println(t);
        }
    }
}

静态方法与泛型

若是在类中定义使用泛型的静态方法,须要添加额外的泛型声明(将这个方法定义成泛型方法)。即便静态方法要使用泛型类中已经声明过的泛型也不能够。

public class Test<T> {
    private static T num;//此时编译器会提示错误信息
    public static void test(T t){//此时编译器会提示错误信息:
        ...
        //Cannot make a static reference to the non-static type T
    }
}

由于静态方法和静态变量属于类全部,而泛型类中的泛型参数的实例化是在建立泛型类型对象时指定的,因此若是不建立对象,根本没法肯定参数类型。可是静态泛型方法是可使用的,咱们前面说过,泛型方法里面的那个类型和泛型类那个类型彻底是两回事。

public class StaticGenerator<T> {
    public static <T> void show(T t){
    }
}

泛型的限制

Java泛型不能使用原始类型

使用泛型,原始类型不能做为类型参数传递。例如

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

若是将int原始类型传递给Test类,那么编译器会报错。为了不这种状况,须要传递Integer对象而不是int原始类型。

Java泛型不能使用实例

类型参数不能用于在方法中实例化其对象。例如

public static <T> void show(Test<T> test) {
   //compiler error
   //Cannot instantiate the type T
   //T item = new T();  
   //test.add(item);
}

若是须要实现这样的功能,可使用反射。

public static <T> void show(Test<T> test, Class<T> clazz) 
   throws InstantiationException, IllegalAccessException{
   T item = clazz.newInstance();   // OK
   test.add(item);
   System.out.println("Item showed.");
}

Java泛型不能使用静态域

使用泛型时,类型参数不容许为静态(static)。因为静态变量在对象之间共享,所以编译器没法肯定要使用的类型。若是容许静态类型参数。

Java泛型不能转换类型

除非由无界通配符进行参数化,不然不容许转换为参数化类型。

Test<Integer> integerTest = new Test<Integer>();
Test<Number> numberTest = new Test<Number>();
//Compiler Error: Cannot cast from Test<Number> to Test<Integer>,下面用法是错误的
integerTest = (Test<Integer>)numberTest;
//可使用无界通配符进行转换功能
private static void add(Test<?> test){
   Test<Integer> integerTest = (Test<Integer>)test;
}

Java泛型instanceof运算符

由于编译器使用类型擦除,运行时不会跟踪类型参数,因此在Test <Integer>Test <String>之间的运行时差别没法使用instanceOf运算符进行验证。

Java泛型不能使用异常

泛型类不容许直接或间接扩展Throwable类。

//The generic class Test<T> may not subclass java.lang.Throwable
class Test<T> extends Exception {}
//The generic class Test1<T> may not subclass java.lang.Throwable
class Test1<T> extends Throwable {}

在一个方法中,不容许捕获一个类型参数的实例,但throws子句中容许使用类型参数。

public static <T extends Exception, J> 
   void execute(List<J> jobs) {
      try {
         for (J job : jobs){}

         // compile-time error
         //Cannot use the type parameter T in a catch block
      } catch (T e) { 
         // ...
   }
}

//
class Test<T extends Exception>  {
   private int t;

   public void add(int t) throws T {
      this.t = t;
   }
   public int get() {
      return t;
   }   
}

Java泛型不能使用数组

错误代码

//Cannot create a generic array of Test<Integer>
Test<Integer>[] arrayOfLists = new Test<Integer>[2];

由于编译器使用类型擦除,类型参数被替换为Object,用户能够向数组添加任何类型的对象。但在运行时,代码将没法抛出ArrayStoreException

下面使用Sun的一篇文档的一个例子来讲明这个问题:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种状况下,因为JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,因此能够给oa[1]赋上一个ArrayList而不会出现异常,可是在取出数据的时候却要作一次类型转换,因此就会出现ClassCastException,若是能够进行泛型数组的声明,上面说的这种状况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。

而对泛型数组的声明进行限制,对于这样的状况,能够在编译期提示代码有类型安全问题,比没有任何提示要强不少。
下面采用通配符的方式是被容许的:数组的类型不能够是类型变量,除非是采用通配符的方式,由于对于通配符的方式,最后取出数据是要作显式的类型转换的。

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

Java泛型不能重载

一个类不容许有两个重载方法,能够在类型擦除后使用相同的签名。

类型擦除

Java编译器应用类型擦除。 类型擦除是指编译器使用实际的类或桥接方法替换泛型参数的过程。 在类型擦除中,编译器确保不会建立额外的类,而且没有运行时开销。

Java编译器编译泛型的步骤:

  1. 检查泛型的类型 ,得到目标类型
  2. 擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换)
  3. 调用相关函数,并将结果强制转换为目标类型。
ArrayList<String> arrayString=new ArrayList<String>();     
 ArrayList<Integer> arrayInteger=new ArrayList<Integer>();     
 System.out.println(arrayString.getClass()==arrayInteger.getClass());

上面代码输入结果为 true,可见经过运行时获取的类信息是彻底一致的,泛型类型被擦除了!

如何擦除:
当擦除泛型类型后,留下的就只有原始类型了,例如上面的代码,原始类型就是ArrayList。擦除类型变量,并替换为限定类型(T为无限定的类型变量,用Object替换),以下所示

擦除以前:

//泛型类型  
class Pair<T> {    
    private T value;    
    public T getValue() {    
        return value;    
    }    
    public void setValue(T  value) {    
        this.value = value;    
    }    
}

擦除以后:

//原始类型  
class Pair {    
    private Object value;    
    public Object getValue() {    
        return value;    
    }    
    public void setValue(Object  value) {    
        this.value = value;    
    }    
}

由于在Pair<T>中,T是一个无限定的类型变量,因此用Object替换。若是是Pair<T extends Number>,擦除后,类型变量用Number类型替换。

若是要死磕Java泛型内部原理,请参考文章泛型的内部原理:类型擦除以及类型擦除带来的问题Java泛型深刻了解

总结

在使用泛型类时,因为 Java 泛型的类型参数之实际类型在编译时会被消除,因此没法在运行时得知其类型参数的类型。虽然传入了不一样的泛型实参,但并无真正生成不一样的类型,传入不一样泛型实参的泛型类在内存上实际只有一个,但在逻辑上,咱们能够理解为多个不一样的泛型类型。

参考文章

https://blog.csdn.net/s10461/...

https://www.cnblogs.com/lwbqq...

https://www.cnblogs.com/iyang...

相关文章
相关标签/搜索