查看内容

泛型的理解及应用(一):泛型擦除

  • 2020-01-12 14:01
  • 新浦京编程
  • Views

新浦京澳门娱乐,“编译器会进行泛型擦除”是一个常识了(好吧,实际擦除的是参数和自变量的类型)。这个过程由新浦京棋牌手机版下载,“类型擦除”实现。但是并非像许多开发者认为的那样,在 <..> 符号内的东西都被擦除了。看下面这段代码:

  在笔者工作过程中,大略地知道Java在泛型设计上是一种“伪泛型”,存在着泛型擦除。在使用Gson编写工具类之前,我一直错误地认为:泛型的擦除就是把泛型内的实参全部替换成Object或者直接消灭泛型实参后生成Java的字节码文件。但我的工作笔记上面清楚地写着解决泛型擦除使用的两个方法:①在构造器内传递泛型相关类型 ②使用反射获取泛型实参

public class ClassTest {
  public static void main(String[] args) throws Exception {
    ParameterizedType type = (ParameterizedType) 
       Bar.class.getGenericSuperclass();
    System.out.println(type.getActualTypeArguments()[0]);

    ParameterizedType fieldType = (ParameterizedType) 
        Foo.class.getField("children").getGenericType();
    System.out.println(fieldType.getActualTypeArguments()[0]);

    ParameterizedType paramType = (ParameterizedType) 
        Foo.class.getMethod("foo", List.class)
        .getGenericParameterTypes()[0];
    System.out.println(paramType.getActualTypeArguments()[0]);

    System.out.println(Foo.class.getTypeParameters()[0]
        .getBounds()[0]);
  }

  class Foo<E extends CharSequence> {
    public List<Bar> children = new ArrayList<Bar>();
    public List<StringBuilder> foo(List<String> foo) {return null; }
    public void bar(List<? extends String> param) {}
  }

  class Bar extends Foo<String> {}
}

  这一对比,擦除泛型实参生成字节码文件与使用反射获取泛型实参,这两个是矛盾的。如果能够使用反射获取出泛型实参,那么字节码文件肯定没有对泛型实参进行擦除。那泛型的擦除这块,虚拟机把泛型擦除了什么?在写这文章之前,一直把学到的东西推翻了再推翻,因此在这里写一些总结,免得以后自己再继续走一些弯路。

新浦京澳门官网,你知道输出了什么吗?

  首先编写一些泛型类以及泛型方法、泛型变量。通过反编译工具对class文件进行反编译,查看class里面泛型实参是否真被擦除了。

class java.lang.String
class ClassTest$Bar
class java.lang.String
class java.lang.StringBuilder
interface java.lang.CharSequence

  因此构思内容如下:

你会发现每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到。那么到底什么是“类型擦除”?至少某些东西被擦除了吧?是的。事实上,除了结构化信息外的所有东西都被擦除了 —— 这里结构化信息是指与类结构相关的信息,而不是与程序执行流程有关的。换言之,与类及其字段和方法的类型参数相关的元数据都会被保留下来,可以通过反射获取到。

  1.编写一个水果的类,分别用草莓、苹果、橙几个类去继承水果类

而其他的信息都被擦除掉了。例如下面这段代码:

  2.编写销售员泛型类,类内含有泛型方法、泛型字段,以及泛型继承子类

List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
   String s = it.next();
}

  3.编写测试代码,使用反射获取泛型类内的各项内容,查看是否能够获取相关的泛型

实际上会被转换成这个(这两段代码的字节码是一致的)

新浦京棋牌手机版下载 1新浦京棋牌手机版下载 2

List list = new ArrayList();
Iterator it = list.iterator();
while (it.hasNext()) {
   String s = (String) it.next();
}
package com.genric.domain;

/**
 * @Author: Travelsky_CLSUN
 * @Date: Created on 17-5-8.下午3:39
 * @Description: 水果类
 */
public class Fruit
{
    private String name;
    private String color;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public String getColor()
    {
        return color;
    }

    public void setColor(String color)
    {
        this.color = color;
    }

    public void showInfo()
    {
        System.out.println(this.name + ":" + this.color);
    }

}

public class Strawberry extends Fruit
{

}

public class Apple extends Fruit
{

}

public class Orange extends Fruit
{

}

