为什么Java无法推断超类型?


19

大家都知道龙延伸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吗?
莫里斯·佩里

它已经在课程中了
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好的。抱歉,我错过了。
莫里斯·佩里

更改Number getNumber()<A extends Number> A getNumber()可使工作正常。不知道这是否是您想要的。正如其他人所说的,问题MyInterface::getNumber可能是Double例如返回的函数而不是Long。您的声明不允许编译器根据当前提供的其他信息来缩小返回类型。通过使用通用返回类型,您可以让编译器这样做,因此可以正常工作。
Giacomo Alzetta

Answers:


9

这个表达式:

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从具有此类签名的函数中返回?它不会为您进行垂头丧气。

因为Number是的超类,Long并且Number不一定是a Long(这就是为什么它不编译)的原因-您必须自己显式地强制转换:

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

制作FFunction<MyIinterface, Long>或像你一样的方法调用中明确地传递通用参数:

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

并且知道R将被视为Number和代码将被编译。


多数民众赞成在一个有趣的类型转换。但我仍然在搜索方法“ with”的定义,该定义将使调用方无需任何强制转换即可进行编译。无论如何,谢谢你的想法。
jukzi

@jukzi你不能。怎么with写都没关系。您MJyInterface::getNumber具有类型Function<MyInterface, Number>so R=Number,然后还具有R=Long其他参数(请记住Java文字不是多态的!)。此时编译器停止运行,因为并非总是可能将a转换Number为a Long。解决这个问题的唯一方法是改变MyInterface使用<A extends Number> Number的返回类型,这使得编译器具有R=A然后R=Long既然A extends Number它可以替代A=Long
贾科莫Alzetta

4

你的错误,关键是在类型的泛型声明FF extends Function<T, R>。无效的语句是:new Builder<MyInterface>().with(MyInterface::getNumber, 4L);首先,您有一个new Builder<MyInterface>。因此,该类的声明暗含T = MyInterface。按你的声明withF必须是Function<T, R>,这是Function<MyInterface, R>在这种情况下。因此,该参数getter必须采用a MyInterface作为参数(方法引用MyInterface::getNumber和满足MyInterface::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语言规范中的以下内容:

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

  • 从布尔类型到布尔类型
  • 从字节类型到字节类型
  • 从短类型到短类型
  • 从char类型到Character类型
  • 从int类型到Integer类型
  • 从长型到长型
  • 从浮点型到浮点型
  • 从Double类型到Double类型
  • 从空类型到空类型

正如您可以清楚地看到的那样,没有从long到Number的隐式装箱转换,并且只有在编译器确定它需要一个Number而不是Long时,才可能发生从Long到Number的扩展转换。由于需要Number的方法引用与提供Long的4L之间存在冲突,因此编译器(由于某种原因?)无法做出Long is-a Number的逻辑飞跃并推论出Fa 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>),因此您甚至不必担心结果对象被修改。坦白地说,要获得类型安全的字段灵活的生成器需要大量的精力,这是绝对的耻辱,但是如果没有一些新功能,代码生成或令人讨厌的反射,这可能是不可能的。


感谢您所做的所有工作,但我看不到避免手动打字的任何改进。我天真的理解是,编译器应该能够推断(即类型转换)人类可以做到的一切。
jukzi

@jukzi我的天真理解是相同的,但是无论出于何种原因,它都不起作用。不过,我想出了一种解决方法,几乎​​可以达到预期的效果
Avi

再次感谢。但是您的新建议太宽了(请参阅stackoverflow.com/questions/58337639),因为它允许编译“ .with(MyInterface :: getNumber,” I AM NOT A NUMBER“)”;
jukzi

您的句子“编译器无法同时推断方法引用的类型和4L”很酷。但是我只想反过来。编译器应根据第一个参数尝试Number并扩展到第二个参数的Number。
jukzi

1
WHAAAAAAAAAAAT?为什么BiConsumer不能按预期工作,而Function不能按预期工作?我不知道。我承认这恰好是我想要的类型安全性,但不幸的是,它不适用于吸气剂。为什么为什么为什么。
jukzi

1

似乎编译器已使用值4L来确定R为Long,而getNumber()返回一个Number,不一定是Long。

但是我不确定为什么值优先于方法...


0

Java编译器通常不擅长推断多个/嵌套的泛型类型或通配符。通常,如果不使用辅助函数来捕获或推断某些类型,我将无法编译某些内容。

但是,您真的需要捕获Functionas 的确切类型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是a Long,所以Ta 也是如此Long,因此您的函数不兼容,并且Java无法知道您的意思是Number还是Long


0

具有以下签名:

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

您的所有示例均会编译,但第三个示例除外,第三个示例明确要求该方法具有两个类型变量。

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


您的代码将被编译with( getNumber,"NO NUMBER"),这是不希望的。同样,泛型永远都是不变的也是不正确的(请参见stackoverflow.com/a/58378661/9549750,以证明
二传手的

@jukzi啊,我的解决方案已经由Avi提出。太糟糕了... :-)。顺便说一句,我们确实可以将a分配Thing<Cat>Thing<? extends Animal>变量,但是对于真正的协方差,我希望Thing<Cat>可以将a分配给a Thing<Animal>。其他语言,例如Kotlin,也允许定义协变和反变类型变量。
Hoopje
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.