如何为字符串到文件句柄的映射创建清洁器?

how to create cleaner for map of string to file handles?

我有一个class

class Something {
    private Map<String, RandomAccessFile> map = new LinkedHashMap<>() {
        @Override
        protected boolean removeEldestEntry(...) { also closes the file if it returns true; }

        @Override
        public RandomAccessFile remove(Object filename) { closes the file too; }
    };
}

这是文件名到文件句柄的映射。这个想法是让一些文件保持打开状态,以便 Something class 的成员函数可以写入任何打开的文件。

现在我想重写 finalize,以便当对象超出范围时,我们关闭所有文件。但是 finalize 已被弃用。所以想法是调用 cleaner.register(map, runnable) ,其中 runnable 获取 'map' 中的所有 RandomAccessFile 并关闭每个。 IE。类似于 cleaner.register(map, () -> map.values().forEach(close file))。但这使得 runnable 具有对地图的引用,因此地图永远不会变得无法访问 - class LinkedHashMap.LinkedValues 不是静态的 class.

有什么想法可以做到这一点吗?

您必须将其可达性应控制清理的对象与清理所需的数据分开。

在您的情况下,Something 实例的生命周期将确定文件是否仍在使用中,而地图实例可以在清理操作期间使用,假设您将地图设为私有并且永远不会分发出去。

还有一些重点需要注意。 documentation of Cleaner 表示:

The cleaning action could be a lambda but all too easily will capture the object reference, by referring to fields of the object being cleaned, preventing the object from becoming phantom reachable. Using a static nested class, as above, will avoid accidentally retaining the object reference.

当你使用lambda表达式() -> map.values().forEach(close file))并且map是当前Something实例的字段访问时,你已经隐式捕获了this。但是,即使你修复了清洁器,让它只引用地图实例,也存在地图本身是 LinkedHashMap 的匿名子 class 和匿名内部 classes 总是有的问题对其外部实例的隐式引用。

您可以通过在静态上下文中执行初始化来解决这两个问题:

class Something {
    private Map<String, RandomAccessFile> map = initializeMapAndCleaner(this);

    static final Cleaner CLEANER = Cleaner.create();

    static Map<String, RandomAccessFile> initializeMapAndCleaner(Object instance) {
        LinkedHashMap<String, RandomAccessFile> map = new LinkedHashMap<>() {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, RandomAccessFile> eldest) {
                /*also closes the file if it returns true;*/
            }

            @Override
            public RandomAccessFile remove(Object filename) { /*closes the file too;*/ }
        };
        CLEANER.register(instance, () -> map.values().forEach(f -> {/*close file*/}));
        return map;
    }
}

通过使用 static 方法,不能有任何对 this 的隐式引用,并且 instance 参数中提供的显式引用被故意键入为 Object,以防止意外访问映射 class 或清理操作中的成员。

但请注意,LinkedHashMap 不保证所有删除操作都通过那个被覆盖的 remove 方法。此外,替换操作不使用 remove。当 removeSomething class 使用的唯一操作(除 put 之外)时,它可能适用于您的内部使用,但在这种情况下,它也是可能的并且使文件关闭调用者职责的清洁器。


就是说,您根本不应该实施清理器。如果所有的清理工作都是为了关闭 RandomAccessFile,它已经过时了,因为当 RandomAccessFile 变得无法访问而不被关闭时,已经有一个清理器关闭了底层资源。如果依赖那个清洁工感觉不对劲,那你就走对了。但是依靠 Something 的清洁器并没有更好的办法。

一般情况下,变量作用域和对象可达性只是远程连接。请参阅 Can java finalize an object when it is still in scope? or finalize() called on strongly reachable objects in Java 8 for a connected real life problem. But of course, the timing issues described in When is the finalize() method called in Java? 也适用于清洁工。垃圾收集器可能永远不会 运行,例如当有足够的内存时,或者它可能 运行 但不会收集所有无法访问的对象,例如当它回收足够的内存供应用程序继续运行时。

当你想在变量范围的末尾进行清理时,try-with-resources Statement 是可行的方法。您只需实现 AutoCloseable 或其子类型,提供一个 close() 方法来关闭所有文件。

class Something implements Closeable { // java.io.Closeable is suitable here
    …

    @Override
    public void close() throws IOException {
        for(RandomAccessFile f: map.values()) f.close();
        map.clear();
    }
}

那你就可以轻松地说

try(Something sth = new Something(…)) {
    // use sth
}
// safely closed