使用方法引用和原始类型的类型推断

huangapple go评论83阅读模式
英文:

Type inference with method reference and primitive types

问题

以下是您提供的内容的翻译:

有没有办法告诉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方法。问题是我仍然需要Object方法,我无法删除它,这仍然会导致编译器接受我上面列出的奇怪调用。

所以问题是:有没有办法告诉Java当方法引用返回原始类型时不要使用Object版本的isEquals?我找不到任何信息,感觉在这方面我运气不佳。

(注意:对象版本的isEquals的实际实现正常工作且应该是安全的。这是因为Object.equals和Objects.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的类型。恰好Integer和String共享相同的上限Serializable & Comparable<? extends Serializable & Comparable<?>>

英文:

Is there a way to tell Java to NOT try to infer a type from a method reference that uses primitive types?

Here is a method that I wrote, the reason for this is irrelevant right now:

    public static &lt;F, T&gt; Predicate&lt;F&gt; isEquals(
            Function&lt;F, T&gt; aInMapFunction, T aInExpectedValue)
    {
        return aInActual -&gt; Objects.equals(
                aInMapFunction.apply(aInActual), aInExpectedValue);
    }

Now, what if you pass a method reference to "isEquals" that returns a primitive type?

Predicate&lt;String&gt; lLengthIs20 = isEquals(String::length, 20);

This is all fine and dandy, except that Java will also accept this strange usage:

Predicate&lt;String&gt; lLengthIs20 = isEquals(String::length, &quot;what the heck?!?!?&quot;);

This is because the compiler will infer type parameter T as "Serializable &amp; Comparable&lt;? extends Serializable &amp; Comparable&lt;?&gt;&gt;", which will accept both Integer and String types.

This is undesirable, in my case, as I would like a compilation error rather than Java figuring out some crazy type argument. For my thing, I can also explicitly override method "isEquals" to take specific primitive types. For example:

    public static &lt;F&gt; Predicate&lt;F&gt; isEquals(
            ToIntFunction&lt;F&gt; aInMapFunction, int aInExpectedValue)
    {
        return aInActual -&gt;
                aInMapFunction.applyAsInt(aInActual) == aInExpectedValue;
    }

This works fine, this method is invoked rather than the Object one when I pass in a method that returns a primitive int. The problem is that I still need the Object method, I cannot remove it, which will still cause the compiler to accept the weird invocation I listed above.

So the question is: is there a way for me to tell Java to not use the Object version of isEquals when the method reference returns a primitive type? I couldn't find anything, I'm feeling I'm out of luck in this one.

(NOTE: the actual implementation of the object version of isEquals works fine and should be safe. This is because Object.equals and Objects.equals accept Object parameters and a String object will never be equals to an Integer object. Semantically, however, this looks weird)

EDIT: after the comment from "paranoidAndroid", one idea that I just had is to wrap the method reference the following way:

    public static &lt;T&gt; Function&lt;T, Integer&gt; wrap(ToIntFunction&lt;T&gt; aInFunction)
    {
        return aInFunction::applyAsInt;
    }

And now,

Predicate&lt;String&gt; lLengthIs20 = isEquals(wrap(String::length), &quot;what the heck?!?!?&quot;);

... generates a compilation error. Still not great though, maybe there is a better way. At least this is better than passing the type in explicitly, which kind of beats the purpose.

EDIT 2: I'm in Java 8 right now. Java 11 might behave differently here, I didn't test.

EDIT 3: I'm thinking there is nothing we can do here, this is just an implication of how type inference works in Java. Here is another example:

    public static &lt;T&gt; boolean isEquals(T t1, T t2) {
        return Objects.equals(t1, t2);
    }

with this method, the following expression is perfectly valid:

System.out.println(isEquals(10, &quot;20&quot;));

This works because Java will try to resolve the type for T based on a common upper bound. It just happens that both Integer and String share the same upper bound Serializable &amp; Comparable&lt;? extends Serializable &amp; Comparable&lt;?&gt;&gt;

答案1

得分: 2

我认为这不是一个错误,而是类型推断的结果。OP已经提到了这一点。编译器不会试图匹配精确的类型,而是最具体的类型

让我们分析一下类型推断如何与OP提供的示例一起工作。

public static &lt;F, T&gt; Predicate&lt;F&gt; isEquals(Function&lt;F, T&gt; func, T expValue) {
    return actual -&gt; Objects.equals(func.apply(actual), expValue);
}
Predicate&lt;String&gt; lLengthIs20 = isEquals(String::length, &quot;Whud?&quot;);

在这里,目标类型是Predicate&lt;String&gt;,根据方法的返回类型,即Predicate&lt;F&gt;(其中F是一个泛型类型),F被绑定到String。然后,方法引用String::length被检查是否适合方法参数Function&lt;F, T&gt;,其中FString,而T是某个未限定的类型。这很重要:尽管方法引用String::length看起来其目标类型是Integer,但它也与Object兼容。类似地,Object obj = &quot;Hello&quot;.length()是有效的。它不是必须是一个Integer。同样,Function&lt;String, Object&gt; func = String::lengthFunction&lt;String, Object&gt; func = str -&gt; str.length()都是有效的,不会发出编译器警告。

推断到底是什么?

推断是将选择适当类型的工作推迟到编译器。你问编译器:“请,你能填入适当的类型,以便它能工作吗?”然后编译器回答:“好的,但我在选择类型时遵循某些规则。”

编译器选择最具体的类型。在isEquals(String::length, 20)的情况下,String::length20的目标类型都是Integer,因此编译器将其推断为这样。

但是,在isEquals(String::length, &quot;Whud?&quot;)的情况下,编译器首先尝试将T推断为Integer,因为String::length的类型,但由于第二个参数的类型,它无法成功。然后编译器尝试找到IntegerString的最接近的交集。

我能帮助或绕过编译器吗?

绕过?不太可能。嗯,有时类型转换是绕过的一种方式,就像以下示例中一样:

Object o = 23; // 运行时类型是整数
String str = (String) o; // 将抛出ClassCastException异常

这里的类型转换是一种潜在的不安全操作,因为o可能是或可能不是String。通过这种类型转换,你告诉编译器:“在这种特定情况下,我比你更懂” - 有可能在运行时出现异常的风险。

不过,并非所有的类型转换操作都被允许:

Integer o = 23;
String str = (String) o;
// 结果是编译器错误:incompatible types: Integer cannot be converted to String

但你确实可以帮助编译器。

类型见证

一种选择可能是使用类型见证

Predicate&lt;String&gt; lLengthIs20 = YourClass.&lt;String, Integer&gt;isEquals(String::length, &quot;what?&quot;);

这段代码会产生编译器错误:

> incompatible types: String cannot be converted to Integer

isEquals中添加Class&lt;T&gt;参数

另一种选择是在isEquals添加一个参数

public static &lt;F, T&gt; Predicate&lt;F&gt; isEquals(Class&lt;T&gt; type, Function&lt;F, T&gt; func, T expValue) {
    return actual -&gt; Objects.equals(func.apply(actual), expValue);
}
// 这会成功:
Predicate&lt;String&gt; lLengthIs20 = isEquals(Integer.class, String::length, 20);
// 这会失败:
Predicate&lt;String&gt; lLengthIs20 = isEquals(Integer.class, String::length, &quot;Whud?&quot;);

类型转换

第三种选择可能是类型转换。在这里,你将String::length强制转换为Function&lt;String, Integer&gt;,现在编译器受限于F = String, T = Integer。现在使用&quot;Whud?&quot;会引发问题。

Predicate&lt;String&gt; predicate = isEquals((Function&lt;String, Integer&gt;) String::length, &quot;Whud?&quot;);
英文:

I think that this is not a bug, but a consequence of type inference. OP already mentioned it. The compiler will not try to match an exact type, but the most specific one.

Let us analyse how type inference works with the example provided by OP.

public static &lt;F, T&gt; Predicate&lt;F&gt; isEquals(Function&lt;F, T&gt; func, T expValue) {
    return actual -&gt; Objects.equals(func.apply(actual), expValue);
}
Predicate&lt;String&gt; lLengthIs20 = isEquals(String::length, &quot;Whud?&quot;);

Here the target type is Predicate&lt;String&gt;, and according to the return type of the method, which is Predicate&lt;F&gt; (where F is a generic type), F is bound to a String. Then the method reference String::length is checked whether it fits into the method parameter Function&lt;F, T&gt;, where F is String and T some unbounded type. And this is important: while the method reference String::length looks like its target type is Integer, it is also compatible to Object. Similarly, Object obj = &quot;Hello&quot;.length() is valid. It is not required to be an Integer. Likewise, both Function&lt;String, Object&gt; func = String::length and Function&lt;String, Object&gt; func = str -&gt; str.length() are valid and do not emit a compiler warning.

What exactly is inference?

Inference is to defer the job of selecting the appropriate type to the compiler. You ask the compiler: "Please, could you fill in appropriate types, so that it'll work?" And then the compiler answers: "Okay, but I follow certain rules when selecting the type."

The compiler selects the most specific type. In the case of isEquals(String::length, 20), both the target type of String::length and 20 is Integer, so the compiler infers it as such.

However, in the case of isEquals(String::length, &quot;Whud?&quot;) the compiler first tries to infer T to an Integer because of the type of String::length, but it fails to do so because of the type of the second argument. The compiler then tries to find the closest intersection of Integer and String.

Can I aid or bypass the compiler?

Bypass? No, not really. Well, sometimes typecasting is a way of bypassing, like in the following example:

Object o = 23; // Runtime type is integer
String str = (String) o; // Will throw a ClassCastException

The typecast here is a potentially unsafe operation, because o may or may not be a String. With this typecast, you say to the compiler: "In this specific case, I know better than you" – with the risk of getting an exception during runtime.

Still, not all typecast operations are permitted:

Integer o = 23;
String str = (String) o;
// Results in a compiler error: &quot;incompatible types: Integer cannot be converted to String&quot;

But you can certainly aid the compiler.

Type witness

One option may be to use a type witness:

Predicate&lt;String&gt; lLengthIs20 = YourClass.&lt;String, Integer&gt;isEquals(String::length, &quot;what?&quot;);

This code will emit a compiler error:

> incompatible types: String cannot be converted to Integer

Add a Class&lt;T&gt; parameter to isEquals

Another option would be to add a parameter to isEquals:

public static &lt;F, T&gt; Predicate&lt;F&gt; isEquals(Class&lt;T&gt; type, Function&lt;F, T&gt; func, T expValue) {
    return actual -&gt; Objects.equals(func.apply(actual), expValue);
}
// This will succeed:
Predicate&lt;String&gt; lLengthIs20 = isEquals(Integer.class, String::length, 20);
// This will fail:
Predicate&lt;String&gt; lLengthIs20 = isEquals(Integer.class, String::length, &quot;Whud?&quot;);

Typecasting

A third option may be typecasting. Here you cast String::length to a Function&lt;String, Integer&gt;, and now the compiler is restricted to F = String, T = Integer. Now the usage of &quot;Whud?&quot; causes trouble.

Predicate&lt;String&gt; predicate = isEquals((Function&lt;String, Integer&gt;) String::length, &quot;Whud?&quot;);

答案2

得分: 0

是的。在泛型的上下文中,告诉Java不要使用Object的方法被称为:“指定一个边界”。

我的实验确认,将以下方法称为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))将调用第一个版本。

