为啥 Object 和 var 变量的行为不同?

Posted

技术标签:

【中文标题】为啥 Object 和 var 变量的行为不同?【英文标题】:Why do Object and var variables behave differently?为什么 Object 和 var 变量的行为不同? 【发布时间】:2020-04-04 12:53:46 【问题描述】:

有人可以解释o2 的行为吗?是因为编译器优化吗?它是否记录在 JLS 的某个地方?

public class Test 
    public static void main(String[] args) 
        Object o1 = new Object() 
            String getSomething() 
                return "AAA";
            
        ;
        // o1.getSomething(); // FAILS
        String methods1 = Arrays.toString(o1.getClass().getMethods());        
        var o2 = new Object() 
            String getSomething() 
                return "AAA";
            
        ;        
        o2.getSomething(); // OK        
        String methods2 = Arrays.toString(o2.getClass().getMethods());        
        System.out.println(methods1.equals(methods2));
        

产生的输出是

是的

[更新]

经过一些富有成效和有用的讨论后,我想我可以理解这种行为(如果我的假设是错误的,请发布 cmets)。

首先,感谢 @user207421,他解释说 Java 编译器将 o2 的类型视为与 RHS 相同,其中:

扩展ObjectgetSomething方法

然后感谢 @Joachim Sauer 指出了 JLS 中的正确位置。

更多相关的 JLS 引用:

局部变量的类型是 T 相对于 T 提到的所有合成类型变量的向上投影(第 4.10.5 节)。

在确定变量的类型时,向上投影应用于初始化器的类型。如果初始化器的类型包含捕获变量,则此投影将初始化器的类型映射到不包含捕获变量的超类型。

虽然允许变量的类型提及捕获变量是可能的,但通过将它们投影出来,我们强制执行一个有吸引力的不变量,即捕获变量的范围永远不会大于包含其类型被捕获的表达式的语句。非正式地,捕获变量不能“泄漏”到后续语句中。

问题:我们可以说“捕获变量”在问题的上下文中也指的是getSomething()吗?

最后,感谢 @Slaw 指出getSomething 被声明为私有包,因此getMethods 没有返回。

感谢任何 cmets/更正。

【问题讨论】:

这是因为o1的类型显式声明为Object,而o2的类型与RHS相同,extends Object并且有额外的方法,所以你可以调用它。 @user207421 我喜欢你的解释“RHS”,这可以更详细地显示如下:new Object() String getSomething() return "AAA"; .getSomething(); 但我很想看看它在哪里被清楚地记录了。 您完全歪曲了我在回答中所说的话,因此它根本不符合我所说的。您对编译器实际执行的操作的误解也不正确。它所做的是将o2type 指定为与​​RHS 的type 相同,这正是var 所做的。这在 JLS 中有记录。 @user207421 抱歉,它确实表达了我相信你并没有说的话,当我制作演示代码 sn-p 时,我的意思是概念上的,而不是字面意思。无论如何,我再次更新了问题。 另见this Q&A。 【参考方案1】:

Object 没有方法getSomething。由于o1Object 类型,编译器将不允许您调用o1.getSomething

o2 的情况下,变量的类型是您在初始化期间创建的匿名内部类型。该类型有一个getSomething 方法,因此编译器将允许您调用它。

有趣的是,这是通过命名类型无法直接完成的。您在 o2 的声明中没有使用类型名称来获得相同的效果,因为该类型是匿名的。

在JLS 14.4.1 Local Variable Declarators and Types 中定义。具体这部分:

如果 LocalVariableType 是 var,那么当 T 被视为没有出现在赋值上下文中时,令 T 为初始化表达式的类型,因此它是一个独立的表达式(第 15.2 节)。

下面还有一个例子说明:

var d = new Object() ;  // d has the type of the anonymous class

【讨论】:

我希望得到更多技术性的答案,两者都是 内部匿名类型。我的问题是为什么编译器不抱怨 o2(我确定它为什么抱怨 o1) @BaratSahdzijeu:我不太明白:o1 的类型是Object,而Object 没有getSomething 方法。这几乎是技术性的......o2 的类型是 not Object,但是您在其初始化时创建的未命名类型......不同之处在于 类型变量不在分配给它的值的类型 当然我理解这种行为导致了Object 变量类型与var 类型名称。你能解释一下为什么在这种情况下,两个对象的方法比较相同,而 btw getSomething 不在其中吗? @BaratSahdzijeu:当您调用getClass() 时,您会查看变量引用的对象的实际类型。这取决于保存引用的变量的类型。 @Barat 该方法可通过反射检索。在您的问题中,您调用 #getMethods() 仅返回 public 方法。如果您要使用#getDeclaredMethods(),那么您会看到您正在寻找的方法。文档更详细。【参考方案2】:

JEP 286: Local-Variable Type Inference 状态中表示为 Non-Denotable Types 的部分:

匿名类类型不能命名,但它们很容易命名 理解——它们只是类。允许变量具有匿名性 类类型为声明单例引入了一种有用的简写 本地类的实例。我们允许他们。

因此,考虑到已创建类实例并推断作为匿名类进一步允许调用该方法,允许使用var 调用的方法进行编译。

规范的Local Variable Declarators and Type 部分在示例中也提到了这一点:

var d = new Object() ;  // d has the type of the anonymous class

请注意,有些用 var 声明的变量不能用 显式类型,因为变量的类型是不可表示的。

另一方面,在第一个实例中,您尝试执行的操作看起来像 Invoking a method of an anonymous class,但由于 o1 的类型被推断为 Object 并且进一步没有方法而失败叫getSomething。如果您要调用方法 getSomething 并在那里修复编译,您可以使用

Object o1 = new Object() 
  String getSomething() 
    System.out.println("something happened");
    return "AAA";
  
.getSomething();

【讨论】:

我建议去 JLS 了解确切的规范,JEP 是将其包含在 JLS 中的过程,有时措辞可能会有所不同...... @JoachimSauer 肯定会在一些空闲时间做到这一点。现在,引用我记得读过的内容。 @JoachimSauer 我可能编辑得太晚了(我可以看到您的回答中引用了 JLS),但我引用了与您的回答相得益彰的行,我希望它值得作为注释供未来的读者阅读。

以上是关于为啥 Object 和 var 变量的行为不同?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 python 对变量有这种行为?

为啥猫鼬用“[object Object]”替换子文档?

一.Dart语法-操作符、方法与异常

为啥 $var 和 vars.get('var') 没有返回相同的值?

C#的一些小知识

Javascript ES6'let'和'var' - 函数内部的意外行为,参数名称匹配重新声明的变量