繁体   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