簡體   English   中英

String.intern()vs手動字符串到標識符映射?

[英]String.intern() vs manual string-to-identifier mapping?

我記得看到幾個字符串密集型程序進行了大量的字符串比較,但相對較少的字符串操作,並且使用單獨的表將字符串映射到標識符以實現有效的相等性和更低的內存占用,例如:

public class Name {
    public static Map<String, Name> names = new SomeMap<String, Name>();
    public static Name from(String s) {
        Name n = names.get(s);
        if (n == null) {
            n = new Name(s);
            names.put(s, n);
        }
        return n;
    }
    private final String str;
    private Name(String str) { this.str = str; }
    @Override public String toString() { return str; }
    // equals() and hashCode() are not overridden!
}

我很確定其中一個程序是來自OpenJDK的javac,所以不是一些玩具應用程序。 當然實際的類更復雜(而且我認為它實現了CharSequence),但是你明白了 - 整個程序在你期望String任何位置都充斥着Name ,並且在極少數需要字符串操作的情況下,它轉換為字符串,然后再次緩存它們,在概念上如下:

Name newName = Name.from(name.toString().substring(5));

我想我明白了這一點 - 尤其是當有很多相同的字符串和很多比較時 - 但是通過使用常規字符串並intern它們可能無法實現相同的目標嗎? String.intern()文檔明確說:

...
調用實習方法時,如果池已經包含等於此字符串對象的字符串(由equals(Object)方法確定),則返回池中的字符串。 否則,將此String對象添加到池中,並返回對此String對象的引用。

因此,對於任何兩個字符串s和t,當且僅當s.equals(t)為真時,s.intern()== t.intern()才為真。
...

那么, 什么是手動管理的優點和缺點Name狀類VS使用intern()

到目前為止我所想到的是:

  • 手動管理地圖意味着使用常規堆, intern()使用permgen。
  • 當手動管理地圖時,您喜歡可以驗證某些東西是Name類型檢查,而實習字符串和非實習字符串共享相同的類型,因此可能會忘記在某些地方實習。
  • 依賴於intern()意味着重用現有的,經過優化的,經過試驗和測試的機制,而無需編寫任何額外的類。
  • 手動管理地圖會導致代碼對新用戶更加困惑,並且strign操作變得更加麻煩。

......但我覺得我在這里缺少別的東西。

不幸的是, String.intern()可能比簡單的同步HashMap慢。 它不需要那么慢,但是到今天在甲骨文的JDK中,它很慢(可能是由於JNI)

另一件需要考慮的事情是:你正在編寫一個解析器; 你在char[]收集了一些字符,你需要用它們制作一個字符串。 由於字符串可能很常見並且可以共享,因此我們想使用池。

String.intern()使用這樣的池; 要查找,你需要一個字符串開頭。 所以我們首先需要new String(char[],offset,length)

我們可以避免自定義池中的開銷,其中可以基於char[],offset,length直接進行查找。 例如,游泳池是特里 字符串最有可能在池中,因此我們將獲得沒有任何內存分配的String。

如果我們不想編寫自己的池,但使用舊的HashMap,我們仍然需要創建一個包裝char[],offset,length (類似CharSequence)的密鑰對象。 這仍然比新的字符串便宜,因為我們不復制字符。

手動管理類似名稱的類與使用實習生()的優點和缺點是什么?

類型檢查是一個主要問題,但不變保存也是一個重要問題。

Name構造函數中添加一個簡單的檢查

Name(String s) {
  if (!isValidName(s)) { throw new IllegalArgumentException(s); }
  ...
}

可以確保*沒有Name實例對應於無效名稱,如"12#blue,,"這意味着將Name s作為參數並且使用其他方法返回的Name s的方法不需要擔心無效Name可能會蔓延。

為了概括這個論點,想象一下你的代碼是一個帶有牆壁的城堡,旨在保護它免受無效輸入的影響。 您需要一些輸入才能通過,因此您需要使用警衛來安裝門,以便在輸入時檢查輸入。 Name構造函數是一個后衛​​的示例。

之間的區別StringNameString s不能被防御。 外圍內外的任何惡意或天真代碼都可以創建任何字符串值。 Buggy String操作代碼類似於城堡內的僵屍爆發。 守衛無法保護不變量,因為僵屍不需要越過它們。 僵屍只是在他們去的時候傳播和破壞數據。

