等号方法中的字符串实习生

时间:2011-11-14 10:08:24

标签: java

在类的equals方法中使用String#intern()是一个好习惯。假设我们有一个班级:

    public class A {
       private String field;
       private int number;
       @Override
       public boolean equals(Object obj) {
           if (obj == null) {
               return false;
           }
           if (getClass() != obj.getClass()) {
               return false;
           }
           final A other = (A) obj;   
           if ((this.field == null) ? (other.field != null) : !this.field.equals(other.field)) {
               return false;
           }
           if (this.number != other.number) {
               return false;
           }
           return true;
       }
   }

使用field.intern() != other.field.intern()代替!this.field.equals(other.field)会更快。

5 个答案:

答案 0 :(得分:4)

没有!像is not a good idea隐式使用String.intern()

  • 会更快。事实上,由于在后台使用哈希表,它会更慢。哈希表中的get()操作包含最终的相等性检查,这是您首先要避免的。像这样使用,每次为您的班级致电intern()时,都会调用equals()

  • String.intern()有很多内存/ GC影响,你不应该暗示强迫这个类的用户。

如果您想尽可能避免完全平等的检查,请考虑以下途径:

  • 如果您知道该字符串集是有限的并且您重复进行了相等检查,那么您可以在创建对象时使用intern()字段,以便任何后续的相等检查将归结为身份比较。

  • 使用显式HashMapWeakHashMap代替intern()以避免在GC永久生成中存储字符串 - 这是旧JVM中的一个问题,不确定它是否是仍然是一个有效的问题。

请注意,如果字符串集无限制,则 会出现内存问题。

那就是说,这听起来像对我来说过早优化。 String.equals()在一般情况下非常快,因为它在比较字符串本身之前比较字符串长度。你有没有想过你的代码?

答案 1 :(得分:4)

良好做法:不。你正在做一些棘手的事情,这会导致代码变得脆弱,不太可读。除非这个equals()方法需要具有疯狂的性能(并且您的性能测试验证它实际上更快),否则它是不值得的。

更快:可能。但请不要忘记使用intern()方法可能会产生意想不到的副作用:http://www.onkarjoshi.com/blog/213/6-things-to-remember-about-saving-memory-with-the-string-intern-method/

答案 2 :(得分:3)

通过String实习的相关成本,可能会超过对实习String执行身份比较所获得的任何好处。

在上面的例子中,您可以考虑在实例化类时实习String,前提是该字段是常量(在这种情况下,您还应将其标记为final)。您还可以在实例化时检查null,以避免检查每次调用equals(假设您不允许null String s。)

然而,通常这些类型的微优化提供的性能几乎没有增加。

答案 3 :(得分:2)

让我们一步一步完成这一步......

这里的想法是,如果您使用String#intern,您将获得String的规范表示。内部保留一个字符串池,并保证每个条目对于equals的该池是唯一的。如果你在一个字符串上调用intern(),那么将要返回一个先前合并的相同字符串,或者将要调用并返回你调用的字符串。

因此,如果我们有两个字符串s1s2,并且我们假设两个字符串都不为null,则以下两行代码被视为幂等:

s1.equals(s2);
s1.intern() == s2.intern();

