使用Java 17和Hamcrest 2.2

import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;

// snip

String m = "something";
JsonNode j = new TextNode("something else");
MatcherAssert.assertThat(j, Matchers.is(m)); // expected compile error here

我预计这会导致编译错误.MatcherAssert.assertThatMatchers.is具有以下签名:

public static <T> void assertThat(T actual, Matcher<? super T> matcher);
public static <T> org.hamcrest.Matcher<T> is(T value);

因此,代码应该分解为

String m = "something";
JsonNode j = new TextNode("something else");
Matcher<String> matcher = Matchers.is(m);  // specify static type
MatcherAssert.assertThat(j, matcher);      // actual compilation error

它失败了,因为String不是JsonNode的超级类型.

相反,当Matchers.is(m)被内联时,它返回Matcher<Object>,将<T>绑定到MatcherAssert.assertThatObject,并导致编译通过(尽管不出所料,断言失败,因为JsonNode不能equals a String).

我不明白为什么Matchers.is(m)要绑定<T>Object.

有人知道吗?

推荐答案

Javac查看行并反复进行解析,填充空白处,就像做拼图一样.具体地说,assertThat行需要一些类型变量绑定;方法调用涉及一个类型变量<T>.我将把它称为<A>,以将它与is方法所具有的完全独立的类型变量区分开来.

需要计算出<A>个.没有明确的 Select (您拨打了MatcherAssert.assertThat;您没有拨打MatcherAssert.<String>assertThat).因此,是时候进行推断了.

让我们来看看有什么可用的提示.第一个参数是T类型,传递的参数是JsonNode.

你会想--哦,太好了,那么,T就是JsonNode.还没那么快.如果我有一个签名为void foo(Number n)的方法,我可以向它传递一个整数.说表达式的类型绑定了typevar,这意味着T从一开始就不可能是接口类型,任何子类型都可能 destruct 东西,甚至是无关紧要的子类型,如new ArrayList<String>() {}(请注意那里的花括号)或任何lambda.

不,它将T绑定到范围[Object ... JsonNode].就像? super JsonNode.如果T最后是Object,那很好.当参数需要Object时,类型JsonNode的表达式是有效参数.

如果类型推断在这里结束,则javac将采用它找到的范围中最具体的类型,并将would绑定到JsonNode.我们可以在这个不同的例子中看到这一点:

public static <T> T ident(T in) { return in; }

var x = ident(new JsonNode());
x.someMethodOnlyJsonNodeHas();

上面的代码编译得很好,运行正常.var x将把var‘绑定’为表达式的类型,而类型为JsonNode.由于上面概述的过程:虽然推断T,但通过new JsonNode(),T被约束为[Object .. JsonNode],并且没有进一步的约束,因此,javac Select 它能 Select 的最具体的东西.

但是,assertThat有更多涉及<A>的参数.我们必须继续寻找.我们还有第二个参数,它的类型需要是Matcher<? super A>.我们发现Matchers.is(m)这个表达式有点古怪--我们试图弄清楚<A>是什么,在做这件事的过程中,我们现在必须弄清楚<B>是什么(<B>是我们将称为public static <T> Matcher<T> is (T value);中的<T>的东西,因为这些类型变量完全独立,这两个使用T的事实纯粹是巧合,与推理没有任何关系.其中之一的重命名应该无关紧要,实际上也不重要).为了弄清楚<B>是什么,javac将..从上下文中推断.

因此,当我们在推断<A>的过程中由外而内时,我们需要由内而外推断出<B>.希望这有助于强调为什么推理有时会产生一些奇怪的结果,以及为什么您可能只想用显式的Matcher<String> matcher = Matchers.is(m)重写这段代码,以避免这种非常棘手的分析.

关键是,Java不能做到这一点,它不能同时走两条路,也不能违反因果关系的基本原理.

因此,相反,Matchers.is(m)是独立解决的,而不是它周围的背景.m是一个字符串,is()的签名很简单.<B>被约束为[Object .. String].

现在我们可以回到解决<A>的问题上了.

我们有一个参数将<A>约束为[Object .. JsonNode],另一个参数将<A>约束为[Object .. String].

然后...我们完事了!没有进一步的限制,也不需要计算重载,所以,我们现在可以合并我们的限制.

因此,javac一如既往地执行它的操作, Select 符合所有限制的最具体类型.也就是Object.对象在这里工作得很好:

Matcher<Object> matcher = Matchers.is("Hello");

编译得很好.试试吧,为什么不好呢所以,这对T = Object人有效.jJsonNode的类型,也可以用:

Object o = new JsonNode():

编译和工作正常.试试看.那怎么会不好呢?

因此,一切都很好.编译器编译并运行它,而不会给你任何错误或警告.

从语义上讲,这也是有意义的.出于同样的原因,java.util.Listcontains方法接受任何Object而不是E--"这份橙子列表中包含这个苹果吗?"是一个有意义的问题,并有一个合理的、尽管相当明显的答案:不,不是.

与此形成对比的是:"这里有一个苹果.把它加到这个橙子 list 上",这是没有意义的.同样的事情也在这里发生:请判断这个JsonNode是否传递了这个字符串匹配器.如果未通过测试,则无法通过测试.

这也是一个语义上合理的问题,有一个显而易见的答案:它永远不会.答案是‘不,它们不相等’,我们甚至在运行代码之前就知道这一点.但是编译器没有意识到这就是assertThat方法的用处,而且任何一行代码如果断言guaranteed失败,最好是一个编译器警告.除了将javac变成某种无休止的可插拔的编辑工具包(这将是非常棒的!但事实并非如此,而且我所知道的编程语言也不是),所以对于javac来说,没有办法知道assertThat带有这样的包袱:"如果通过它的所有路径都必然导致失败的结果,那么这个调用是无用的",甚至也不知道Matcher<String>个对象附带的包袱是,当被要求匹配类型JsonNode的对象时,它们是guaranteed失败的.如果不运行这段代码,Javac就无法知道这一点,而且您真的不希望编译器在编译代码的过程中运行代码.对于初学者来说,需要考虑的是停顿问题.

Java相关问答推荐

具有默认分支的JUnit代码覆盖率切换声明

为什么我的画布没有显示在PFA应用程序中?

如何在Android上获取来电信息

取消按钮,但没有任何操作方法引发和异常

Java List with all combinations of 8 booleans

';com.itextpdf.ext.html.WebColors已弃用

对某一Hyroby控制器禁用@cacheable

如何调整工作时间日历中的时间

如何在Java中从XML中获取特定的 node ,然后将其删除?

由于 list 中的权限错误,Android未生成

测试容器无法加载类路径初始化脚本

使用存储在字符串变量中的路径目录打开.pdf文件

如何在Java中为thunk创建映射器函数

根本不显示JavaFX阿拉伯字母

如何在列表(链表)中插入一个新 node (作为prelast)

我如何为我的Java抵押贷款代码执行加薪操作(&Q)

为了安全起见,有必要复制一份 list 吗?

Java类型推断:为什么要编译它?

控制器建议异常处理

@此处不能应用可为null的批注