【问题标题】:How use of == operator brings in performance improvements compared to equals?与 equals 相比,使用 == 运算符如何提高性能?
【发布时间】:2013-11-30 09:43:52
【问题描述】:

在 Joshua Bloch 的 Effective JAVA 中,当我阅读静态工厂方法时,有如下声明

静态工厂方法返回相同对象的能力 重复调用允许类保持严格的控制 任何时候都存在哪些实例。据说这样做的类是 实例控制。写作有几个原因 实例控制的类。实例控制允许一个类 保证它是单例(第 3 项)或不可实例化(第 4 项)。 此外,它允许不可变类(第 15 条)做出保证 不存在两个相等的实例:a.equals(b) 当且仅当 a==b。如果 一个类做出这个保证,然后它的客户可以使用 == 运算符而不是 equals(Object) 方法,这可能会导致 改进的性能。枚举类型(第 30 项)提供了这种保证。

要研究 == 运算符如何带来性能改进, 我得看看String.java

我看到了这个sn-p

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String) anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                            return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

这里所说的性能改进是什么意思?它如何带来性能提升。

他是不是想说下面的意思

如果每个类都可以确保 a.equals(b) 当且仅当 a==b 时,这意味着它带来了一个间接要求,即不能有对象引用 2 个不同的内存空间并且仍然持有相同的数据,这是内存浪费。如果它们持有相同的数据,它们就是同一个对象。也就是说,它们指向相同的内存位置。

我的推断是否正确?

如果我错了,你能指导我理解这一点吗?

【问题讨论】:

  • 但这是否意味着 String a = "xyz";和 String b ="xyz" 一起是浪费内存,是不可能的?这不是用例吗?
  • 字符串字面量始终在 Java 中。所以你会发现,在你的例子中,a == b.

标签: java performance equals referenceequals


【解决方案1】:

如果每个类都可以确保 a.equals(b) 当且仅当 a==b ,这意味着它带来了一个间接要求,即不能有对象引用 2 个不同的内存空间并且仍然保存相同的数据,这是内存浪费。如果它们持有相同的数据,它们就是同一个对象。也就是说,它们指向相同的内存位置。

是的,这就是作者的目的。

如果可以(对于给定的类,这对所有人来说都是不可能的,特别是它不适用于可变类)调用 ==(这是单个 JVM 操作码)而不是 equals(这是动态调度的方法调用),它节省了(一些)开销。

例如,enums 就是这样工作的。

即使有人调用了equals 方法(这将是一种很好的防御性编程实践,恕我直言,您不想养成使用== 对象的习惯),该方法也可以作为一个简单的实现==(而不必查看潜在的复杂对象状态)。