因此,定义在方法体内的类型参数会被擦除,在必要的时候会有类型转换。另外,如果一个方法被定义为接受 List 参数,这个 T 会被转换成 Object (如果定义了类型的上界的话就转换成对应的类型。这也是你不能 new T() 的原因)。(顺便这里有个关于类型擦除的问题)

编写水果类及其四个子类

目前为止类型擦除定义中的前两点我们都讲完了。第三点是关于bridge方法,我已经在 stackoverflow 上的这个问题(和回答)中已经说明了。

新浦京棋牌手机版下载 3新浦京棋牌手机版下载 4

两个结论。第一,java 泛型是非常复杂的。但是不用完全理解这些细节也可以使用它们。

package com.genric.domain;

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

/**
 * @Author: Travelsky_CLSUN
 * @Date: Created on 17-5-8
 * @Description: 泛型销售员群体,该群体只卖水果
 */
public class Sellers<T extends Fruit>
{
    // 使用属性记录售卖某种水果的最佳日期
    public List<Strawberry> BerryDateList = new ArrayList<Strawberry>();
    public List<Apple> AppleDateList = new ArrayList<Apple>();
    public List<Orange> OrangeDateList = new ArrayList<Orange>();

    // 获取具体的水果品种售卖时间列表
    public List<Apple> getAppleList(List<Apple> eList)
    {

ArrayList<Apple> appleList  =  new  ArrayList<Apple> ();
return null;
    }

    // 使用泛型extends解决调用实体方法名称带来的异常
    public void getInfo(T t)
    {
        t.showInfo();
    }

    // 获取某种类水果日期销售列表
    public List<T> getList(List<T> eProduuctList)
    {
        return null;
    }

    public static <T> T getListByStatic(List<T> eList)
    {
        return null;
    }

}

/**
 * @Author: Travelsky_CLSUN
 * @Date: Created on 17-5-8
 * @Description: 卖橙的个体户
 * 在继承泛型类的时候必须明确继承类的泛型实参,如果父类属于不确定的泛型内容,则必须把子类也声明为不确定的泛型
 * eg:class SelfEmployed extends Sellers<T> 错误,必须指明子类的泛型参数
 * 正确签名如下:
 * 1.class SelfEmployed<T> extends Sellers<T>
 * 2.class SelfEmployed extends Sellers<Fruit>
 **/
public class SelfEmployed extends Sellers<Orange>
{

}

第二,不要假设所有的类型信息都被擦除了 —— 结构化的类型参数还存在,需要的话还是可以用下的(不过不要过分依赖反射机制)。

销售员泛型类及其子类

新浦京棋牌手机版下载 5新浦京棋牌手机版下载 6

 1 package com.genric.test;
 2 
 3 import java.lang.reflect.Field;
 4 import java.lang.reflect.Method;
 5 import java.lang.reflect.ParameterizedType;
 6 import java.util.List;
 7 
 8 import org.junit.Test;
 9 
10 import com.genric.domain.SelfEmployed;
11 import com.genric.domain.Sellers;
12 
13 /**
14  * @Author: Travelsky_CLSUN
15  * @Date: Created on 17-5-8
16  * @Description: 测试类
17  */
18 public class TestGenric
19 {
20     /**
21      * @Author: Travelsky_CLSUN
22      * @Date: Created on 17-5-8
23      * @Description: 测试泛型实参是否被擦除
24      */
25     @Test
26     public void testGenricTypeExit() throws Exception
27     {
28         Class<?> sellersTemp = Sellers.class;
29         ParameterizedType actualType = null;
30         log("====根据反射获取泛型类、泛型字段以及泛型方法的类型====");
31         // 测试使用反射获取泛型类的实际参数
32         log("泛型类Sellers的泛型实参:" + sellersTemp.getTypeParameters()[0].getBounds()[0].toString());
33         // 测试使用反射获取泛型类下泛型变量的实际参数
34         Field fieldBerry = sellersTemp.getField("BerryDateList");
35         log("泛型字段BerryDateList的类型:" + fieldBerry.getGenericType().toString());
36         Method methodAppleList = sellersTemp.getMethod("getAppleList", new Class[]
37         { List.class });
38         actualType = (ParameterizedType) methodAppleList.getGenericParameterTypes()[0];
39         log("泛型方法getAppleList的形式参数的类型:" + actualType.getActualTypeArguments()[0].toString());
40         actualType = (ParameterizedType) methodAppleList.getGenericReturnType();
41         log("泛型方法getAppleList的返回值的类型:" + actualType.getActualTypeArguments()[0].toString());
42         Class<?> selfEmployed = SelfEmployed.class;
43         actualType = (ParameterizedType) selfEmployed.getGenericSuperclass();
44         log("泛型子类SelfEmployed的主类实参:" + actualType.getActualTypeArguments()[0]);
45 
46     }
47 
48     /**
49      * @Author: Travelsky_CLSUN
50      * @Date: Created on 17-5-8
51      * @Description: 输出函数
52      */
53     private void log(String str)
54     {
55         System.out.println(str);
56     }
57 }

