【问题标题】:Question about Java generics and design of java.util.function.Function关于Java泛型和java.util.function.Function设计的问题
【发布时间】:2019-08-19 02:45:29
【问题描述】:
  1. 关于通配符的问题

例子:Student extends Person

    Person person = new Person();
    Student student = new Student();

    List<? super Student> list = new ArrayList<>();
    list.add(student); // success
    list.add(person); // compile error

    List<? extends Person> list2 = new ArrayList<>();
    list2.add(person); // compile error
    list2.add(student);// compile error

我已阅读问题“capture#1-of ? extends Object is not applicable”下方的答案

您正在使用通用通配符。您不能执行添加操作,因为类类型不确定。您不能添加/放置任何东西(null 除外)-- Aniket Thakur

官方文档:通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数

但是为什么list.add(student)可以编译成功

  1. java.util.function.Function的设计
public interface Function<T, R>{

    //...

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
}

当返回类型是 Function&lt;V,R&gt; 并且输入类型是 V 时,为什么 before 被设计为 Function&lt;? super V, ? extends T&gt; 而不是 Function&lt;V,T&gt; ? (还是可以通过编译灵活使用)

【问题讨论】:

标签: java generics


【解决方案1】:

要理解这些问题,您必须了解泛型如何与subtyping(在Java 中使用extends 关键字明确表示)一起工作。 Andreas 提到了PECS 规则,这是它们在 Java 中的表示。

首先,我想指出,上面的代码可以通过简单的转换来纠正

ArrayList<? super Student> list = new ArrayList<>();
list.add(new Student());
ArrayList<Person> a = (ArrayList<Person>) list; // a covariance
a.add(new Person());

并且编译和运行良好(而不是引发任何异常)

原因很简单,当我们有一个consumer(它接受一些对象并消费它们,例如add 方法)时,我们期望它接受no more than(超类)类型的对象@我们指定了 987654328@,因为消费过程可能需要它想要的类型的任何成员(变量、方法等),并且我们要确保类型 T 满足消费者需要的所有成员。

相反,为我们生成对象的producer(如get 方法)必须提供no less than 类型的对象指定类型T,以便我们可以访问@ 987654334@已对产生的对象。

这两个与称为covariancecontravariance 的子类型密切相关

至于第二个问题,也可以参考Consumer&lt;T&gt;的实现(简单一些):

default Consumer<T> andThen(Consumer<? super T> after) {
  Objects.requireNonNull(after);
  return (T t) -> { accept(t); after.accept(t); };
}

我们需要这个? super T的原因是:当我们使用andThen方法组合两个Consumers时,假设前一个Consumer接受一个T类型的对象,我们期望稍后获取no more than T 类型的对象,因此它不会尝试访问T 没有的任何成员。

因此,我们允许之前的消费者(T 类型)与接收不完全是 T 类型的对象的消费者组合,而不是简单地写 Consumer&lt;T&gt; after 而是 Consumer&lt;? super T&gt; after,但可能更小然后T,方便covariance。这使得以下代码听起来不错:

Consumer<Student> stu = (student) -> {};
Consumer<Person> per = (person) -> {};
stu.andThen(per);

出于同样的考虑,Function 类型的 compose 方法也适用。

【讨论】:

    【解决方案2】:

    IMO 这可能是 vanilla Java 中最复杂的概念。所以让我们分解一下。我将从你的第二个问题开始。

    Function&lt;T, R&gt; 接受T 类型的实例t 并返回R 类型的实例r。使用继承意味着如果Foo extends T 可以提供Foo 类型的实例foo,如果Bar extends R 则类似地返回Bar 类型的bar

    作为一个想要编写灵活的泛型方法的库维护者,很难而且实际上不可能提前知道所有可能与此方法一起使用的扩展TR 的类。那么我们将如何编写一个处理它们的方法呢?此外,这些实例具有扩展基类的类型这一事实与我们无关。

    这就是通配符的用武之地。在方法调用期间,我们说您可以使用任何满足所需类信封的类。对于有问题的方法,我们有两个使用上限和下限泛型类型参数的不同通配符:

    public interface Function<T, R>{
        default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    

    现在让我们说我们想利用这个方法...例如,让我们定义一些基本类:

    class Animal{} 
    class Dog extends Animal{} 
    class Fruit{} 
    class Apple extends Fruit{} 
    class Fish{} 
    class Tuna extends Fish{} 
    

    想象一下我们的函数和变换定义如下:

    Function<Animal, Apple> base = ...;
    Function<Fish, Animal> transformation = ...;
    

    我们可以使用compose组合这些函数来创建一个新函数:

    Function<Fish, Apple> composed = base.compose(transformation);
    

    这一切都很好,但现在想象一下,在所需的输出函数中,我们实际上只想使用Tuna 作为输入。如果我们不使用下界? super V 作为我们传递给composeFunction 的输入类型参数,那么我们会得到一个编译器错误:

    default <V> Function<V, R> compose(Function<V, ? extends T> before)
    ...
    Function<Tuna, Apple> composed = base.compose(transformation);
    > Incompatible types: 
    > Found: Function<Fish, Apple>, required: Function<Tuna, Apple>
    

    发生这种情况是因为调用compose 的返回类型将V 指定为Tuna,而另一方面,transformation 将其“V”指定为Fish。所以现在当我们尝试将transformation 传递给compose 时,编译器要求transformation 接受Tuna 作为它的V,当然TunaFish 不完全匹配。

    另一方面,代码的原始版本 (? super V) 允许我们将 V 视为下限(即,它允许对 V 进行“逆变”与“不变”)。编译器不会遇到TunaFish 之间的不匹配,而是能够成功应用下限检查? super V,其计算结果为Fish super Tuna,因为Tuna extends Fish 是正确的。

    对于另一种情况,假设我们的调用被定义为:

    Function<Animal, Apple> base = ...;
    Function<Fish, Dog> transformation = ...;
    Function<Fish, Apple> composed = base.compose(transformation);
    

    如果我们没有通配符? extends T,那么我们会得到另一个错误:

    default <V> Function<V, R> compose(Function<? super V, T> before)
    Function<Fish, Apple> composed = base.compose(transformation);
    // error converting transformation from
    //    Function<Fish, Dog> to Function<Fish, Animal>
    

    通配符? extends T 允许它工作,因为T 解析为Animal,通配符解析为Dog,可以满足约束Dog extends Animal

    对于您的第一个问题;这些界限实际上只在方法调用的上下文中起作用。在该方法的过程中,通配符将被解析为实际类型,就像? super V被解析为Fish? extends T被解析为Dog一样。如果没有来自泛型签名的信息,我们将无法让编译器知道可以在该类型的方法上使用哪个类,因此不允许使用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-01-18
      • 1970-01-01
      • 2011-08-01
      • 1970-01-01
      • 2011-04-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多