2011-11-14 56 views
2

在類的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)

回答

4

不!使用String.intern()隱含這樣is not a good idea

  • 它將更快。事實上,由於在後臺使用哈希表,它會變慢。一個散列表中的get()操作包含最終的相等性檢查,這是您首先要避免的。像這樣使用,intern()將每次打電話equals()爲您的班級。

  • String.intern()有很多內存/ GC的影響,你不應該暗示強迫這個類的用戶。

如果你想避免完全成熟的平等檢查可能的情況下,可以考慮以下途徑:

  • 如果您知道這組字符串是有限的,你有反覆平等檢查,您可以在創建對象時使用intern()作爲字段,以便任何後續的相等性檢查將歸結爲身份比較。

  • 使用一個明確的HashMapWeakHashMap代替intern()可避免儲存在GC串永久代 - 這是較舊的JVM的一個問題,不知道,如果它仍然是一個有效的關注。

請記住,如果設定的字符串是無界的,你有記憶的問題。

這就是說,這一切聽起來像是對我不成熟的優化。 String.equals()在一般情況下非常快,因爲它在比較字符串本身之前比較字符串長度。你有沒有分析你的代碼?

+2

在現代JVM中,permgen空間被垃圾收集。 –

0

這很難說,因爲你沒有指定硬件。定時測試很難得到正確的,並不普遍。你自己做了時間測試嗎?

我的感覺是,實習生模式不會更快,因爲每個字符串都需要與所有實習字符串字典中的可能字符串進行匹配。

3

通過在實習生String上執行身份比較可獲得的任何好處都可能超過實習String的相關費用。

在上述情況下,您可以考慮在實例化類時實施String,如果該字段不變(在這種情況下,您還應將其標記爲final)。您還可以檢查實例化中的null,以避免檢查每次致電equals(假設您不允許nullString s)。

但是,通常這些類型的微優化在性能上幾乎沒有什麼收穫。

2

讓我們通過這一步一步的時間...

這裏的想法是,如果你使用String#intern,你將被給予該String的標準表示。一個字符串池保存在內部,並且每個條目保證對於該池在equals方面是唯一的。如果你在一個字符串上調用intern(),那麼要麼返回一個以前合併的相同字符串,要麼將你的Intern所調用的字符串合併並返回。

因此,如果我們有兩個字符串s1s2,我們也不承擔爲空,然後將下面的代碼兩行被認爲是冪等:

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

讓我們研究一下兩個假設我們現在已經提出:

  1. s1.intern()s2.intern()真的會返回相同的對象,如果s1.equals(s2)計算爲true
  2. 使用==運算符對兩個相同字符串的實參引用將比使用equals方法更有效。

第一個假設可能是最危險的。 JavaDoc for the intern method告訴我們,使用此方法將返回內部保留的字符串池的規範表示形式。但它並沒有告訴我們有關該池的任何信息。一旦條目添加到池中,它是否可以再次移除?池會無限期地增長,還是偶爾會被剔除以使其充當有限大小的緩存?你必須檢查Java語言和虛擬機的實際規格才能得到確定性,如果他們提供的話。不得不檢查規範的有限優化通常是一個很大的警告信號。檢查Sun的JDK 7的源代碼,我發現intern被指定爲本地方法。因此,不僅實施可能是針對特定供應商的,而且可能因平臺不同而出現同一供應商的虛擬機。所有投注都不符合規範。

關於我們的第二個假設。讓我們考慮一下實習String需要花些什麼......首先,我們需要檢查String是否已經在池中。我們假設他們已經試圖通過使用一些散列方案來獲得O(1)複雜性,以保持這一速度。但是,這是假設我們已經得到了字符串的散列。由於這是一種本地方法,我不確定將使用什麼......本地表示的一些散列或簡單地返回什麼。我從Sun的JDK的源代碼知道一個String實例緩存了它的哈希碼。它只會在第一次調用該方法時計算出來,然後計算出的值將被返回。所以至少,如果我們要使用它,則必須至少計算一次散列。獲取字符串的可靠散列可能涉及對每個字符進行算術運算,這對於長度值可能會很昂貴。即使我們有散列,因此也有一組字符串可以作爲候選人在被限制的池中進行匹配,但我們仍然必須驗證其中一個真正的是否爲完全匹配,這將涉及...平等檢查。意思是貫穿字符串的每個字符,並且看看它們是否匹配,如果像不等長度這樣的小事例不能首先應用。更糟糕的是,我們可能必須爲多個其他字符串執行此操作,就像我們使用常規的equals那樣執行此操作,因爲池中的多個字符串可能具有相同的散列或最終在相同的散列存儲區中。

