Java将出现在源代码中的字符串竞争者存储在池中。 换句话说,当您有如下代码时:
String a = "I am a string";
String b = "I am a string";
变量a
和b
将具有相同的值。 不只是两个相等的字符串,而是完全相同的字符串。 用Java单词a == b
是正确的。 但是,这仅适用于字符串以及较小的整数和长值。 其他对象不会被插入,因此,如果您创建两个具有完全相同值的对象,它们通常是不相同的。 它们可能并且可能相等,但对象不同。 有时候这可能很麻烦。 可能是当您从某个持久性存储中获取某个对象时。 如果您碰巧多次获取同一对象,则可能希望获取同一对象而不是两个副本。 换句话说,我也可以说您只希望在持久性中的单个对象的内存中拥有一个副本。 一些持久层可以为您完成此任务。 例如,JPA实现遵循此模式。 在其他情况下,您可能需要自己执行缓存。
在这个示例中,我将描述一个简单的内部池实现,也可以在stackoverflow主题上进行查看。 在本文中,我还将解释导致此处(以及此处)描述的解决方案的细节和注意事项。 本文包含的矿石信息比原始讨论要多。
对象池
实习需要一个对象池。 当您有一个对象并且想要实习该对象时,您实际上是在对象池中查看以查看是否已经有一个对象等于手头的对象。 如果有一个,我们将使用已经存在的一个。 如果没有对象等于实际对象,则将实际对象放入池中,然后使用该对象。
在实施过程中,我们必须面对两个主要问题:
- 垃圾收集
- 多线程环境
如果不再需要某个对象,则必须将其从池中删除。 删除可以由应用程序完成,但这将是完全过时且古老的方法。 Java优于C ++的主要优势之一是垃圾回收。 我们可以让GC收集这些对象。 为此,我们不应该在对象池中强烈引用池中的对象。
参考
如果您知道什么是软引用,弱引用和幻像引用,请跳至下一部分。
您可能会注意到,我并不是简单地说“参考”,而是说“强参考”。 如果您了解到在没有对对象的引用时GC会收集对象,则它不是绝对正确的。 事实上,这是GC处理不可触摸对象所需的强大参考。 为了更精确,强引用应该可以与其他强引用一起从局部变量,静态字段和类似的普遍位置传播。 换句话说:(强)引用从一个死亡对象指向另一个死亡对象的点不计算在内,它们将一起被删除并收集。
因此,如果这些都是强引用,那么您可能不会想到这么强的引用。 你是对的。 有一个名为java.lang.ref.Reference
的类,还有另外三个扩展它的类。 这些类是:
-
PhantomReference
-
WeakReference
和 -
SoftReference
在同一包中。 如果阅读文档,您可能会怀疑我们所需要的只是薄弱的一环。 幻影在池中使用毫无疑问,因为幻影引用不能用于访问对象。 软引用太过分了。 如果没有对该对象的强引用,那么将其保留在池中就没有意义。 如果它再次来自某个来源,我们将再次实习。 这肯定是一个不同的实例,但是没有人注意到它,因为没有引用前一个实例。
弱引用是可以用于访问对象但不会改变GC行为的引用。
WeakHashMap
弱引用不是我们必须直接使用的类。 有一个名为WeakHashMap
的类,该类使用软引用来引用关键对象。 这实际上是我们所需要的。 当我们实习一个对象并想查看它是否已经在池中时,我们搜索所有对象以查看是否有任何对象等于实际对象。 地图就是实现此搜索功能的东西。 在弱引用中持有密钥只会让GC在没人需要时收集密钥对象。
到目前为止我们可以搜索,这很好。 使用地图,我们还必须获得一些价值。 在这种情况下,我们只想获取相同的对象,因此必须在不存在该对象的情况下将其放入地图中。 但是,将对象本身放置在那里会破坏我们获得的结果,即我们仅保留对同一对象的弱引用作为键。 我们必须创建并弱引用该对象作为键。
弱水池
在那之后的解释是代码。 它只是说如果有一个对象等于实际对象,那么get(actualObject)
应该返回它。 如果没有,则get(actualObject)
将返回null。 方法put(newObject)
将一个新对象放入池中,如果有一个等于新对象的对象,它将用新对象覆盖旧对象的位置。
public class WeakPool<T> {private final WeakHashMap<T, WeakReference<T>> pool = new WeakHashMap<T, WeakReference<T>>();public T get(T object){final T res;WeakReference<T> ref = pool.get(object);if (ref != null) {res = ref.get();}else{res = null;}return res;}public void put(T object){pool.put(object, new WeakReference<T>(object));}
}
实习生池
解决该问题的最终方法是一个内部池,该池很容易使用已有的WeakPool
来实现。 InternPool
内部有一个弱池,并且intern(T object)
只有一个同步方法。
public class InternPool<T> {private final WeakPool<T> pool = new WeakPool<T>();public synchronized T intern(T object) {T res = pool.get(object);if (res == null) {pool.put(object);res = object;}return res;}
}
该方法尝试从池中获取对象,如果该对象不存在,则将其放在那里,然后将其返回。 如果已经有一个匹配的对象,那么它将返回池中已经存在的对象。
多线程
该方法必须同步以确保新对象的检查和插入是原子的。 如果没有同步,则可能发生两个线程检查池中两个相等的实例,两个线程都发现其中没有匹配的对象,然后将其版本插入池中。 其中一个,后来放置其对象的对象将是赢家,它会覆盖已存在的对象,但较宽松的人也认为它拥有真正的单个对象。 同步解决了这个问题。
与垃圾收集器赛车
即使使用该池的java应用程序的不同线程不会在同时使用该池时遇到麻烦,但是如果对垃圾收集器线程有任何干扰,我们仍然应该查看它。
调用弱引用get
方法时,引用可能返回null。 当垃圾回收器回收了关键对象,但是弱轮询实现中的弱哈希映射仍然没有删除条目时,就会发生这种情况。 即使弱映射实现每次查询映射时都检查密钥的存在,它也可能发生。 垃圾收集器可以在通话之间踢get()
弱哈希映射,并号召get()
来返回的弱引用。 哈希映射返回对对象的引用,该对象在返回时就已经存在,但是由于该引用是弱引用,因此将其删除,直到我们的Java应用程序执行到下一条语句为止。
在这种情况下, WeakPool
实现将返回null。 没问题。 InternPool
也不InternPool
困扰。
如果查看前面提到的stackoverflow主题中的其他代码,则可以看到一个代码:
public class InternPool<T> {private WeakHashMap<T, WeakReference<T>> pool = new WeakHashMap<T, WeakReference<T>>();public synchronized T intern(T object) {T res = null;// (The loop is needed to deal with race// conditions where the GC runs while we are// accessing the 'pool' map or the 'ref' object.)do {WeakReference<T> ref = pool.get(object);if (ref == null) {ref = new WeakReference<T>(object);pool.put(object, ref);res = object;} else {res = ref.get();}} while (res == null);return res;}
}
在此代码中,作者创建了一个无限循环来处理这种情况。 不太吸引人,但是有效。 该循环不可能无限期地执行。 可能不超过两次。 构造很难理解,很复杂。 士气:单一责任原则。 专注于简单的事情,将您的应用程序分解为简单的组件。
结论
即使Java仅对String进行实习,并且某些装箱原始类型的对象也可能进行实习,有时也希望进行实习。 在这种情况下,实习不是自动的,应用程序必须明确执行它。 通过将复制粘贴粘贴到代码库中,可以使用此处列出的两个简单类来实现此目的,或者可以:
<dependency><groupId>com.javax0</groupId><artifactId>intern</artifactId><version>1.0.0</version>
</dependency>
从Maven中央插件导入库作为依赖项。 该库是最小的,仅包含这两个类,可在Apache许可下使用。 该库的源代码在GitHub上 。
翻译自: https://www.javacodegeeks.com/2014/03/java-object-interning.html