Tagir Valeev最近发布了一条有关即将发布的Java JDK14版本的预览功能的推文:
#Java14模式匹配将名称隐藏带入了更高的疯狂程度。 在这里,我为FLAG字段添加或删除了final修饰符,该修饰符仅在不可达的if分支中访问。 这实际上改变了程序的语义! #ProgrammingIsFun 。 pic.twitter.com/UToRY3mpW9
问题在于,有一个计划中的并且在EA版本中已经可用的Java新功能引入了模式变量,而所提议的新标准的当前版本为某些真正令人毛骨悚然的编码问题留出了空间。
鸣叫之后,对细节进行了足够详细的讨论,以了解实际问题。 但是,在本文中,我将总结所有这些内容,以使您无需深入了解推文和标准。
什么是模式变量
在上面的推文中深入探讨问题概述之前,让我们先讨论一下模式变量是什么。 (也许有点草率,比准确和完整更能说明问题,但是来了。)
进行多次编程后,我们需要检查某些对象的类型。 运算符instanceof
为我们做到了。 典型的示例代码可以是这样的:
// HOW THIS IS TODAY, JAVA < 14 Object z = "alma" ; if (!(z instanceof String)){
throw new IllegalArgumentException(); } System.out.println(((String)z).length());
在现实生活中,变量z
可能来自其他地方,在这种情况下,它是不那么明显,这是一个字符串。 当我们想使用println
打印出字符串的长度时,我们已经知道z
引用的对象是一个字符串。 另一方面,编译器则没有,我们必须将变量强制转换为String
,然后才能使用length()
方法。 其他语言做得更好。 理想情况下,我可以写:
// HOW IT WOULD BE THE SIMPLEST Object z = "alma" ; if (!(z instanceof String)){
throw new IllegalArgumentException(); } System.out.println(z.length());
这不是Java方式,也不是JDK14简化此编程模式的方式。 相反,建议的功能为instanceof
运算符引入了一种新语法,该语法引入了一个新变量: 模式变量 。
长话短说,上面的示例如下所示:
// HOW IT IS IN JDK14-EA / OpenJDK (build 14-ea+28-1366) Object z = "alma" ; if (!(z instanceof String s)){
throw new IllegalArgumentException(); } System.out.println(s.length());
它引入了一个新变量s
,仅当引用的对象是String
时才在范围内。 没有异常抛出部分的简单代码版本是
Object z = "alma" ; if (z instanceof String s){
// we have here 's' and it is a String
System.out.println(s.length()); } // we do not have 's' here
当条件为true时,对象是字符串,因此我们有's'。 如果条件为假,那么我们将跳过then_statement,并且由于没有字符串,所以这里没有's'。 代码中只有在对象为字符串时才运行“ s”。 这样,模式变量的变量范围不仅受变量的句法范围的限制,而且还受可能的控制流程的限制。 仅考虑可以确定分析的控制流。
在Java编译器中,这种控制流分析并非无与伦比。 例如,如果存在编译器可以检测到的无法访问的代码,则Java程序将不会编译。
到目前为止,这似乎很简单,我们都很高兴获得Java 14的新功能。
JSL14标准
精确的范围计算在JLS14(Java语言规范14)标准中定义。 在撰写本文时,该规范仅作为预览提供。
由于Java程序的执行流程可以由许多不同的语言构造来控制,因此为每种结构定义了模式变量的范围。 针对不同的逻辑运算符,有单独的部分来评估短路,“ if”语句,“ while”语句等。 我不想广泛讨论不同的情况。 在这里,我将仅关注“ if”语句的情况,而没有“ else”部分。 上面引用的标准说:
以下规则适用于“ if(e)S”(14.9.1)语句:
*当e为true时,由e引入的模式变量肯定与`S`相匹配。
如果`e`引入的任何模式变量true都已经在`S`的作用域内,则是编译时错误。
*当且仅当`false`和`S'无法正常完成时,`if(e)S`引入`V`。
如果`if`语句引入的任何模式变量已经在范围内,则是编译时错误。
有趣的部分是“无法正常完成”。 上面的示例就是一个很好的例子:我们创建了一个所谓的guard if
语句。 当变量z
不是String
我们将抛出异常,返回或执行其他操作,这将始终阻止在变量不是String
的if
语句之后执行代码。
对于throw
或return
语句,通常很容易直接看出代码“无法正常完成”。 在无限循环的情况下,这并不总是那么明显。
问题
让我们看一下以下代码片段:
private static boolean FLAG = true ; static String variable = "Hello from field" ; public static void main() {
Object z = "Hello from pattern matching" ;
if (!(z instanceof String variable)){
while (FLAG) {
System.out.println( "We are in an endless loop" );
}
}
System.out.println(variable); }
在这种情况下,我们有一个循环,它是无限的或不是无限的。 这取决于代码的另一部分,这可能会将类字段FLAG
的值从true
更改为false
。 这部分代码“可以正常完成”。
如果我们修改上面的代码,只需稍微使字段FLAG
为final
,如
private static final boolean FLAG = true ; static String variable = "Hello from field" ; public static void main() {
Object z = "Hello from pattern matching" ;
if (!(z instanceof String variable)){
while (FLAG) {
System.out.println( "We are in an endless loop" );
}
}
System.out.println(variable); }
那么编译器将看到循环是无限的并且无法正常完成。 在第一种情况下,程序Hello from field
中打印出Hello from field
Hello from pattern matching
打印Hello from pattern matching
。 第二种情况下的模式variable
隐藏了字段variable
因为模式变量的范围扩展到了if
语句之后的命令,因为那么部分无法正常完成。
确实,此预览功能确实存在问题。 在这种情况下,代码的可读性非常可疑。 模式变量的范围以及是否隐藏字段取决于该字段的final
修饰符,该修饰符不存在。 当我们查看某些代码时,实际的执行和代码结果应该很简单,并且不应真正依赖于距离很远的某些代码,并且可能会跳过我们在本地阅读代码的注意。
这不是Java中唯一出现此异常的情况。 例如,您的代码库中可以有一个名为String
的类。 当它们引用String
类型时,位于同一包中的类的代码将使用该类。 如果我们从用户代码中删除String
类,则String
类型的含义变为java.lang.String
。 该代码的实际含义取决于“远”的其他代码。
然而,第二个示例是一个黑客,没有失去主意的Java程序员不可能将String
类命名为(严重https://github.com/verhas/jScriptBasic/blob/master/src/main/ java / com / scriptbasic / classification / String.java ?)或JDK中java.lang
包中也存在的其他名称。 也许这是纯粹的运气,也许在决策过程中考虑到了避免从java.lang
包中强制导入类的java.lang
。 这是历史。
另一方面,变量名隐藏和上面的情况似乎并不那么怪异,某些Java代码中肯定不会偶然发生某些事情。
幸运的是,这只是预览功能。 它将按原样出现在JDK14中,但作为预览功能,仅当javac编译器和Java执行使用--enable-preview
标志并且预览功能将来可能以不兼容的方式更改时,它才可用。
解
我不知道它将如何改变。 我什至不能说它会改变。 仅凭我个人的看法,如果仍然这样下去将是非常可悲的。 有了此功能,只要我们计算经验丰富的Java程序员可以编写的程序的精妙程度和可读性,Java就会是更好的语言。 但是,如果我们看看没有经验的,新鲜的初级人员如何弄糟代码,情况将会更糟。 以我的拙见,第二点更为重要,而Java在这方面有很强的优势。 Java不是一种黑客语言,您应该非常拼命编写一个非常不可读的代码。 我不希望它改变。
说完之后,我们可以看看技术上的可能性。 一种是放弃该功能,这实际上不是一个好的解决方案。 这实际上不是解决方案。
另一种可能性是将模式变量的范围限制为then
语句或else
语句。
这样,我们就不会依赖代码的“无法正常完成”功能。 else
保证只有在if
语句的条件为false
时才执行else
分支。 这将使解决方案不太优雅。
同样,另一种可能性是禁止模式变量遮盖任何字段变量。 它可以解决上面概述的问题,但是会引入一个不同的问题。 受此限制,当我们引入一个名为V
的新字段变量时,可能会发生带有方法和模式变量V
的现有类停止编译的情况。 至少此问题是编译时的问题,而不是运行时有问题的某些代码。
我宁愿有100个编译时错误,也不愿有1个运行时错误。
还有一种可能是放弃模式变量,而仅使用原始变量和扩展的类型信息,而当前的预览解决方案使用模式变量。 Kotlin粉丝会喜欢这种解决方案。 由于局部变量已经遮蔽(或不遮盖)字段变量,因此这也可以很好地消除阴影问题。 此解决方案的缺点是,重新作用域限定的变量类型在代码的不同位置将具有不同的类型。 让我们看下面的代码:
package javax0.jdk14.instanceof0; public class Sample2 {
public static class A {
public static void m(){
System.out.println( "A" );
}
}
public static class B extends A {
public static void m(){
System.out.println( "B" );
}
}
public static void main(String[] args) {
A a = new B();
if ( a B b){ ( a instanceof B b){
bm();
}
am();
} }
此代码将先打印出B
然后打印出A
因为根据变量b
的声明类型,对bm()
的调用与Bm()
相同,并且根据声明的类型,对am()
与Am()
的相同方法的变量a
。 省略模式变量并使用原始变量可能会造成混淆:
// NOT ACTUAL CODE
public static void main(String[] args) {
A a = new B();
if ( a B){ ( a instanceof B){
am();
}
am();
}
am()
会在不同的行上调用不同的方法吗?
如您所见,没有已知的最佳或最佳解决方案……除了一个。 在JDK中称您的代表为“议会”,并告诉他们那样不好。 (Psst:他们已经从原始推文中知道了。)
带走
这是一篇特别的文章,因为这与某些完善的Java功能或某些良好的编程工具或样式,模式,方法无关。 我们讨论了预览功能。 预览功能也许证明了我们为什么需要Java中的预览功能。
对于需要长期支持的长期商业项目,请使用最新的LTS版本。
将最新发布的Java版本用于您的实验和开源项目,并在用户需要时准备支持较旧的Java版本。
不要在项目中使用预览功能,也不要准备从代码中获得新版本,以防它们在变为非预览但正常功能时在下一个Java版本中发生更改。
尝试使用预览功能以将其包含在内,并在它们成为真实功能时具有某种肌肉记忆。 并且还可以向Java社区提供反馈,以防您觉得它们不是很完美。
翻译自: https://www.javacodegeeks.com/2020/01/jdk14-instance-of-ea-issue.html