Java的String Intern是一种轻量级模式吗?

14

Java的String内存池的实现是否遵循享元模式?

我对此有疑问是因为在Intern中没有外在状态。在GoF中,我读到应该在内在和外在状态之间保持适当的平衡。但在intern中,一切都是内在的。

或者我们可以说并没有关于属性的严格规定,只需共享对象以减少内存即可称之为享元。

请帮助我理解。


1
我认为,如果你的对象没有外在的上下文,那么你只是在基本上进行缓存。Flyweight模式之所以有用,是因为人们经常忘记他们至少可以缓存与上下文无关的对象部分并共享它。 - C S
6个回答

5
无论是否使用 interning,Java String 都会利用享元模式通过在字符串和从它派生的字符串(例如 substring)之间共享 char[]。然而这也有一个反面:如果你从一个巨大的字符串中取出一个小的子串,那个巨大的 char[] 就不会被垃圾回收。
注意:截至 OpenJDK version 1.7.0_06 版本,上述内容已经过时:代码已更改,char[] 不再在实例之间共享。substring() 现在会创建一个新的数组。

在享元对象中持有内在状态并传递外在状态信息 - 我们需要担心这个吗?因为在 GoF 书中,我看到更多的重要性被赋予了内在/外在分离。在 char[] 享元中,内在和外在是什么? - Joseph
1
很简单 - char[] 是完全内在的,而对象所代表的字符串则是完全外在的。使用 String,你甚至不知道 char[] 的存在。 - Marko Topolnik
HotSpot实现最终将改用精确长度的char[](或者可能是byte[]),而不带有偏移和长度字段。同时,将char[]作为单独的分配应该也被消除。 - Tom Hawtin - tackline
@TomHawtin-tackline 这非常有趣。你能否指向一篇相关的文章?我对细节很感兴趣 :) - Marko Topolnik
1
@fredoverflow 答案仍然有效,只是需要更详细的解释。由于不再与子字符串共享数组,因此 String(String) 构造函数已更改为不再复制数组。在更改之前,使用此构造函数是一种未记录的技巧,用于构造不共享数组的字符串,以解决小(子)字符串引用大数组的问题。由于不再需要这个技巧,您可以再次使用相同的数组构造字符串,尽管这些字符串将是完全相等的字符串,而不是子字符串。此外,最近的 JVM 中还有字符串去重功能。 - Holger
显示剩余2条评论

4

是的,String.intern()的实现遵循享元模式。

正如javadoc所述:

返回字符串对象的规范表示。类String私有地维护一个最初为空的字符串池。

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

由此可知,对于任何两个字符串s和t,当且仅当s.equals(t)为true时,s.intern()==t.intern()也为true。

所有字面值字符串和字符串常量表达式都会被内部化。字符串字面值的定义详见Java语言规范§3.10.5。

内部化的字符串驻留在 “Perm Gen” 空间中,并且对于使用 .intern() 返回的字符串对象,可以使用运算符==,因为对于相等的值,.intern() 总是返回同一个对象。

然后请记住,.intern() 方法不会导致泄漏,因为JVM现在能够垃圾回收池。

也请尝试阅读这篇文章


2
但是享元模式是关于共享对象内部状态的,而字符串缓存则只是为了整个对象的缓存。我认为这里并不适用。 - Marko Topolnik
3
我的问题是,“仅为节省内存而共享是否足以称之为轻量级?”无论实现细节如何,比如外在/内在状态。 - Joseph
1
共享整个对象确实是最好的,但是你的设计根本不符合“轻量级”这个术语的涵盖范围,因为其核心是拥有独特、复杂的实体,而在幕后共享它们的复杂数据。你所引用的维基百科页面已经非常清楚地表明了这一点。 - Marko Topolnik
2
然而,如果你是正确的,网络上充满了关于享元模式的糟糕示例。 - dash1e
@tcarvin 意图可能相同,但两种模式可以具有相同的意图(但不同的权衡)。根据 GoF,几乎逐字逐句地说,享元模式利用与实例的外在状态分离的内在状态。 - C S
显示剩余4条评论