值“是一個” String滿足的有用不變量少於值“是” Name

請參閱字符串鍵入以查看同一主題的另一種方法。

* - 通常需要重新反Serializable允許繞過構造函數。

我總是使用Map,因為intern() 必須在內部String的字符串池中進行(可能是線性的)搜索。 如果你經常這樣做,它就不如Map - Map快速搜索那么高效。

Java 5.0和6中的String.intern()使用通常具有較小最大大小的perm gen空間。 它可能意味着即使有足夠的空閑堆也會耗盡空間。

Java 7使用常規堆來存儲intern()ed字符串。

字符串比較它非常快,我不認為在考慮開銷時削減比較時間有很多優勢。

這樣做的另一個原因是,如果有許多重復的字符串。 如果有足夠的重復,這可以節省大量內存。

緩存字符串的一種更簡單的方法是使用像LinkedHashMap這樣的LRU緩存

private static final int MAX_SIZE = 10000;
private static final Map<String, String> STRING_CACHE = new LinkedHashMap<String, String>(MAX_SIZE*10/7, 0.70f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 10000;
    }
};

public static String intern(String s) {
    // s2 is a String equals to s, or null if its not there.
    String s2 = STRING_CACHE.get(s);
    if (s2 == null) {
        // put the string in the map if its not there already.
        s2 = s;
        STRING_CACHE.put(s2,s2);
    }
    return s2;
}

這是一個如何工作的例子。

public static void main(String... args) {
    String lo = "lo";
    for (int i = 0; i < 10; i++) {
        String a = "hel" + lo + " " + (i & 1);
        String b = intern(a);
        System.out.println("String \"" + a + "\" has an id of "
                + Integer.toHexString(System.identityHashCode(a))
                + " after interning is has an id of "
                + Integer.toHexString(System.identityHashCode(b))
        );
    }
    System.out.println("The cache contains "+STRING_CACHE);
}

版畫

String "hello 0" has an id of 237360be after interning is has an id of 237360be
String "hello 1" has an id of 5736ab79 after interning is has an id of 5736ab79
String "hello 0" has an id of 38b72ce1 after interning is has an id of 237360be
String "hello 1" has an id of 64a06824 after interning is has an id of 5736ab79
String "hello 0" has an id of 115d533d after interning is has an id of 237360be
String "hello 1" has an id of 603d2b3 after interning is has an id of 5736ab79
String "hello 0" has an id of 64fde8da after interning is has an id of 237360be
String "hello 1" has an id of 59c27402 after interning is has an id of 5736ab79
String "hello 0" has an id of 6d4e5d57 after interning is has an id of 237360be
String "hello 1" has an id of 2a36bb87 after interning is has an id of 5736ab79
The cache contains {hello 0=hello 0, hello 1=hello 1}

這樣可以確保intern()ed字符串的緩存數量有限。

更快但不太有效的方法是使用固定陣列。

private static final int MAX_SIZE = 10191;
private static final String[] STRING_CACHE = new String[MAX_SIZE];

public static String intern(String s) {
    int hash = (s.hashCode() & 0x7FFFFFFF) % MAX_SIZE;
    String s2 = STRING_CACHE[hash];
    if (!s.equals(s2))
        STRING_CACHE[hash] = s2 = s;
    return s2;
}

除了您的需要,上述測試的工作方式相同

System.out.println("The cache contains "+ new HashSet<String>(Arrays.asList(STRING_CACHE)));

打印出顯示以下內容的內容包括null表示空條目。

The cache contains [null, hello 1, hello 0]

這種方法的優點是速度,並且可以安全地使用多個線程而無需鎖定。 即,不同的線程是否具有不同的STRING_CACHE視圖並不重要。

那么,手動管理類似於類的類與使用intern()相比有哪些優點和缺點?

一個優點是:

因此,對於任何兩個字符串s和t,當且僅當s.equals(t)為真時,s.intern()== t.intern()才為真。

在一個必須經常比較許多小字符串的程序中,這可能會有所回報。 而且,它最終節省了空間。 考慮一個經常使用AbstractSyntaxTreeNodeItemFactorySerializer名稱的源程序。 使用intern(),這個字符串將被存儲一次,就是這樣。 其他所有內容,如果只是引用,但無論如何參考。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM