I have a type which is fairly heavy (maintains an off-heap object), and which can be uniquely derived from a String. It requires cleanup, so of course the type has a close()
method, but it is used in contexts where clients may not be sure when they are done with their object; or they may simply not be aware they are supposed to close it. Therefore the class also has a finalize()
method as a last resort:
class Wrapper implements Closeable {
private Heavy h;
private Wrapper(String s) { this.h = initNative(s); }
public static Wrapper fromString(String s) { return new Heavy(s); }
public int doSomething() { return h.doSomething(); }
public synchronized void close() {
if (h != null) {
cleanupNative(h);
h = null;
}
}
public void finalize() { close(); }
}
Now I would like to introduce a layer of indirection, so that:
- Two clients who call
Wrapper.fromString
with the same string can share the sameHeavy
(I don't care if they share the sameWrapper
or not). - I can limit the number of
Heavy
objects live in the system at a time.
My plan is to keep track of which Wrapper
objects have been used recently, and for those which have not been used for some time, I can deallocate their Heavy
objects to make room for other requests. Then if the Wrapper
turns out to be needed again, it will notice that its Heavy
is null, and reinitialize it from the same String: a sort of lazy-loading where I also unload objects. Something like this:
class WrapperFactory {
private Cache<String, Wrapper> cache = new Cache<>(/*capacity=*/10);
public Wrapper fromString(String s) {
if (cache.isFull()) {
cache.evictOldest().close();
}
return cache.computeIfAbsent(s, Wrapper::fromString);
}
}
class Wrapper {
private String s;
private Heavy h;
public static Wrapper fromString(String s) { this.s = s; }
public synchronized int doSomething() {
if (h == null) {
h = initNative(s);
}
return h.doSomething();
}
// same cleanup methods
}
The overall idea seems workable to me, but I'm not sure of some of the details.
- I'm not sure what kind of data structure to use for my Cache type: I think ben-manes/caffeine has the features I need, but I'd prefer not to take it on as a dependency if something simpler will work.
- How do I get the Wrapper objects to communicate back with the WrapperFactory so that I don't erase an in-use object, and the objects I do erase are the ones least often used?
Lastly it seems to me that what I'm doing is not so uncommon: there must be some library handling it, or perhaps design pattern(s) describing how to do it well. It's a bit like Flyweight, a bit like interning.