3
你已经正确地认识到Interning和Flyweight都基于相同的想法:缓存和共享公共状态。
在Flyweight中,当没有外在状态需要存储时,只有指向内在状态的指针保留。那么,甚至不需要外在状态成为一个对象,指针本身就可以成为外在状态。这就是Flyweight变成Interning的情况。
无论Interning是否真正是Flyweight的一种类型,都只是关于定义的争论。最重要的是理解如何将其视为其他专业实例的理解,所以你做得很好。

我认为这个答案中“内在”和“外在”的术语被颠倒了。“内在数据”是共享的共性,“外在数据”是不共享的独特上下文。 - jaco0646
@jaco0646 哦!你说得对,我的答案错误已经三年了。我已经更正了。 - Eldritch Conundrum

0

不,共享对象以减少内存并不足以称之为享元模式。换句话说,缓存并不自动成为享元模式。

我认为可以公正地说,享元模式是一种特殊形式的缓存,即部分缓存;但请注意,《设计模式》这本书在享元章节中没有使用“cache”或“caching”这些词语(尽管这些术语在前一章节中的门面模式和后续章节中的代理模式中都有使用)。

这个主题中的一些评论值得重复,因为它们简明扼要地回答了整体问题。

  • 如果你的对象没有外在的上下文,那就只是做缓存而已。享元模式有用的原因在于人们经常忘记他们可以至少缓存独立于上下文的对象部分并共享它。

    --C S

  • 享元是关于共享对象内部的。国际化仅仅是缓存整个对象。

    --Marko Topolnik

但让我们将字符串国际化与GoF定义的标准进行比较(第197页)。

当满足以下所有条件时,应用享元模式:
- 应用程序使用大量对象。 - 由于对象数量庞大,存储成本很高。 - 大多数对象状态可以变为外在状态。 - 一旦去除了外在状态,许多对象组可以被相对较少的共享对象替换。 - 应用程序不依赖于对象标识。由于享元对象可以共享,因此概念上不同的对象将返回 true。
明显地,许多应用程序使用大量字符串,因此这个标准通过了。
与原始类型相比,存储字符串是昂贵的,所以我们可以给这个标准一个通过。
这里是我们被绊倒的地方:没有一个字符串的状态是外在的。这个标准失败了。
如果我们宽容一些,忽略外在状态的部分,我们也可以通过这个标准,因为字符串确实往往会被重用。
任何使用 Java 中的“==”比较字符串的人都知道不能依赖对象标识,因此这个标准通过了。

4/5的通过标准已经相当不错了,对吧?这难道不足以说明实习/缓存和享元是相同的吗?不是的:相似并不等于相同。GoF引用中“所有”一词的强调是他们的,而不是我的。自然而然地,人们希望尽可能多地使用GoF模式名称来标记实现,因为这样做可以使这些实现合法化。(最严重的情况是工厂模式,你可以轻松地找到将每种创造性代码都标记为工厂模式的例子;但我跑题了。)如果模式没有按照其公布的定义进行保持,则它们会重叠并失去意义,从而击败它们的大部分目的(共同词汇)。

最后,让我们分析一下享元章节的第一句话:GoF如何定义享元模式的“意图”。

使用共享来有效地支持大量细粒度对象。

我认为没有外在状态的对象不是细粒度的,而恰恰相反;因此,这里建议缓存的“意图”:使用缓存来有效地支持大量粗粒度对象。

显然,字符串内部化/缓存和享元模式之间存在相似之处;但它们并不相同。


0
就像其他人所说的那样,String.intern() 是关于缓存的。它返回对池中已存储的字符串字面量的引用。以这种方式,它与享元模式有些相似,因为它使用现有对象,从而导致更低的内存消耗和更高的性能(尽管intern在字符串池中查找时具有自己的性能开销)。因此,这两者看起来可能相似,但实际上并不是。

-1

Flyweight 是关于共享对象不可变的内部状态。而 Interning 只是缓存整个对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接