单击蓝色的执行按钮在此演示中以查看它的工作原理。

英文:

> „&hellip;is there a way for me to tell Java to not use the Object version&hellip;

Yes. And the term — in the context of Generics — for telling Java to not use Object is called: „Specifying a bound“.

My experiment confirmed that calling the following method as isEquals(String::hashCode, &quot;What the theoretical fuck!&amp;?*!?@!&quot;) will produce error: no suitable method found for isEquals(String::hashCode,String)...

public static &lt;F extends String, T extends Number&gt; Predicate&lt;F&gt; isEquals(Function&lt;F, T&gt; aFunction, T aValue)
{
    return input -&gt; Objects.equals(aFunction.apply(input), aValue);
}  

If you have both, the above method, and the following one in the same class, then this version is called for isEquals(String::length, 20)...

public static &lt;F&gt; Predicate&lt;F&gt; isEquals(ToIntFunction&lt;F&gt; aFunction, int aValue)
{
    return input -&gt; aFunction.applyAsInt(input) == aValue;
}

...But the first one is called for isEquals(String::length, Integer.valueOf(42)).

Click the blue Execute button in this demo to see it working.

答案3

得分: 0

根据我看,这似乎是一个真正的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);
英文:

As far as I'm concerned, this smells like a real java compiler bug to me..Compiler should be able to infer arguments without assignment to a variable, since we have Function&lt;F, T&gt; aInMapFunction which should enforce T, as the compiler "knows" that String::length returns an Integer.
However I came up with a sort of solution for you:

public class PredicateBuilder&lt;F,T&gt;
{
	public Predicate&lt;F&gt; isEquals(
			Function&lt;F, T&gt; aInMapFunction, T aInExpectedValue)
	{
		return aInActual -&gt; Objects.equals(
				aInMapFunction.apply(aInActual), aInExpectedValue);
	}
}

and usage:

new PredicateBuilder&lt;String, Integer&gt;().isEquals(String::length, 5);

Won't compile with other argument types, won't compile either if you try this:

new PredicateBuilder&lt;&gt;().isEquals(String::length, 5);

huangapple
  • 本文由 发表于 2020年8月13日 05:25:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/63384914.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定