【问题标题】:Are non-static fields static until they're changed in Java?非静态字段在 Java 中更改之前是静态的吗?
【发布时间】:2015-03-28 12:03:03
【问题描述】:

让我们考虑以下代码:

public class Testing {
    static int i = 47;

    public static void main(String[] args) {
        Testing t1 = new Testing();
        Testing t2 = new Testing();
        System.out.println(t1.i == t2.i);

我正在创建一个属于 Testing 类的静态字段,并且该字段在该类 t1t2 的两个实例之间共享。然后我测试它们是否引用了内存中的相同值,事实上,它们确实如此,结果是正确的。这对我来说很清楚。

但是,如果我从 int i 的声明中删除 static 关键字,就会发生意想不到的事情。

public class Testing {
    int i = 47;

    public static void main(String[] args) {
        Testing t1 = new Testing();
        Testing t2 = new Testing();
        System.out.println(t1.i == t2.i);

我希望 t1t2 这两个实例的字段值都为 47,但它们的字段位于不同的内存地址中。但令人惊讶的是,当测试 t1.i == t2.i 在这种情况下我也得到了正确的结果 - 为什么? int i = 47; 字段不再是静态的,因此我希望它对于类的每个实例都位于不同的内存地址中,但相等性会产生 true。

【问题讨论】:

  • == 测试值相等。
  • int 是“原始”,而不是参考。您只是在测试整数值。

标签: java memory reference


【解决方案1】:

int 是原始类型,而不是引用类型。条件t1.i == t2.i 不测试引用是否相等——这里首先没有引用。它只是比较值,在这种情况下,两者都具有值47

如果你有一个不是原语的成员字段,结果会有所不同,例如:

public class Testing {
    Integer i = new Integer(47);

    public static void main(String[] args) {
        Testing t1 = new Testing();
        Testing t2 = new Testing();
        System.out.println(t1.i == t2.i);   // false
    }
}

在这种情况下,每个实例都有一个对使用 new 关键字创建的 Integer 对象的不同引用,该对象调用构造函数,条件 t1.i == t2.i 比较这两个引用。

【讨论】:

  • 如果你确实想测试引用相等性,我相信 Java 中有一个 referenceEquals()
  • @BrianJ 我认为您无法访问本机类型的内存地址。
  • @AbbéRésina 原来我一直在考虑 C#。 Java 没有referenceEquals,它使用==,这意味着你一定是对的。
【解决方案2】:

关于为什么您的 == 测试仅适用于本机 int,即使您创建了 2 个实例,已经有了很好的答案。

我只想指出编译器在幕后可以做的一些奇怪的事情,这些事情可能会产生违反直觉的结果。考虑以下测试:

public class Foo {
    final Integer i = 47;
    final Integer j = 1234;
    public static void main(String args[]) {
        Foo p = new Foo();
        Foo q = new Foo();
        System.out.println(p.i.equals(q.i));
        System.out.println(p.i == q.i);
        System.out.println(p.j.equals(q.j));
        System.out.println(p.j == q.j);
    }
}

你期待

  • 要么是true, true, true, true,因为编译器很擅长识别,即使有两个Foo 实例,它们对于ij 具有相同的值。

  • 还是true, false, true, false,因为毕竟ijInteger的不同实例,而==比较的参考值应该不一样。

但令人惊讶的是你实际上得到了true, true, true, falseij 到底有什么区别?

好吧,如果你用javap -verbose Foo.class查看生成的代码,你会看到:

  stack=2, locals=1, args_size=1
     0: aload_0
     1: invokespecial #11                 // Method java/lang/Object."<init>":()V
     4: aload_0
     5: bipush        47
     7: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    10: putfield      #19                 // Field i:Ljava/lang/Integer;
    13: aload_0
    14: sipush        1234
    17: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    20: putfield      #21                 // Field j:Ljava/lang/Integer;
    23: return

编译器生成了使用Integer#valueOf(int) 的代码。文档指出:

此方法将始终缓存范围内的值 -128 到 127(含),并且可能缓存此范围之外的其他值

这解释了为什么 Foo 的 2 个实例实际上为 47 共享同一个 Integer 对象,而不是 1234

道德是:在比较对象时要小心==。在大多数情况下,您想要和需要的是equals()

【讨论】:

    【解决方案3】:

    int 的标识与对象的标识不同。

    对象的身份来自于同一个对象, 原始类型标识派生自相同的值 (47 == 47)。

    如果您将代码更改为Integer i = 47;,那么== 将不会拆箱整数,而是比较对象引用,结果将为假。

    如果您如图所示初始化 Integer(使用文字 47),自动装箱将从内部缓存中选择 Integer 对象。在身份比较 (==) 上,结果为真。如果您将至少一个整数设置为 Integer i = new Integer(47)`,则比较将失败,因为您现在创建了一个新对象。如果您超出缓存范围,使用带符号字节范围之外的文字初始化 Integer 也是如此。然后,运行时将为文字创建一个新的 Integer,并且身份比较将返回 false。

    我认为这是拆箱时的一个不好的警告,如果您真的想比较 Integer 的引用而不是值,则很难找到问题。 我使用以下代码来说明不同的行为。我现在认为使用 int 文字简单地实例化 Integer 并依赖自动装箱是一种坏习惯。这将对运行时行为产生细微的变化。此外,如果您处理对象,请在保存方面与.equals() 进行比较。

    public class Test {
        public static void main(String...args) {
            Integer h = new Integer(47); // Object created w/o boxing
            Integer i = new Integer(47);
            Integer j = 47; // Object created with boxing
            Integer k = 47; // due to  caching, this is the same Integer
    
            Integer j2 = 247; // Object created with boxing
            Integer k2 = 247; // no caching, these are different Integers
    
            int l = 47;  // primitives 
            int m = 47;
    
            // compare two explicit Objects 
            System.out.println((h == i) ? "true" : "false"); // false
    
            // compare one explicit Object with a autoboxed Object
            // compare is reference compare
            System.out.println((h == j) ? "true" : "false"); // false
            System.out.println((j == h) ? "true" : "false"); // false
    
            // compare two autoboxed Objects, compare is by reference
            // because value was in cache range, the Integers are identical
            System.out.println((k == j) ? "true" : "false"); // true 
    
            // compare two autoboxed Objects, compare is by reference
            // because value was not in cache range, these are two Objects of type Integer
            System.out.println((k2 == j2) ? "true" : "false"); // false 
    
            // adding a primitive to the compare will
            // always compare by value
            System.out.println((i == l) ? "true" : "false"); // true
            System.out.println((m == l) ? "true" : "false"); // true
        }
    }
    

    更新:我考虑了 cmets 并更新了我的示例以包含 intValue() 的意外缓存。

    【讨论】:

    • 现在我明白我错在哪里了,但我也检查了您提出的解决方案(将 int 更改为 Integer),但结果仍然正确,因此看起来发生了从 Integer 到 int 的自动转换。
    • 不应该如此,根据 oracle 文档:“== 运算符对 Integer 表达式执行引用标识比较,对 int 表达式执行值相等比较。”
    • 我已经测试过了,java编译器在拆箱方面似乎比以前聪明了。
    • @thst 看看我的回答,看看幕后发生了什么
    • @AbbéRésina 感谢您的解释。我更新了我的示例以反映 intValue 缓存。
    【解决方案4】:

    static 所做的只是告诉解释器对象/变量仅包含在该类中,而无需在使用之前定义该类的实例(对象)。

    发生这种情况的原因是因为这两个变量包含相同的值,因为它们都是ints 和值47,因此,比较它们会发现它们完全相同。

    如果你想比较两个非原始类的相等性,你可以使用这个:

    if(t1.equals(t2)){
    ...
    }
    

    否则,如果你想比较两个对象/变量的,你可以通过测试来做到这一点:

    if(t1.getClass() == t2.getClass()){
    ...
    }
    

    或者,如果您想检查它是否是另一个类的实例(扩展或继承自它),您可以使用:

    if(t1 instanceof int){
    ...
    }
    

    【讨论】:

    • “根据经验,最好将 public、private 或 protected 放在变量定义之前” - 不同意,有时需要默认可见性
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-19
    • 1970-01-01
    相关资源
    最近更新 更多