Java泛型

为什么使用泛型

首先,我们举个例子。

  1. 求和函数
    针对开发中常见的数值求和需求,如int,long,double等类型。
    public static int addInt(int x,int y){
        return x+y;
    }

    public static float addFloat(float x,float y){
        return x+y;
    }

没有泛型的情况下,对不同的类型需要封装不同的方法。使用泛型则可以减少重复代码

    public static  double addNumber(T a, T b){
        return a.doubleValue() + b.doubleValue();
    }

此时,返回值选择double类型,因为其取值范围和精度相对其它Number都更合适。

2.List集合
List集合在没有使用泛型时,默认是Object元素,可以存放任意数据类型。

        List list = new ArrayList();
        list.add("mark");
        list.add("OK");
        list.add(100);

        for (int i = 0; i < list.size(); i++) {
            String name = list.get(i).toString(); 
            System.out.println("name:" + name);
        }

但是取出来使用的时候,仍需要知道元素类型,这就需要强制类型转换了。这种行为安全性不高,建议使用List时配合泛型。

        List list = new ArrayList<>();

泛型机制的优点

泛型机制的优点有:
1.泛型可编写模版代码来适应多种类型,减少重复代码
2.泛型可避免强制类型转换,编译时进行类型检查,减少出错机会

泛型擦除

Java泛型是伪泛型,因在编译期间泛型信息会被擦除,也就是生成的字节码文件中不包含泛型中的类型信息。编码使用泛型时添加类型信息,编译器编译的时候去掉,这个过程就是泛型擦除。

        ArrayList list1 = new ArrayList<>();
        list1.add("abc");

        ArrayList list2 = new ArrayList<>();
        list2.add(123);

        System.out.println("class:" + list1.getClass()); //class:class java.util.ArrayList
        System.out.println(list1.getClass() == list2.getClass());//true

最终list1.getClass() == list2.getClass()的结果是true,说明泛型类型String和Integer被擦除了,只剩下原始类型java.util.ArrayList。

    public void a(List list){
    }

    public void a(List list){
    }

上述的代码会出现编译错误both methods have same erasure,因为泛型擦除后,二者不能构成重载。

综上,Java的泛型也被称为伪泛型。

  • 真泛型:泛型中的类型是真实存在的。
  • 伪泛型:仅在编译时类型检查,在运行时擦除类型信息。

Java泛型擦除的原因是向前兼容,把已有的类型(主要是Collections容器)泛型化,保证已经部署的程序可以继续运行。

泛型擦除问题

先了解下泛型擦除究竟擦除了什么,保留了什么信息。

问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以Signature的形式 保留在Class文件的Constant pool中。但是使用侧泛型则不会。

声明侧泛型主要指以下内容

1.泛型类,或泛型接口的声明 2.带有泛型参数的方法 3.带有泛型参数的成员变量

使用侧泛型

也就是方法的局部变量,方法调用时传入的变量。

Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析

如何获取泛型信息

通过class的getTypeParameters只能获取到声明泛型参数的占位符。

        List list = new ArrayList<>();
        Map map = new HashMap<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters())); //E
        System.out.println(Arrays.toString(map.getClass().getTypeParameters())); //K,V

但是开发中有些场景需要获取泛型的信息,如Retrofit接口,Gson序列化,这时该如何办呢。请看下面修改后的代码:

        Map map1 = new HashMap() {};
        Type type1 = map1.getClass().getGenericSuperclass();
        ParameterizedType parameterizedType1 = ParameterizedType.class.cast(type1);
        for (Type typeArgument : parameterizedType1.getActualTypeArguments()) {
            System.out.println(typeArgument.getTypeName()); //class java.lang.String / class java.lang.Integer
        }

示例代码获取了map1实例所对应的泛型信息,两端示例代码结果不同的关键就是map和map1的定义不同。其中变量map1是创建了一个HashMap的匿名内部类,其泛型参数限定为 String和Integer。通过定义类的方式,在类信息中保留泛型信息,进而在运行时获得这些泛型信息。