让我们研究一下我们现在做出的两个假设:

    如果s1.intern()评估为s2.intern()
  1. s1.equals(s2)true确实会返回相同的对象。
  2. 在对同一个字符串的两个实习引用上使用==运算符比使用equals方法更有效。
  3. 第一个假设可能是最危险的。 JavaDoc for the intern method告诉我们使用此方法将返回内部保留的字符串池的规范表示。但它没有告诉我们关于该池的任何信息。一旦条目添加到池中,它是否可以再次删除?池会不会无限期地增长,还是会偶尔剔除条目以使其成为有限大小的缓存?您必须检查Java语言和虚拟机的实际规范才能获得任何确定性,如果他们提供它的话。必须检查有限优化的规格通常是一个重要的警告信号。检查Sun的JDK 7的源代码,我看到intern被指定为本机方法。因此,实施不仅可能是特定于供应商的,而且可能因平台以及来自同一供应商的VM而异。关于那些不在规范中的内容,所有投注都是关闭的。

    继续我们的第二个假设。让我们考虑一下实现String的实际内容......首先,我们需要检查字符串是否已经存在于池中。我们假设他们已经尝试通过使用一些散列方案来获得O(1)复杂性以保持这种速度。但是,假设我们已经获得了字符串的哈希值。由于这是一种本地方法,我不确定将使用什么...本机表示的一些哈希或简单地hashCode()返回的内容。我从Sun的JDK源代码中了解到,String实例缓存了它的哈希码。它仅在第一次调用方法时计算,之后将返回计算的值。所以至少,如果我们要使用它,必须至少计算一次哈希。获得String的可靠散列可能涉及对每个字符进行算术运算,这对于长度值来说可能是昂贵的。即使我们有哈希值,因此有一组字符串可以作为在实习池中进行匹配的候选者,我们仍然需要验证其中一个真正 是否完全匹配涉及......平等检查。意味着要经历字符串的每个字符,并查看它们是否匹配,如果不等长度这样的微不足道的情况不能首先应用。更糟糕的是,我们可能不得不为多个其他字符串执行此操作,就像我们使用常规equals一样,因为池中的多个字符串可能具有相同的哈希值或最终位于相同的哈希桶中

    所以,我们需要做的就是找出一个字符串是否已被实习,这听起来像是equals需要做的事情。基本上,我们一无所获,甚至可能使我们的equals实施更加昂贵。至少,如果我们每次都打电话给intern。所以也许我们应该马上实习String并且总是使用那个实习参考。如果是这种情况,让我们检查A类的外观。我假设String字段在构造时初始化:

    public class A {
    
        private final String field;
    
        public A(final String s) {
    
            field = s.intern();
    
        }
    
    }
    

    那看起来更明智一点。传递给构造函数且相等的任何字符串将最终成为相同的引用。现在我们可以安全地在A实例的==字段之间使用field进行等式检查,对吗?

    嗯,它没用。为什么?如果你检查类String中equals的来源,你会发现有半脑的人所做的任何实现都会先进行==检查,以捕捉实例和参数首先是相同的参考。这可以节省可能比较重的char-by-char比较。我知道我使用的JDK 7源代码就是这样做的。因此,您最好还是使用equals,因为无论如何都要进行参考检查。

    这个问题的第二个原因是第一点上面......我们根本不知道实例是否会无限期地保存在池中。检查这种情况,根据JVM实现可能会也可能不会发生:

    String s1 = ... //Somehow gets passed a non-interned "test" value
    A a1 = new A(s1);
    //Lots of time passes... winter comes and goes and spring returns the land to a lush green...
    String s2 = ... //Somehow gets passed a non-interned "test" value
    A a2 = new A(s2);
    a1.equals(a2); //Totally returns the wrong result
    

    发生什么事了?好吧,如果事实证明,实际的字符串池有时会被某些条目剔除,那么A的第一个构造可以s1实习,只有看到它被从池中移除,才能它后来被s2实例取代。由于s1s2可能是不同的实例,因此==检查失败。这会发生吗?我不知道。我当然不会去检查规格和本机代码以找出答案。程序员是否会通过调试器查看您的代码,以找出地狱"test""test"不一样的原因?

    如果我们使用equals,这没问题。它会提前捕获相同的实例案例以获得最佳结果,这将使我们在使用我们的字符串时受益,但我们不必担心实例最终会因为不同而不同的情况然后equals将进行经典的比较工作。它只是表明最好不要再猜测实际的运行时实现或编译器,因为这些东西是由那些知道规格的人做出的,比如他们手中的后背并且真的担心性能。

    因此,当您确保...

    时,手动字符串实习可能会有所帮助
    • 你不会每次都实习,但只是实习一个字符串就像初始化一个字段然后继续使用那个实习的实例一样;
    • 您仍然使用equals来确保实施细节不会破坏您的一天,而您的代码实际上并不依赖于该实习,而是依靠该方法的实施来捕获琐碎的事实例。

    记住这一点后,肯定值得使用intern()吗?好吧,我们还不知道intern()有多贵。它是一种原生方法,所以它可能非常快。但除非我们检查目标平台和JVM实现的代码,否则我们不确定。我们还必须确保我们确切了解实习的内容以及我们对此做出的假设。您确定阅读代码的下一个人具有相同的理解水平吗?他们可能会对这种奇怪的方法感到困惑,他们以前从未见过这种方法涉及JVM内部,可能花一个小时阅读我现在打字的同样的胡言乱语,而不是完成工作。

    那就是问题......之前,这很简单。您使用了equals并完成了。现在,你已经添加了另一件可以萦绕在你脑海中的小东西,让你有一天晚上醒来,因为你已经意识到哦,天哪,你忘了带==使用中的一个,并且在控制杀手机器人的例程中使用该段代码。对公民不服从的厌恶,你听说它的JVM不太稳固!

    唐纳德·克努特(Donald Knuth)将这句引言归结为着名......

    "我们应该忘记效率很低,大约97%的时间说:过早的优化是所有邪恶的根源"

    Knuth非常聪明,可以添加97%的细节。有时,彻底微观优化一小部分代码可以产生很大的不同。比如说,如果那段代码占用了程序运行时执行的30%。微优化的问题在于它们倾向于在假设上工作。当你开始使用intern()并相信从那时起它可以安全地进行参考平等检查,你已经做了很多假设。即使你下到实施水平来检查他们是否正确,你确定他们会在下一个JRE版本中吗?

    我自己手动使用intern()。在一些代码中,同样少数字符串会在数百个(如果不是数千个)对象实例中作为字段结束。这些字段将用作HashMaps中的键,并且在对这些实例进行一些验证时经常使用。我认为实习对于两个目的是值得的:通过使所有那些相等的字符串成为单个实例来减少内存开销并加速地图查找,因为他们使用hashCode()equals。但我已经确定你可以从代码中取出所有intern()个调用,一切都会正常工作。在这种情况下,实习只是一些锦上添花,有点额外可能会或可能不会在路上产生一些差异。但它并不是我的代码正确性的重要组成部分。

    长篇大论,是吗?为什么我会遇到输入所有这些的麻烦?为了告诉你如果你进行微观优化,你最好知道你做了什么,并且愿意如此彻底地记录它,以至于你可能不会感到困扰。

答案 4 :(得分:0)

鉴于您尚未指定硬件,这很难说。时间测试很难正确,并不是普遍的。你自己做过计时测试吗?

我的感觉是实习生模式不会更快,因为每个字符串都需要与所有实习字符串的字典中的可能字符串匹配。