为什么Java不能推断出超类型?

19
我们都知道Long扩展了Number。那么为什么这个代码不编译呢?
如何定义方法“with”,以便程序在没有任何手动转换的情况下编译?
import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}

  • 无法推断出<F,R>的类型参数with(F,R)
  • 来自Builder.MyInterface的getNumber()的类型为Number,这与描述符的返回类型Long不兼容

请参见用例:为什么Lambda返回类型不会在编译时检查


你能发布 MyInterface 吗? - Maurice Perry
它已经在类中了。 - jukzi
我尝试过 <F extends Function<T, R>, R, S extends R> Builder<T> with(F getter, S returnValue) 但是出现了 java.lang.Number cannot be converted to java.lang.Long 的错误,这让我很惊讶,因为我不知道编译器从哪里得到的信息需要将 getter 返回值转换为 returnValue - jingx
@jukzi 好的。抱歉,我错过了那个。 - Maurice Perry
Number getNumber()更改为<A extends Number> A getNumber()可以使事情正常运作。我不知道这是否是您想要的。正如其他人所说,问题在于MyInterface :: getNumber可能是返回例如Double而不是Long的函数。您的声明不允许编译器根据其他信息缩小返回类型。通过使用通用返回类型,您允许编译器这样做,因此它可以工作。 - Giacomo Alzetta
@GiacomoAlzetta,您的解释并没有让我理解为什么“with(castToFunction(MyInterface::getNumber), 4L)”可以编译通过。显然,建造者的用例不是使用“A extends Number”。不过这是一个有趣的想法。 - jukzi
6个回答

10

这个表达式:

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

可以改写为:

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

考虑到方法签名:

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R将被推断为Long
  • F将成为Function<MyInterface, Long>

然后您传递一个方法引用,该引用将被推断为Function<MyInterface, Number>这是关键 - 编译器如何预测您实际需要从具有这种签名的函数返回Long它不会为您执行下转换。

由于NumberLong的超类,而Number不一定是Long(这就是为什么它不能编译的原因)- 您必须自己显式进行强制转换:

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

使F成为Function<MyIinterface, Long>,或在方法调用期间明确传递泛型参数,如下所示:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

现在,R将被视为Number,代码可以编译。


这是一个有趣的类型转换。但我仍在寻找方法“with”的定义,它将使调用者在没有任何强制转换的情况下编译。无论如何,感谢您提供的想法。 - jukzi
@jukzi 你做不到。无论你的 with 怎么写都没用。因为你的 MJyInterface::getNumber 的类型是 Function<MyInterface, Number>,所以 R=Number,然后你还有另一个参数 R=Long(记住 Java 字面量不具有多态性!)。此时编译器停止了,因为将 Number 转换为 Long 并不总是可能的。唯一的解决方法是将 MyInterface 改为使用 <A extends Number> Number 作为返回类型,这样编译器就会有 R=A,然后 R=Long,由于 A extends Number,它可以替换为 A=Long - Giacomo Alzetta

4
你错误的关键在于类型 F 的通用声明: F extends Function<T, R>。不能工作的语句是:new Builder<MyInterface>().with(MyInterface::getNumber, 4L); 首先,你有一个新的 Builder<MyInterface>。因此类的声明暗示着 T = MyInterface。根据你对 with 的声明,F 必须是一个 Function<T, R>,这在这种情况下是一个 Function<MyInterface, R>。因此,参数 getter 必须以 MyInterface 作为参数(由方法引用 MyInterface::getNumberMyInterface::getLong 满足),并返回 R,它必须是函数 with 的第二个参数的相同类型。现在,让我们看看这是否适用于你所有的情况:
// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

您可以通过以下选项“修复”此问题:
// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

在这个点之后,对于哪个选项能减少你特定应用程序的代码复杂性,这主要是一个设计决策,所以选择最适合你的。

你不能不进行转换就完成这个操作的原因在于,来自Java语言规范:

装箱转换将原始类型的表达式视为相应引用类型的表达式。具体而言,以下九个转换称为 装箱转换

  • 从 boolean 类型到 Boolean 类型
  • 从 byte 类型到 Byte 类型
  • 从 short 类型到 Short 类型
  • 从 char 类型到 Character 类型
  • 从 int 类型到 Integer 类型
  • 从 long 类型到 Long 类型
  • 从 float 类型到 Float 类型
  • 从 double 类型到 Double 类型
  • 从 null 类型到 null 类型
正如你所看到的,从长整型到数字类型之间没有隐式的装箱转换,而从Long到Number的扩展转换只能在编译器确定需要一个Number而不是Long时发生。由于需要一个Number和提供一个Long之间存在方法引用的冲突,编译器(因为某种原因???)无法进行逻辑推断,认为Long是一个Number,从而推断出F是一个Function<MyInterface,Number>