顺便说一句,即使对于“普通”的 equals 方法(例如 String 的),在其实现中首先检查对象身份然后查看对象状态的快捷方式(这就是 String#equals 所做的)可能是一个好主意,正如你所发现的)。

【讨论】:

  • +1 以提供enum 作为此作用 作用的示例。
  • 我非常不同意关于cannot work for mutable classes 的部分。实际上,每当您将可变对象放入集合时,它就会停止工作equals。引用相等总是起作用,只是语义不同。
  • @maaartinus Ew。如果有其他可变字段,我永远不会创建仅比较 idequals 方法,因为这些是对象可观察状态的一部分。
  • @maaartinus 这取决于您认为您的类型是“值类型”还是“引用类型”。我觉得数据访问对象是值类型。在我们构建代码的方式中,我们使用Integer 作为映射键,而不是数据访问对象本身。我们有一个单独的缓存(通常带有软引用值)来查找这些对象,如果它们重新获取的成本很高。
  • @maaartinus:如果那个设计适合你,那太好了,尽管对我来说没有意义。在我看来,对象通常应该封装数据,在这种情况下,只有完全匹配的对象应该相等,或者实体,在这种情况下,只有表示同一实体的对象应该相等。您的对象听起来像是一种奇怪的混合体,我想将其拆分为单独的实体部分和数据部分,但也许您可以不进行这样的细分。
【解决方案2】:

引用部分的意思是不可变类可以选择intern 其实例。这很容易通过 Guava 的Interner 实现,例如:

public class MyImmutableClass {
    private static final Interner<MyImmutableClass> INTERN_POOL = Interners.newWeakInterner();
    private final String foo;
    private final int bar;

    private MyImmutableClass(String foo, int bar) {
        this.foo = foo;
        this.bar = bar;
    }

    public static MyImmutableClass of(String foo, int bar) {
        return INTERN_POOL.intern(new MyImmutableClass(foo, bar));
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(foo, bar);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;        // fast path for interned instances
        if (o instanceof MyImmutableClass) {
            MyImmutableClass rhs = (MyImmutableClass) o;
            return Objects.equal(foo, rhs.foo)
                    && bar == rhs.bar;
        }
        return false;
    }
}

这里,构造函数是私有的:所有实例都必须通过MyImmutableClass.of()工厂方法,它使用Interner来确保如果新实例是equals()到现有实例,则现有实例是而是返回。

Interning 只能用于不可变对象,我指的是具有可观察状态的对象(即所有外部可访问方法的行为,尤其是equals()hashCode())在对象的生命周期内不会改变。如果你实习可变对象,当实例被修改时,行为将是错误的。

正如许多其他人已经说过的,您应该谨慎选择要实习的对象,即使它们是不可变的。仅当实习值集相对于您可能拥有的重复项数而言较小时才执行此操作。例如,一般不值得实习Integer,因为有超过 40 亿个可能的值。但值得实习最常用的Integer 值,事实上,Integer.valueOf() 实习生值介于 -128 和 127 之间。另一方面,枚举非常适合实习生(根据定义,它们是实习生的),因为可能值的集合很小。

一般来说,对于大多数课程,您必须进行堆分析,例如使用jhat(或者,插入我自己的项目,fasthat),以确定是否有足够的重复项来保证实习。在其他情况下,请保持简单,不要实习。

【讨论】:

  • 是的。我想要澄清一下。在阅读了你的答案后,我看到了blog.codecentric.de/en/2012/03/…,它说“请注意,所有硬编码的字符串(作为常量或代码中的任何位置)都会被编译器自动执行。 " .所以我的问题是字符串实习什么时候发生?总是?当字符串实习不会发生时,该注释是否间接给出了一些提示?
  • @Harish Kayarohanam:字符串暂留没有自动的过程。只是在您的源代码中显示为常量的所有字符串已经在实习池中开始了它们的生命。每当您写String a = "x", b = "x"; 时,您就可以确定a == b 成立。我猜类似Something.class.getName() 这样的东西。每当你打电话给s.substring(),你可以打赌它不会自动被实习。
  • @Chris Jester-Young:我会稍微完善您关于可变类的陈述。只要equals 中没有包含的字段发生变化,它就可以工作(这里无需谈论hashCode,因为其中的所有字段也必须包含在equals 中)。实习可变对象与将它们用作集合中的键具有完全相同的问题。
  • @maaartinus 你能指导我看一些可以解释这些实习和可变性概念的视频教程或资源吗?
  • @Harish Kayarohanam:我怀疑除了JLS 之外还有什么。请注意,实习并不像看起来那么酷。正如另一个答案所指出的那样,它的成本很高,而且很少值得。请注意过早优化。你可能想看看hash code caching
【解决方案3】:

如果你能保证不存在一个对象的两个实例,使得它们的语义值是等价的(即,如果xy 引用不同的实例[x != y] 那么x.equals(y) == false 表示所有xy),那么这意味着您可以通过检查它们是否引用同一个实例来比较两个引用的对象是否相等,这就是== 所做的。

== 的实现本质上只是比较两个整数(内存地址),通常比.equals() 的几乎所有非平凡实现都快。

值得注意的是,这不是可以为Strings 进行的跳转,因为您不能保证String 的任何两个实例不相等,例如:

String x = new String("hello");
String y = new String("hello");

由于x != y &amp;&amp; x.equals(y),仅执行x == y 来检查是否相等是不够的。

【讨论】:

  • 如果想要为字符串提供这个系统,则需要重新规范 JVM 以在所有新字符串对象上调用 intern== 确实适用于实习字符串)。
  • 但是您的示例是错误的,因为xy 都指向同一个字符串文字,并且字符串文字总是在Java 中被实习。如果您将示例更改为至少对其中一个变量使用new String("hello"),那么您对x != y 的断言确实是正确的。
  • 我要从代码中推断出什么 public class HelloWorld{ public static void main(String []args){ String x = new String("hello");字符串 y = 新字符串(“你好”); System.out.println(x == y);字符串 a = "你好";字符串 b = "你好"; System.out.println(a == b); } } o/p false true 为什么在第一种情况下不会发生实习 new String()。是不是因为我们使用 new String() 明确地请求一个新实例;
  • 是的,我在这里找到了解决方案。 ntu.edu.sg/home/ehchua/programming/java/J3d_String.html
  • 即使对 Java 对象、语言和“新”的了解很少,您也可以自己得出这个结论。你真的需要回到语言基础,查看 oracle 网站上的官方教程。
