String.intern()vs手动字符串到标识符映射?

时间:2012-01-13 16:06:47

标签: java string string-interning

我记得看过几个字符串密集型程序,它们进行了大量的字符串比较,但字符串操作相对较少,并且使用了一个单独的表来将字符串映射到标识符,以实现高效的相等性和更低的内存占用,例如:

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),但是你明白了 - 整个程序在你期望Name的任何位置充满了String,并且需要字符串操作的罕见情况,它转换为字符串然后再次缓存它们,概念上如下:

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

我想我理解这一点 - 特别是当有很多相同的字符串和很多比较时 - 但是通过使用常规字符串和intern无法实现相同的目标。他们? documentation for String.intern()明确说:

  

...
  调用实习方法时,如果池已经包含等于此字符串对象的字符串(由equals(Object)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回对此String对象的引用。

     

对于任何两个字符串s和t,当且仅当s.equals(t)为真时,s.intern()== t.intern()才为真。
  ...

那么,手动管理Name类似于使用intern() 的优势和劣势是什么?

我到目前为止所考虑的是:

  • 手动管理地图意味着使用常规堆,intern()使用permgen。
  • 当手动管理地图时,您喜欢可以验证某些内容是Name的类型检查,而实习字符串和非实习字符串共享相同的类型,因此可能会忘记实习一些地方。
  • 依赖intern()意味着重用现有的,经过优化的,经过验证的机制,而无需编写任何额外的类。
  • 手动管理地图会导致代码更容易让新用户感到困惑,而且操作变得更加麻烦。

...但我觉得我在这里错过了其他的东西。

5 个答案:

答案 0 :(得分:2)

不幸的是,String.intern()可能比简单的同步HashMap慢。它并不需要如此缓慢,但截至今天在甲骨文的JDK中,它很慢(可能是由于JNI)

要考虑的另一件事是:你正在编写一个解析器;你在char[]中收集了一些字符,你需要用它们制作一个字符串。由于字符串可能很常见且可以共享,因此我们希望使用池。

String.intern()使用这样的池;要查找,你需要一个字符串开头。所以我们首先需要new String(char[],offset,length)

我们可以避免自定义池中的开销,可以根据char[],offset,length直接进行查找。例如,池是 trie 。字符串最有可能在池中,因此我们将在没有任何内存分配的情况下获取字符串。

如果我们不想编写自己的池,但使用旧的HashMap,我们仍然需要创建一个包装char[],offset,length的密钥对象(类似CharSequence)。这仍然比新的字符串便宜,因为我们不会复制字符。

答案 1 :(得分:1)

  

手动管理类似名称的类与使用intern()

相比有哪些优点和缺点

类型检查是一个主要问题,但不变保存也是一个重要问题。

Name构造函数

添加简单检查
Name(String s) {
  if (!isValidName(s)) { throw new IllegalArgumentException(s); }
  ...
}

可以确保*不存在与Name之类的无效名称相对应的"12#blue,,"个实例,这意味着将Name作为参数并消耗Name的方法返回通过其他方法,不需要担心无效的Name可能会在哪里蔓延。

为了概括这个论点,想象一下你的代码是一个带有墙壁的城堡,旨在保护它免受无效输入的影响。您需要一些输入才能通过,因此您需要使用警卫来安装门,以便在输入时检查输入。 Name构造函数是一个守卫的例子。

StringName之间的区别在于String s无法防范。外围内外的任何恶意或天真代码都可以创建任何字符串值。 Buggy String操纵代码类似于城堡内的僵尸爆发。守卫无法保护不变量,因为僵尸不需要越过它们。僵尸只是随着时间的推移传播和破坏数据。

值“是”String满足的有用不变量少于“是Name”。

请参阅stringly typed了解查看相同主题的其他方式。

* - 通常需要重新反序列化Serializable,允许绕过构造函数。

答案 2 :(得分:1)

我会一直使用地图,因为intern() 在内部字符串的字符串池中进行(可能是线性的)搜索。如果你经常这样做,它就不如Map - Map快速搜索那么高效。

答案 3 :(得分:1)

Java 5.0中的

String.intern()&amp; 6使用通常具有较小最大尺寸的烫发空间。即使有足够的免费堆,也可能意味着你的空间不足。

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视图无关紧要。

答案 4 :(得分:0)

  

那么,手动管理a的优点和缺点是什么?   类似于类的类vs使用intern()?

一个优点是:

  

对于任何两个字符串s和t,s.intern()== t.intern()   当且仅当s.equals(t)为真时才为真。

在一个必须经常比较许多小字符串的程序中,这可能会有所回报。 而且,它最终节省了空间。考虑一个经常使用AbstractSyntaxTreeNodeItemFactorySerializer等名称的源程序。使用intern(),这个字符串将被存储一次,就是这样。其他所有内容,如果只是引用它,但无论如何都是你的参考。