【问题标题】:Why can Java not infer a supertype?为什么 Java 不能推断超类型?
【发布时间】:2019-10-14 13:38:34
【问题描述】:

我们都知道 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;
  }

}

  • 无法推断&lt;F, R&gt; with(F, R) 的类型参数
  • Builder.MyInterface 类型的 getNumber() 的类型是 Number,这与描述符的返回类型不兼容:Long

用例见:Why is lambda return type not checked at compile time

【问题讨论】:

  • 你能发帖MyInterface吗?
  • 它已经在类中
  • 嗯,我尝试了&lt;F extends Function&lt;T, R&gt;, R, S extends R&gt; Builder&lt;T&gt; with(F getter, S returnValue),但得到了java.lang.Number cannot be converted to java.lang.Long),这令人惊讶,因为我没有看到编译器从哪里得到getter的返回值需要转换为@的想法987654330@.
  • @jukzi 好的。抱歉,我错过了。
  • 将 Number getNumber() 更改为 &lt;A extends Number&gt; A getNumber() 可以使事情正常进行。不知道这是否是你想要的。正如其他人所说,问题在于MyInterface::getNumber 可能是一个返回Double 的函数,而不是Long。您的声明不允许编译器根据存在的其他信息来缩小返回类型。通过使用泛型返回类型,您允许编译器这样做,因此它可以工作。

标签: java type-inference


【解决方案1】:

这个表达式:

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&lt;MyInterface, Long&gt;

并且你传递了一个方法引用,它将被推断为Function&lt;MyInterface, Number&gt; 这是关键 - 编译器应该如何预测你实际上想要从具有这种签名的函数返回Long?不会为你失望。

由于Number 是Long 的超类,而Number 不一定是Long(这就是它无法编译的原因)- 你必须自己显式地强制转换:

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

使F 成为Function&lt;MyIinterface, Long&gt; 或在方法调用期间显式传递通用参数:

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

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

【讨论】:

  • 这是一个有趣的类型转换。但我仍然在寻找“with”方法的定义,这将使调用者在没有任何强制转换的情况下编译。无论如何,谢谢你的想法。
  • @jukzi 你不能。你的with 是如何写的并不重要。你有MJyInterface::getNumber 有类型Function&lt;MyInterface, Number&gt; 所以R=Number 然后你也有来自另一个参数的R=Long(记住Java 文字不是多态的!)。此时编译器停止,因为并非总是可以将Number 转换为Long。解决此问题的唯一方法是将MyInterface 更改为使用&lt;A extends Number&gt; Number 作为返回类型,这使得编译器具有R=A,然后是R=Long,并且由于A extends Number 它可以替换A=Long
【解决方案2】:

错误的关键在于F 类型的泛型声明:F extends Function&lt;T, R&gt;。不起作用的语句是:new Builder&lt;MyInterface&gt;().with(MyInterface::getNumber, 4L); 首先,你有一个新的Builder&lt;MyInterface&gt;。因此类的声明暗示T = MyInterface。根据您对with 的声明,F 必须是Function&lt;T, R&gt;,在这种情况下是Function&lt;MyInterface, R&gt;。因此,参数getter必须以MyInterface为参数(通过方法引用MyInterface::getNumber和MyInterface::getLong满足),并返回R,该R必须与函数@的第二个参数类型相同987654338@。现在,让我们看看这是否适用于您的所有情况:

// 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);

除此之外,对于哪个选项可以降低特定应用程序的代码复杂性,主要是一个设计决策,因此请选择最适合您的选项。

不进行强制转换就无法做到这一点的原因在于from the Java Language Specification:

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

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

您可以清楚地看到,从 long 到 Number 没有隐式装箱转换,并且从 Long 到 Number 的扩大转换只能在编译器确定它需要 Number 而不是 Long 时发生。由于需要 Number 的方法引用与提供 Long 的 4L 之间存在冲突,因此编译器(出于某种原因???)无法进行 Long 是 Number 的逻辑飞跃并推断出 F 是Function&lt;MyInterface, Number&gt;。

相反,我设法通过稍微编辑函数签名来解决问题:

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");
  }
}

提供了构造对象的类型安全能力,希望在未来的某个时候我们能够从构建器返回一个immutable data object(也许通过向接口添加一个toRecord()方法,并指定builder 为Builder&lt;IntermediaryInterfaceType, RecordType&gt;),因此您甚至不必担心生成的对象会被修改。老实说,要获得一个类型安全的字段灵活构建器需要付出如此多的努力,这绝对是一种耻辱,但如果没有一些新功能、代码生成或烦人的反射量,这可能是不可能的。

【讨论】:

  • 感谢您所做的所有工作,但我没有看到在避免手动类型转换方面有任何改进。我天真的理解是编译器应该能够推断(即类型转换)人类可以推断的一切。
  • @jukzi 我的天真理解是一样的,但无论出于何种原因,它都不能那样工作。不过,我想出了一个几乎可以达到预期效果的解决方法
  • 再次感谢。但是你的新提案太宽泛了(参见stackoverflow.com/questions/58337639),因为它允许编译 ".with(MyInterface::getNumber, "I AM NOT A NUMBER")";
  • 你的句子“编译器不能同时推断方法引用和 4L 的类型”很酷。但我只是想反过来​​。编译器应根据第一个参数尝试 Number 并对第二个参数的 Number 进行扩展。
  • 哇哇哇哇哇?为什么 BiConsumer 能按预期工作,而 Function 不能?我不明白。我承认这正是我想要的类型安全,但不幸的是不适用于 getter。为什么为什么为什么。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    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")" 不应该编译
    【解决方案5】:

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

    // 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。

    【讨论】:

      【解决方案6】:

      带有以下签名:

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

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

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

      【讨论】:

      • 您的代码将编译 with( getNumber,"NO NUMBER"),这是不希望的。此外,泛型始终不变也是不正确的(请参阅stackoverflow.com/a/58378661/9549750 以证明 setter 的泛型行为不同于 getter 的行为)
      • @jukzi 啊,我的解决方案已经被 Avi 提出了。太糟糕了... :-)。顺便说一句,我们确实可以将Thing&lt;Cat&gt; 分配给Thing&lt;? extends Animal&gt; 变量,但对于真正的协方差,我希望Thing&lt;Cat&gt; 可以分配给Thing&lt;Animal&gt;。其他语言,例如 Kotlin,确实允许定义协变和逆变类型变量。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-15
      • 1970-01-01
      • 2019-02-11
      相关资源
      最近更新 更多