另一种获取field泛型类型的方法如下
        //    public Map memMap;
        Field field = null;
        try {
            field = GenericDemo.class.getField("memMap");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        ParameterizedType parameterizedType2 = (ParameterizedType)field.getGenericType();
        System.out.println("parameterizedType toString " + parameterizedType2); //java.util.Map
        System.out.println("parameterizedType  的参数信息 " + Arrays.asList(parameterizedType2.getActualTypeArguments())); //[class java.lang.Integer, class java.lang.String]

Gson反序列化时如何解析泛型类型

当使用Gson库进行json的解析时,使用方式如下。可以看到也是使用了匿名内部类。

    // Gson 常用的情况
    public  List parse(String jsonStr){
        List topNews =  new Gson().fromJson(jsonStr, new TypeToken>() {}.getType());
        return topNews;
    }

Gson反序列化原理

Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。也就是说java的class文件会保存继承的父类或者接口的泛型信息。

TypeToken的部分代码如下:

public class TypeToken {
  final Class rawType;
  final Type type;
  final int hashCode;

@SuppressWarnings("unchecked")
  protected TypeToken() {
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class) $Gson$Types.getRawType(type);
    this.hashCode = type.hashCode();
  }

  /**
   * Returns the type from super class's type parameter in {@link $Gson$Types#canonicalize
   * canonical form}.
   */
  static Type getSuperclassTypeParameter(Class subclass) {
    Type superclass = subclass.getGenericSuperclass();
    if (superclass instanceof Class) {
      throw new RuntimeException("Missing type parameter.");
    }
    ParameterizedType parameterized = (ParameterizedType) superclass;
    return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
  }

  /**
   * Returns the raw (non-generic) type for this type.
   */
  public final Class getRawType() {
    return rawType;
  }

  /**
   * Gets underlying {@code Type} instance.
   */
  public final Type getType() {
    return type;
  }
}

通过创建继承自TypeToken的匿名内部类,并实例化泛型参数T。TypeToken的默认无参构造方法通过Class的public Type getGenericSuperclass()方法,获取了父类的泛型信息。即上述使用用例中的List。最终Gson利用子类会保存父类class的泛型参数信息的特点,通过匿名内部类实现了泛型参数的解析。

PECS原则

PECS即Produce Extend Consumer Super,PECS是从集合的角度出发的,含义如下:
1.如果你只是从集合中取数据,那么它是个生产者,你应该用extend
2.如果你只是往集合中加数据,那么它是个消费者,你应该用super
3.如果你往集合中既存又取,那么你不应该用extend或者super

示例

public class Collections { 
  public static  void copy(List dest, List src)   {  
      for (int i=0; i

解释如下:

  • 在List的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit,具体是Fruit的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit,所以从这个泛型集合中取Fruit类型的元素是可以的。

  • 在List的泛型集合中,元素的类型是Apple的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入Apple与Apple的子类,因为这个集合中的元素都是Apple的父类,子类型是可以赋值给父类型的。

  • ? 无限定通配符。eg Pair,既不能读也不能写,只能做一些null判定。
    大多数情况下,可以引入泛型参数消除通配符。通配符有一个独特的特点,就是:Pair是所有Pair的超类,也就是可以安全的向上转型。

反射和泛型

Java的部分反射API也是泛型。例如:Class就是泛型:

Class clazz = String.class;
String str = clazz.newInstance();

调用Class的getSuperclass()方法返回的Class类型是Class

Class sup = String.class.getSuperclass();

我们可以声明带泛型的数组,但不能用new操作符创建带泛型的数组。必须通过强制转型实现带泛型的数组:

Pair[] ps = null; // ok
Pair[] ps = new Pair[2]; // compile error!

@SuppressWarnings("unchecked")
Pair[] ps = (Pair[]) new Pair[2];

使用泛型数组时要特别注意,因为如果持有原强制转换对象的引用,该对象没有泛型的限制,编译器不对检查对其的修改操作。泛型数组对象和其指向同一对象,可能有不安全的类型转换。推荐上面的写法,避免持有原引用

带泛型的数组实际上是编译器的类型擦除,所以我们不能直接创建泛型数组T[],因为擦拭后代码变为Object[],必须借助Class来创建泛型数组。Java提供了Array类来动态创建数组,但是仍需要强制类型转换。提供的方法名为Array.newInstance。

T[] createArray(Class cls) {
    return (T[]) Array.newInstance(cls, 5);
}

还可以利用可变参数创建泛型数组T[],但是不推荐。

参考文档:
【知识点】Java泛型机制7连问
Java 的泛型擦除和运行时泛型信息获取
Java Type 类型详解
super通配符
extends通配符
泛型和反射

你可能感兴趣的:(Java泛型)