【解决方案4】:

回答你的问题……

这里提到的性能改进是什么意思 [String]?它如何带来性能提升。

这不是布洛赫所说的一个例子。 Bloch 说的是实例控制的类,而String 不是这样的类!

我的推论正确吗?

是的,这是正确的。实例不可变的实例控制类可以确保根据== 运算符,“相同”的对象将始终相等。

一些观察结果:

  • 这仅适用于不可变对象。或者更准确地说是突变不影响相等语义的对象。

  • 这仅适用于完全实例控制的类。

  • 实例控制可能很昂贵。考虑由 String 类的intern 方法和字符串池提供的(部分)实例控制的形式。

    • 字符串池实际上是对字符串对象的弱引用的哈希表。这会占用额外的内存。

    • 每次你实习一个字符串时,它都会计算字符串的哈希码并探测哈希表以查看是否已经实习过类似的字符串

    • 每次执行完整 GC 时,字符串池中的弱引用都会导致 GC 进行额外的“跟踪”工作,如果 GC 决定中断引用,则可能会进行更多工作。

    当您实现自己的实例控制类时,您通常会获得类似的开销。当您进行成本效益分析时,这些开销计入更快的实例比较的好处。

【讨论】:

    【解决方案5】:

    我认为是这个意思:

    如果您需要测试两个复杂结构的相等性,您通常需要进行大量测试以确保它们相同。

    但是,如果由于语言的某些技巧,您知道两个复杂但相等的结构不能同时存在,那么您可以验证它们是否在内存中的相同位置,而不是通过逐位比较来验证相等性如果不是,则返回 false。

    如果任何人都可以创建对象,那么您不能保证不能创建两个相同但不同的对象。但是如果您控制对象的创建并且只创建不同的对象,那么您就不会需要复杂的相等性测试。

    【讨论】:

      【解决方案6】:

      在使用对不可变对象的引用来封装复杂值的情况下,比较两个引用时通常会出现三种情况:

      • 它们是对同一个对象的引用(非常快)

      • 它们是对封装不同值的不同对象的引用(通常很快,但有时很慢)

      • 它们是对封装相同值的不同对象的引用(通常总是很慢)

      如果发现对象经常相等,则将情况 3 的频率最小化可能具有重要价值。如果对象通常非常接近相等,则确保慢速子情况也具有重要价值的情况 2 不经常发生。

      如果确定对于任何给定的值,拥有该值的对象永远不会超过一个,那么观察到两个引用标识不同对象的代码可能会推断它们封装了不同的值,而不必实际检查其中的值题。然而,这样做的价值通常是有限的。如果所讨论的对象是大型、复杂、嵌套的集合,有时会非常相似,则可以让每个集合计算并缓存其内容的 128 位散列;具有不同内容的两个集合不太可能具有匹配的哈希值,并且具有不同哈希值的集合可能很快被识别为不相等。另一方面,让封装相同内容的引用通常标识到同一个对象,即使存在对相同集合的一些引用,也可以提高否则总是不好的“等于”的性能案例。

      如果不想使用单独的实习集合,可以使用的一种方法是让每个对象保留一个long 序列号,以便始终可以确定首先创建两个其他相同对象中的哪一个,以及对已知包含相同内容的最旧对象的引用。要比较两个引用,首先要确定已知与每个引用等效的最旧对象。如果已知与第一个匹配的最旧对象与已知与第二个匹配的最旧对象不同,则比较对象的内容。如果它们匹配,一个将比另一个更新,并且该对象可以将另一个视为“已知匹配的最旧对象”。

      【讨论】:

        猜你喜欢
        • 2013-07-23
        • 2011-04-19
        • 2019-02-12
        • 2014-09-09
        • 2012-11-03
        • 2021-06-19
        • 1970-01-01
        • 2021-10-08
        相关资源
        最近更新 更多