相反,我通过轻微编辑函数签名来解决了这个问题:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

在这个更改之后,发生以下情况:
// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

编辑:
花了更多时间后,发现通过getter实现类型安全非常困难。这里提供一个使用setter方法实现构建器类型安全的可行示例:

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

提供了类型安全构建对象的能力,希望在未来的某个时候我们能够从构建器中返回一个不可变数据对象(可能通过向接口添加toRecord()方法,并将构建器指定为Builder<IntermediaryInterfaceType, RecordType>来实现),这样你甚至都不必担心结果对象被修改。说实话,获得类型安全的字段灵活构建器需要付出如此之多的努力,真是太可惜了,但可能无法实现没有一些新功能、代码生成或大量反射的情况下。


再次感谢。但是你的新提议太宽泛了(请参见https://dev59.com/4ek5XIcBkEYKwwoY49kS),因为它允许编译“.with(MyInterface :: getNumber,“I AM NOT A NUMBER”)”; - jukzi
你的句子“编译器不能同时推断方法引用和4L的类型”很棒。但是我想要相反的结果,编译器应该基于第一个参数尝试Number类型,并对第二个参数进行扩展为Number类型。 - jukzi
@jukzi 我认为要实现类型安全可能会非常困难,因为当尝试接受字符串时,Java 会将类型隐式扩展为对象(必须这样做)。我会尽力而为,但不能保证成功。 - Avi
1
什么?为什么BiConsumer按预期工作而Function却不行?我不明白。我承认这正是我想要的类型安全,但不幸的是不能与getter一起使用。为什么?为什么?为什么? - jukzi
@jukzi 真是悲催啊... :( 这个想法是在声明 setter 时就已经定义了它们想要的边界。但是对于 getter,你 无法 阻止 Java 自动将类型扩展到 Object。唯一的限制方式是自己指定最大可接受的参数类型或强制转换参数。 - Avi
显示剩余5条评论

1
似乎编译器使用值4L来决定R是Long类型,并且getNumber()返回一个Number类型,不一定是Long类型。但我不确定为什么值比方法具有优先级...

0
Java编译器通常不能很好地推断多个/嵌套泛型类型或通配符。经常情况下,我无法在不使用帮助函数来捕获或推断某些类型的情况下编译某些内容。
但是,你真的需要将Function的确切类型作为F捕获吗?如果不需要,也许以下代码可以工作,并且正如你所看到的,它似乎也适用于Function的子类型。
import java.util.function.Function;
import java.util.function.UnaryOperator;

public class Builder<T> {
    public interface MyInterface {
        Number getNumber();
        Long getLong();
    }

    public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
        return null;
    }

    // example subclass of Function
    private static UnaryOperator<String> stringFunc = (s) -> (s + ".");

    public static void main(String[] args) {
        // works
        new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
        // works
        new Builder<String>().with(stringFunc, "s");

    }
}

"with(MyInterface::getNumber, "NOT A NUMBER")" 不应该编译通过 - jukzi

0

我认为最有趣的部分在于这两行之间的差异:

// works:
new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
// compilation error: Cannot infer ...
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);


在第一种情况下,T 明确为 Number,所以 4L 也是一个 Number,没有问题。在第二种情况下,4L 是一个 Long,所以 T 是一个 Long,因此您的函数不兼容,Java 不知道您是指 Number 还是 Long

0

使用以下签名:

public <R> Test<T> with(Function<T, ? super R> getter, R returnValue)

你的所有示例都可以编译,除了第三个,它明确要求该方法具有两个类型变量。

你的版本无法工作的原因是,Java的方法引用没有一个特定的类型。相反,它们具有在给定上下文中所需的类型。在你的情况下,由于4LR被推断为Long,但getter不能具有类型Function<MyInterface,Long>,因为在Java中,泛型类型在其参数中是不变的。


你的代码会编译 with( getNumber,"NO NUMBER"),这是不期望的。此外,泛型并不总是不变的(请参见 https://dev59.com/BVMH5IYBdhLWcg3wxic9#58378661 以证明设置器的泛型行为与获取器不同)。 - jukzi
@jukzi 啊,我的解决方案已经被 Avi 提出了。太糟糕了... :-)。顺便说一下,确实可以将 Thing<Cat> 分配给 Thing<? extends Animal> 变量,但是对于真正的协变,我希望可以将 Thing<Cat> 分配给 Thing<Animal>。其他语言,如 Kotlin,允许定义协变和逆变类型变量。 - Hoopje

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接