您的位置 首页 java

Java 泛型的 PECS 原则

下面这些 PECS 原则 相关的面试题,你能回答上来多少?

如果回答不了,本文就绝对值得你认真看完。

Java 泛型的 PECS 原则

泛型通配符的用法有些复杂,Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则 ,帮助理解及正确使用 Java 泛型通配符。

当我们真正理解了 Java 泛型的 PECS 原则 ,上面这些问题也就迎刃而解了。

泛型 学习资料:《泛型最全知识导图》、《大厂泛型面试真题26道》,到本篇结尾处获得~

1 知识回顾

首先,我们来简单温习下泛型通配符的相关基础知识。

Java 的泛型通配符为一个泛型类所指定的类型集合,提供了一个有用的类型范围。

如果将一个对象划分为声明、使用两个部分:

  • 泛型 则侧重于类型的声明的代码复用,用于 定义内部数据类型的参数化
  • 通配符 则侧重于使用上的代码复用,用于 定义使用的对象类型的参数化

Java 的 泛型通配符形式

  • 固定上边界通配符 ?extends T ,代表类型变量的范围有限,只能传入某种类型、或者它的子类,适合 频繁往外读取数据 的场景。
  • 固定下边界通配符 ? super T ,代表类型变量的范围有限,只能传入某种类型、或者其父类,适合 频繁插入数据 的场景。

更详细深入的介绍,可以看下这两篇(点击蓝字):

2 Java 泛型的 PECS 原则

温顾了基础,我们接着来介绍 PECS 原则

PECS 的英文全称是 Producer Extends , Consumer Super ,即读取时使用 extends ,写入时使用 super

也就是说:

  • 参数化类型表示一个生产者,就使用 <? extends T>
  • 参数化类型表示一个消费者,就使用 <? super T>

不是很明白也没关系,下面的代码示例可以帮助理解。

2.1 我们先来看一个错误

 List<? extends Foo> list1 = new ArrayList<Foo>();
List<? extends Foo> list2 = new ArrayList<Foo>();

 /* Won't compile */ list2.add( new Foo() ); //error 1
 list1.addAll(list2);    //error 2 
  

1) error 1

IntelliJ says:

 add(capture<? extends Foo>) in List cannot be applied to add(Foo)
  

The compiler says:

 cannot find symbol
symbol  : method addAll(java.util.List<capture#692 of ? extends Foo>)
location: interface java.util.List<capture#128 of ? extends Foo>
  

2) error 2

IntelliJ gives me

 addAll(java.util.Collection<? extends capture<? extends Foo>>) in List cannot be applied to addAll(java.util.List<capture<? extends Foo>>)
  

Whereas the compiler just says

 cannot find symbol
symbol  : method addAll(java.util.List<capture#692 of ? extends Foo>)
location: interface java.util.List<capture#128 of ? extends Foo>
        list1.addAll(list2);
  

2.2 为什么会出现上面的错误

下面会一步一步,逐步来推导。

Apple Fruit 的子类,但是, List< Apple> 不是 List< Fruit> 的子类,有没有办法让两者兼容使用呢?

这个时候,我们就可以使用型变和逆变来实现了,主要是 extends super 关键字。

例如:

1)型变

  HashMap < T extends String>;
HashMap< ? extends String>;
  

2)逆变

 HashMap< T super String>;
HashMap< ? super String>;
  

2.3 协变 < ? extends T>

类型的上界是 T,参数化类型可能是 T 、又或者 T 的子类。

 public class Test {
     static  class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static  void  main(String[] args) throws IO Exception  {
        List<? extends Fruit> fruits = new ArrayList<>();
  

不能加入任何元素:

         fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile error
        fruits.add(new Apple());    // compile error  
  

集合元素的类型,符合 extends Fruit ,可赋值给变量 fruits

         fruits = new ArrayList<Food>(); // compile error
        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile success  注1
        fruits.add(new Apple());   // compile error 注2 


        fruits = new ArrayList<? extends Fruit>(); // 在java中会出现 compile error: 通配符类型无法实例化  

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

注 1 和注 2 ,两条语句在 kotlin 中,AS 不报错,可以正常运行。

把 Kotlin 转为 Java ,我们发现 Java 代码没有协变,但是其它错误语句在 AS 中是报错的,把 Java 代码贴到 AS 中,是没有报错的。

得出推论

AS 检查优先级更高 ,有报错就无法运行,没有报错,就按照 Java 代码去执行。

这是因为 AS 在检查 Kotlin 时,还不够严谨的原因吗?我们来看看。

1)存入数据

  • 编译器会阻止将 Apple 类加入 fruits ,在向 fruits 中添加元素时,编译器会检查类型是否符合要求。因为编译器只知道 fruits 是fruit 某个子类的 List,但并不知道这个子类具体是什么类,只能阻止向其中加入任何子类。为了类型安全,不能往使用了 ? extends 的数据结构中写入任何的值;
  • 元素类型为 Fruit 和其子类的集合,都可以成功赋值给变量 fruits ,赋值后,变量 fruits 类型就是具体的类型(不再是协变);
  • 通配符类型无法实例化。

2)读取数据

