美文网首页
Java泛型

Java泛型

作者: zizi192 | 来源:发表于2021-07-17 23:34 被阅读0次

为什么使用泛型

首先,我们举个例子。

  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 <T extends Number> 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<String> list = new ArrayList<>();

泛型机制的优点

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

泛型擦除

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

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

        ArrayList<Integer> 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<String> list){
    }

    public void a(List<Integer> list){
    }

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

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

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

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

泛型擦除问题

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

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

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

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

使用侧泛型

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

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

如何获取泛型信息

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

        List<Integer> list = new ArrayList<>();
        Map<Integer, String> 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<String, Integer> map1 = new HashMap<String, Integer>() {};
        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<Integer, String> 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<java.lang.Integer, java.lang.String>
        System.out.println("parameterizedType  的参数信息 " + Arrays.asList(parameterizedType2.getActualTypeArguments())); //[class java.lang.Integer, class java.lang.String]

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

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

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

Gson反序列化原理

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

TypeToken的部分代码如下:

public class TypeToken<T> {
  final Class<? super T> rawType;
  final Type type;
  final int hashCode;

@SuppressWarnings("unchecked")
  protected TypeToken() {
    this.type = getSuperclassTypeParameter(getClass());
    this.rawType = (Class<? super T>) $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<? super T> getRawType() {
    return rawType;
  }

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

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

PECS原则

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

示例

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src)   {  
      for (int i=0; i<src.size(); i++) { 
          dest.set(i, src.get(i)); 
      }
  } 
}

解释如下:

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

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

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

反射和泛型

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

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

调用Class的getSuperclass()方法返回的Class类型是Class<? super T>:

Class<? super String> sup = String.class.getSuperclass();

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

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

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

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

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

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

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

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

相关文章

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • Java泛型

    参考:Java知识点总结(Java泛型) 自定义泛型类 自定义泛型接口 非泛型类中定义泛型方法 继承泛型类 通配符...

  • Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型

    Java泛型—Java语法糖,只在编译有作用,编译后擦出泛型 在代码进入和离开的边界处,会处理泛型 Java泛型作...

  • JAVA 核心笔记 || [xxx] 泛型

    泛型 JAVA 的参数化类型 称为 泛型 泛型类的设计 Learn12.java 运行

  • 简单回顾Java泛型之-入门介绍

    什么时候开始有了Java泛型?什么是Java泛型?为什么要引入Java泛型?什么时候用到了泛型?可不可以给泛型下一...

  • Kotlin 泛型

    Kotlin 支持泛型, 语法和 Java 类似。例如,泛型类: 泛型函数: 类型变异 Java 的泛型中,最难理...

  • JAVA-泛型

    JAVA-泛型 sschrodinger 2018/11/15 简介 泛型是Java SE 1.5的新特性,泛型的...

网友评论

      本文标题:Java泛型

      本文链接:https://www.haomeiwen.com/subject/xrebpltx.html