问题描述
有没有办法告诉Java不要尝试从使用原始类型的方法引用中推断类型?
这是我写的一种方法,目前的原因是无关紧要的:
public static <F,T> Predicate<F> isEquals(
Function<F,T> aInMapFunction,T aInExpectedValue)
{
return aInActual -> Objects.equals(
aInMapFunction.apply(aInActual),aInExpectedValue);
}
现在,如果您传递对返回原始类型的“ isEquals”的方法引用怎么办?
Predicate<String> lLengthIs20 = isEquals(String::length,20);
这一切都很好,但Java也会接受这种奇怪的用法:
Predicate<String> lLengthIs20 = isEquals(String::length,"what the heck?!?!?");
这是因为编译器会将类型参数T推断为“ Serializable & Comparable<? extends Serializable & Comparable<?>>
”,它将接受Integer和String类型。
在我的情况下,这是不希望的,因为我想要一个编译错误,而不是Java找出一些疯狂的类型参数。就我而言,我也可以显式重写方法“ isEquals”以采用特定的原始类型。例如:
public static <F> Predicate<F> isEquals(
ToIntFunction<F> aInMapFunction,int aInExpectedValue)
{
return aInActual ->
aInMapFunction.applyAsInt(aInActual) == aInExpectedValue;
}
这很好用,当我传入一个返回原始int的方法时,将调用此方法,而不是对象。问题是我仍然需要Object方法,无法删除它,这仍将导致编译器接受我上面列出的怪异调用。
所以问题是:当方法引用返回原始类型时,有没有办法让我告诉Java不要使用isEquals的对象版本?我什么也找不到,我觉得自己不走运。
(注意:isEquals对象版本的实际实现工作正常并且应该是安全的。这是因为Object.equals和Object s .equals接受Object参数,而String对象永远不会等于一个Integer对象。但是,从语义上来说,这看起来很奇怪)
编辑:在来自“ paranoidAndroid”的评论之后,我刚刚想到的一个想法是用以下方式包装方法引用:
public static <T> Function<T,Integer> wrap(ToIntFunction<T> aInFunction)
{
return aInFunction::applyAsInt;
}
现在,
Predicate<String> lLengthIs20 = isEquals(wrap(String::length),"what the heck?!?!?");
...生成编译错误。仍然不是很好,也许有更好的方法。至少这比显式地传递类型要好得多,后者会达到目的。
编辑2:我现在正在使用Java 8。 Java 11在这里的行为可能有所不同,我没有测试。
编辑3:我认为我们在这里无能为力,这只是Java中类型推断如何工作的暗示。这是另一个示例:
public static <T> boolean isEquals(T t1,T t2) {
return Objects.equals(t1,t2);
}
使用此方法,以下表达式完全有效:
System.out.println(isEquals(10,"20"));
之所以可行,是因为Java会尝试基于公共上限来解析T的类型。碰巧整数和字符串共享相同的上限Serializable & Comparable<? extends Serializable & Comparable<?>>
解决方法
我认为这不是错误,而是类型推断的结果。 OP已经提到过。编译器不会尝试匹配确切的类型,而是最具体的类型。
让我们用OP提供的示例来分析类型推断的工作原理。
public static <F,T> Predicate<F> isEquals(Function<F,T> func,T expValue) {
return actual -> Objects.equals(func.apply(actual),expValue);
}
Predicate<String> lLengthIs20 = isEquals(String::length,"Whud?");
目标类型为Predicate<String>
,根据方法的返回类型为Predicate<F>
(其中F
是通用类型),F
为绑定到String
。然后检查方法引用String::length
是否适合方法参数Function<F,T>
,其中F
是String
和T
是某种无界类型。这很重要:尽管方法引用String::length
的目标类型为Integer
,但它也与Object
兼容。同样,Object obj = "Hello".length()
有效。 不需要是Integer
。同样,Function<String,Object> func = String::length
和Function<String,Object> func = str -> str.length()
均有效,并且不会发出编译器警告。
推理到底是什么?
推断是为了将选择适当类型的工作推迟到编译器。您问编译器:“请,您可以填写适当的类型,以便它起作用吗?”然后编译器回答:“好的,但是选择类型时我遵循某些规则。”
编译器选择最特定的类型。对于isEquals(String::length,20)
,String::length
和20
的目标类型均为Integer
,因此编译器将其推断为此类。
但是,在isEquals(String::length,"Whud?")
的情况下,由于T
的类型,编译器首先尝试将Integer
推导到String::length
,但由于这样做而失败了。第二个参数的类型。然后,编译器尝试找到Integer
和String
的最近交点。
我可以帮助还是绕过编译器?
绕过?不,不是。好吧,有时类型转换是一种绕过的方法,如以下示例所示:
Object o = 23; // Runtime type is integer
String str = (String) o; // Will throw a ClassCastException
此处的类型转换是潜在的不安全操作,因为o
可能是String
,也可能不是。通过这种类型转换,您对编译器说:“在这种情况下,我比您更了解” –可能会在运行时出现异常。
仍然,并非所有类型转换操作都被允许:
Integer o = 23;
String str = (String) o;
// Results in a compiler error: "incompatible types: Integer cannot be converted to String"
但是您当然可以帮助编译器。
键入见证人
一种选择可能是使用类型证人 :
Predicate<String> lLengthIs20 = YourClass.<String,Integer>isEquals(String::length,"what?");
此代码将发出编译器错误:
不兼容的类型:字符串不能转换为整数
向Class<T>
添加一个isEquals
参数
另一种选择是向isEquals
添加参数:
public static <F,T> Predicate<F> isEquals(Class<T> type,Function<F,expValue);
}
// This will succeed:
Predicate<String> lLengthIs20 = isEquals(Integer.class,String::length,20);
// This will fail:
Predicate<String> lLengthIs20 = isEquals(Integer.class,"Whud?");
类型转换
第三个选项可能是 类型转换 。在这里,您将String::length
强制转换为Function<String,Integer>
,现在编译器被限制为F = String,T = Integer
。现在使用"Whud?"
会带来麻烦。
Predicate<String> predicate = isEquals((Function<String,Integer>) String::length,"Whud?");
,
„ …我有办法告诉Java不要使用Object版本…”
是的。用来告诉Java不要使用Object
的术语“在泛型上下文中的 ”被称为:“ Specify an bound ”。
My experiment confirmed,将以下方法称为isEquals(String::hashCode,"What the theoretical fuck!&?*!?@!")
将产生error: no suitable method found for isEquals(String::hashCode,String)
...
public static <F extends String,T extends Number> Predicate<F> isEquals(Function<F,T> aFunction,T aValue)
{
return input -> Objects.equals(aFunction.apply(input),aValue);
}
如果您同时拥有上述方法和下一个方法,则此版本将用于isEquals(String::length,20)
...
public static <F> Predicate<F> isEquals(ToIntFunction<F> aFunction,int aValue)
{
return input -> aFunction.applyAsInt(input) == aValue;
}
...但是第一个叫isEquals(String::length,Integer.valueOf(42))
。
点击蓝色的 Execute 按钮in this demo可以正常使用。
,就我而言,这对我来说就像一个真正的Java编译器错误。.编译器应该能够推断参数而无需分配变量,因为我们有Function<F,T> aInMapFunction
应该强制执行T,因为编译器“知道” String::length
返回一个整数。
但是我为您提出了一种解决方案:
public class PredicateBuilder<F,T>
{
public Predicate<F> isEquals(
Function<F,T> aInMapFunction,T aInExpectedValue)
{
return aInActual -> Objects.equals(
aInMapFunction.apply(aInActual),aInExpectedValue);
}
}
和用法:
new PredicateBuilder<String,Integer>().isEquals(String::length,5);
不会与其他参数类型一起编译,如果您尝试这样做,也不会编译:
new PredicateBuilder<>().isEquals(String::length,5);