测试类,测试是否可以通过反射获取泛型参数

  如果泛型真的是全部擦除的话,通过反射应该没法获取到相关的泛型实参。但是运行测试代码,发现结果如下:

新浦京棋牌手机版下载 7新浦京棋牌手机版下载 8

1 ====根据反射获取泛型类、泛型字段以及泛型方法的类型====
2 泛型类Sellers的泛型实参:class com.genric.domain.Fruit
3 泛型字段BerryDateList的类型:java.util.List<com.genric.domain.StrawBerry>
4 泛型方法getAppleList的形式参数的类型:class com.genric.domain.Apple
5 泛型方法getAppleList的返回值的类型:class com.genric.domain.Apple
6 泛型子类SelfEmployed的主类实参:class com.genric.domain.Orange

通过反射获取到的结果

  再使用JD-gui工具对Sellers泛型类进行反编译,发现结果如下:

         新浦京棋牌手机版下载 9

  从这反编译的内容上看,给了我一个错觉:泛型擦除没有把类以及方法、字段等元数据下的实参给擦除,只是擦除了方法内部的泛型实参(见getAppleList方法内的appleList)。

     基于这个错觉,我觉得可以在Sellers这个泛型类下面编写签名为 public List<T> getList(List<T> eProduuctList) 的方法,通过反射动态地获取到eProductList下的具体类型,从而不用再去写好几个获取某个品种最佳销售日期的函数(类似getAppleList这样的每次写一个获取相关实体列表的函数)。

  所以,把getList方法写成了下面的样子,在反射的时候动态地获取泛型实参T类型。    

新浦京棋牌手机版下载 10新浦京棋牌手机版下载 11

 1     // 获取某种类水果日期销售列表
 2     public List<T> getList(List<T> eProductList)
 3     {
 4         Class<?> paramClass = eProductList.getClass();
 5         System.out.println("before transfromer: " + paramClass);
 6         Type paramType = paramClass.getTypeParameters()[0].getBounds()[0];
 7         if (paramType instanceof ParameterizedType)
 8         {
 9             System.out.println(paramType);
10             ParameterizedType genricType = (ParameterizedType) ((ParameterizedType) paramType)
11                     .getActualTypeArguments()[0];
12             System.out.println("actualType of eProductList is :" + genricType);
13         }
14         else
15         {
16             System.out.println("Type of eProductList is :" + paramType);
17         }
18         return null;
19     }

获取水果销售时间列表泛型方法(查看泛型实参)

新浦京棋牌手机版下载 12新浦京棋牌手机版下载 13

1     // 测试在泛型方法内动态获取泛型实参
2     @Test
3     public void testGetParameterType() throws Exception
4     {
5         Sellers<Apple> appleSellers = new Sellers<Apple>();
6         appleSellers.getList(new ArrayList<Apple>());
7     }

使用客户端代码测试获取获取泛型实参

新浦京棋牌手机版下载 14新浦京棋牌手机版下载 15

1 before transfromer: class java.util.ArrayList
2 Type of eProductList is :class java.lang.Object

返回结果

  由返回结果上面看来,通过反射根本不能动态地获取方法内的泛型实参。那为什么不能获取到相关的泛型实参,通过反射的时候获取到的是一个Object实参?我想了几个可能导致出现这个结果的猜想:①泛型擦除 ②方法是个动态方法,没关联到类的信息。

  在这两个思考原因论证处理之前,先来回复无法使用反射该泛型参数的原因:Java在编译时会校验泛型参数,生成时会以泛型实参的上限类型替代真实的泛型实参。但,Java虚拟机会以签名的形式保留这些泛型实参类型(包括类的定义、泛型方法、泛型字段都会保留参数的签名信息)

上一篇:从Java开发者的视角解释JavaScript 下一篇:没有了