所以,我們需要做的事情,以確定一個字符串是否已被實施或不聽起來像equals需要做的事情。基本上,我們一無所獲,甚至可能使我們的equals實施更加昂貴。至少,如果我們打算每次打電話intern。所以,也許我們應該馬上實習字符串,並且總是使用該實際參考。讓我們來看看如果是這種情況,A級會如何看待。我假設字符串字段在構造上初始化:

public class A { 

    private final String field; 

    public A(final String s) { 

     field = s.intern(); 

    } 

} 

這看起來更合理一些。任何傳遞給構造函數並且相等的字符串將最終成爲相同的引用。現在我們可以安全地使用==之間的A實例的field字段進行等式檢查,對吧?

那麼,這是沒用的。爲什麼?如果您在類String中檢查equals的源代碼,您會發現任何半腦人員所做的任何實現都將首先執行==檢查來捕獲實例和參數首先是同一個參考的普通情況。這可以節省一個可能很重的逐字符比較。我知道我用作參考的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會去做經典比較工作。它只是表明最好不要對實際的運行時實現或編譯器進行二次猜測,因爲這些事情是由那些知道規格(如手背)並且確實擔心性能的人制造的。

所以字符串手動實習可以是有益的,當你確保...

  • 你沒有實習每一次,但只是實習生intializing字段時像一個String一次,然後繼續使用該實例;
  • 您仍然使用equals來確保實現細節不會毀了您的一天,您的代碼實際上並不依賴於該實習,而是依靠該方法的實現來捕捉微不足道的情況。

記住這一點後,當然值得使用intern()?那麼,我們仍然不知道intern()是多麼昂貴。這是一種本地方法,所以它可能非常快。但除非我們檢查我們的目標平臺和JVM實現的代碼,否則我們不確定。我們還必須確保我們確切瞭解實習的內容以及我們對此做出的假設。你確定下一個閱讀你的代碼的人會有相同的理解程度嗎?他們可能會對這種他們以前從未見過的奇怪的方法感到困惑,因爲他們在JVM內部實現了這個方法,並且可能花一個小時閱讀我現在正在輸入的同一個亂碼,而不是完成工作。

這就是問題所在......之前,它很簡單。您使用了equals並完成。現在,你已經添加了另一個小東西,可以在你的頭腦中隱藏自己,並讓你醒來一個晚上尖叫,因爲你剛剛意識到,哦,我的上帝,你忘記了==用途之一,並且一段代碼被用於控制殺人殭屍對公民不服從行爲的常規程序中,並且你聽說它的JVM不是太固體!

高德納是著名歸因報價...

「我們應該忘記小的效率,講的時候約97%:過早的優化是所有罪惡的根源」

Knuth非常聰明,足以在97%的細節中加入。有時,徹底微觀優化一小部分代碼可以產生很大的不同。假設這段代碼佔用了程序運行時執行的30%。微觀優化的問題在於他們傾向於假設。當你開始使用intern()並且認爲從那時開始進行參考平等檢查是安全的,你已經做了很多假設。而且,即使你到實施層面檢查它們是否正確,你確定它們將在下一個JRE版本中嗎?

我自己手動使用了intern()。在一些代碼中,相同的一小部分字符串最終會以幾百甚至幾千個對象實例作爲字段結束。這些字段將在HashMaps中用作關鍵字,並且在對這些實例進行驗證時經常使用。我認爲實習是值得的,其目的有兩個:通過使所有這些字符串等於一個單一實例並加快地圖查找來減少內存開銷,因爲他們使用的是hashCode()equals。但是我確信你可以將所有這些intern()調用從代碼中取出,並且一切都可以正常工作。在這種情況下,實習僅僅是一些錦上添花,有些額外的東西可能會或可能不會在道路上有所改變。但這不是我的代碼正確性的重要部分。

長帖子,呃?爲什麼我經歷了將所有這些打字的麻煩?爲了向你展示,如果你進行微觀優化,你最好知道你在做什麼,並且願意如此徹底地記錄它,以至於你可能沒有打擾過。

+0

很好的詳細分析! –