由于编译器知道它是 Fruit 的子类型,因此,我们可以从中读取出 fruit 对象:

 Fruit fruit = fruits.get(0);
  

3)kotlin 的协变 out

从关键字可以看出,只能 读出 数据:

 var fruits :MutableList<out Fruit> 
  

2.3 逆变<? super T>

表示类型的下界是 T ,参数化类型可以是 T、 或者 T 的超类:

 public class Test {
    static class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static void main(String[] args) throws IOException {
        List<? super Fruit> fruits = new ArrayList<>();
  

fruit 及其子类,可被看做是 fruit ,从而添加成功。

         fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile success
        fruits.add(new Apple());    // compile success
  

集合元素的类型,符合 super fruit ,可赋值给变量 fruits ,赋值后 fruits 不再是逆变类型。

         fruits = new ArrayList<Food>(); // compile success
        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile error

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

        Fruit object = fruits.get(0); // compile error,
    }
}
  

1)kotlin 的逆变——in

从关键字,也能看出,只能 写入 数据

 var fruits :MutableList<out Fruit>
  

2)存入数据

  • 添加 fruit 及其子类元素都可以成功,因为编译器会自动向上转型,fruit 及其子类元素,可以被认为是 fruit 类型被成功添加 。但是,由于编译器并不知道 List 的内容,究竟是 fruit 的哪个超类,因此,不允许加入特定的任何超类型。
  • 元素类型 为 fruit 和其超类的集合,都可以成功赋值给变量 fruits ,赋值后,变量 fruits 类型就是具体的类型,而不再是逆变。
  • super 通配符类型同样不能实例化。

3)读取数据

Object 是任何 Java类的最终祖先类。因此,编译器在不知道这个超类具体是什么类的情况下,只能返回 Object 对象。

 Object fruit = apples.get(0);
  

4)数组是协变的

在 Java 语言中,数组是协变的。如果 Number 是 Integer 的超类型,那么 Number[] 也是 Integer[] 的超类型。

对数组而言, String [] 可以赋值给 Object[] 。

 public class Test {
    public static void main(String[] args) {
        String[] strArray = new String[3];
        Object[] objArray = strArray;
    }
}
  

5)kotlin 的泛型实化

泛型实化 在 Java 中是个不存在的概念,属于 Kotlin 的新特性,它可以借助于关键字 inline ,在运行时保留泛型信息。

6)不使用 inline

 fun <T> create(): T = mRetrofit.create(T::class.java)  

7)使用 inline

 inline fun <reified T> create(): T = mRetrofit.create(T::class.java)  

这个方法可以被合法声明,在调用时也非常优雅。

 val service = create<NetworkService>()
  

create() 方法不接收任何对象作为参数,只是传入了一个类型参数,根据传入类型的不同,返回我们需要的对象。

原理分析

任何被声明称 inline 的函数,都会把函数体内的所有代码,直接复制到每一个被调用的地方。由于泛型参数值的不同,每一个调用 inline 函数的位置,都会因为泛型参数值的不同而有所不同。

它在编译器时,就能确定具体的类型,这样才能实例化。

3 总结

经过上述分析, Java 泛型的 PECS 原则 总结如下:

  • 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符,即 PE( Producer Extends )
  • 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符,即 CS ( Consumer Super )
  • 如果既要存又要取,那么就不要使用任何通配符。

以上,是 Java 泛型的 PECS 原则 详细介绍。

我是大全哥,持续更新成体系的 Java 核心技术。

知识成体系 学习才高效 ,如果觉得有帮助,请顺手 点赞 支持下,谢谢。

我们下期见~

附泛型学习资料:

1 《泛型知识全景导图》

快速构建泛型知识体系,高清版本原图,几乎 囊括了所有泛型核心知识点

泛型知识全景导图

2 《大厂泛型面试真题26道》

精选大厂高频 泛型面试题 ,都是我最新整理的,备面、复习时都可以查看

大厂泛型面试题26道

end

文章来源:智云一二三科技

文章标题:Java 泛型的 PECS 原则

文章地址:https://www.zhihuclub.com/185492.shtml

关于作者: 智云科技

热门文章

网站地图