Java 泛型

泛型

泛型即参数化类型,即实际使用时再确定参数的类型。就像我们在定义方法时可以声明形参,在实际调用方法时传入实参,那么我们同样可以把类型也参数化,只有在实际使用时才确定数据的类型 。

泛型包括泛型接口、类、方法、变量等。Java的泛型只在编译时存在,编译完成后会进行擦除。即在字节码文件中是没有泛型存在的。

泛型优缺点

优点:

  • 在编译时提供了类型检查的机制防止非法类型,同时避免了显示的类型转换。
  • 在使用泛型的时候可以根据传入泛型类型的不同而使用对应类型的API。

缺点:

  • 泛型不支持基本类型所以会涉及自动拆装箱问题有性能损耗
  • 泛型变量不能当做真实类型去使用(比如通过泛型变量调用方法T.getXx())
  • 泛型会自动进行类型强转这也有性能损耗

如何判断是否需要使用泛型

  1. 判断当前这个类中的方法是否需要在返回值中使用泛型,如果需要则可以使用泛型。
  2. 使用泛型的时候根据传入的不同类型而使用对应类型的API 而去使用泛型。
  3. 如果你的类型确定的话是完全不需要使用泛型的

泛型擦除

Java中的泛型类型在代码编译时会被擦除掉(一般情况下会被擦成Object类型,如果使用了上限通配符的话会被擦成extends右边的类型,如T extends View则最终会被擦成View类型)。

泛型擦除的原因:出于兼容性的考虑,泛型是JDK1.5以后才引入的,为了兼容之前的JDK版本所以在运行时将泛型类型都擦掉以保证和之前JDK版本的java字节码相同。

不过泛型的擦除会保留部分泛型信息,在 signature 属性中保存了泛型的参数类型信息,不过这仅限于类中的属性,对于方法中的局部变量无能为力。

泛型数组

数组中的元素为泛型,那么这个数组就是泛型类型的数组,泛型数组在java中使用GenericArrayType接口来表示,但是Java中不能创建一个确切的泛型类型的数组,但是使用通配符创建型数组是可以的。

  ArrayList<String> [] a=new ArrayList<String>[10];//error
  ArrayList<String> [] a=new ArrayList[10];//success
        
  List<?>[] ls = new ArrayList<?>[10]; 

某种特定的场合,你仍然要使用泛型数组,推荐的方式是使用 类型标签+Array.newInstance 来实现,并用注解 @SuppressWarnings(“unchecked”) 抑制住警告:

运行时获取泛型信息

Signature属性

类型擦除是有范围的,编译时类中定义的泛型类型是不会被擦除的,对应的泛型类型会被保存在Signature中。即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到。

额外保存参数类型

比如 Jackson 反序列化泛型类型,将参数类型信息显式保存下来。此外还可以使用注解处理器,在编译期间获取泛型真实类型,并保存到类文件中。

通配符

由于泛型不是协变的,它不支持继承,比如在使用 List< Number> 的地方不能传递 List< Integer>,所以引入了通配符去解决对应的问题,可以理解通配符是对泛型功能的扩展和增强。

  • extends 上限通配符 可以接收extend后的类型及子类
  • super 下限通配符 可以接收super后的类型及父类
  • ? 通配符 代表了接收未知类型的数据

extends

    List<? extends Fruit> fruits = new ArrayList<>();
    
        fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile error
        fruits.add(new Apple());    // compile error

        Fruit object = fruits.get(0);    // compile success

        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile success
        fruits = new ArrayList<Food>(); // compile error
        fruits = new ArrayList<? extends Fruit>(); // compile error: 通配符类型无法实例化

可以看到add操作全部编译失败因为 fruits 集合并不知道实际类型是 Fruit、Apple 还是 Food,所以无法对其赋值。

super

       List<? super Fruit> fruits1 = new ArrayList<>();
        
        fruits1.add(new Food());     // compile error
        fruits1.add(new Fruit());    // compile success
        fruits1.add(new Apple());    // compile success

        fruits1 = new ArrayList<Fruit>(); // compile success
        fruits1 = new ArrayList<Apple>(); // compile error
        fruits1 = new ArrayList<Food>(); // compile success
        fruits1 = new ArrayList<? super Fruit>(); // compile error: 通配符类型无法实例化
        
         Fruit object = fruits1.get(0); // compile error

add 时 Fruit 及其子类均可成功,为啥呢?因为已知 fruits 的参数化类型必定是 Fruit 或其超类 T,那么 Fruit 及其子类肯定可以赋值给 T。

归根到底,还是“子类对象可以赋值给超类引用,而反过来不行”这一规则导致 extends 和 super 通配符在 add 操作上表现如此的不同。同样地,也导致 super 限定的 fruits 中 get 到的元素不能赋值给 Fruit 引用,而 extends 则可以。

使用规范:super只能用作实参不能用于形参,?和extends实参形参都可以

形参:public class Shop< T>的那个T,即在声明泛型时的类型参数(类或方法声明的类型参数)

实参:除了形参其他都是实参,比如声明变量(Shop < Apple> appleShop;此处的Apple)、方法参数中的泛型(public int getTotalWeight(List list) {}此处的Fruit)

当我们使用上限通配符时对应方法参数中使用到泛型的方法将都无法调用,因为我们不能确定具体传入的是哪种类型。

当我们使用下限通配符时对应方法返回值中使用到泛型的方法将都无法调用,因为我们不能确定具体返回的是哪种类型。

  • 静态方法使用泛型必须把方法生命为泛型方法
  • 构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!

泛型,有点难度,会让很多人懵逼,那是因为你没有看这篇文章!

深入理解 Java 泛型

面试重灾区-泛型攻克

Java 的泛型擦除和运行时泛型信息获取

java泛型你需要知道的一切

Java泛型超详细解读 : super和extend

https://blog.csdn.net/s10461/article/details/53941091

posted @ 2020-09-26 21:02  Robin132929  阅读(128)  评论(0编辑  收藏  举报