Java 中的 RAII 设计模式

RAII design pattern in Java

我有 C++ 背景,是 RAII 模式的超级粉丝。我已经广泛使用它来处理内存管理和锁管理以及其他用例。

使用 Java 1.7 我发现我可以使用 try-with-resources 模式来创建 RAII 模式。

我使用 RAII 创建了一个示例应用程序并且它可以运行,但我看到来自 java 的编译器警告。

示例应用程序

try(MyResource myVar = new MyResource(..))
{
    //I am not using myVar here 
}

我收到以下错误

warning: [try] auto-closeable resource node is never referenced in body of corresponding try statement

我理解这个警告,它暗示我应该在 try 块中使用变量,我真的不需要一直这样做。

看着这个,我假设 Java 没有真正支持 RAII,我可能误用了仅用于资源管理的功能,而不是 C++ 中的 RAII 等价物。

几个问题:

  1. 我的理解正确吗?
  2. 忽略这些警告的风险有多大?
  3. 如何通过 ant 忽略这些警告?
  4. 有什么简单的方法可以解决这个问题吗?

对于 4,我正在考虑将构造函数调用拆分为一个更简单的构造函数和一个像这样的实例方法

try(MyResource myVar = new Resource())
{
   myvar.Initialize()
   ....

}

它解决了编译器问题,但从类似 RAII 的设计中提取了本质。

1. Is my understanding correct?

或多或少。是的,您可以通过这种方式使用 try-with-resources,是的,它在语义上与 RAII 相当。区别在于没有销毁释放,只有方法调用。

很难找到写成只是来包装一些资源管理逻辑的对象,例如:

import java.util.concurrent.locks.Lock;

public class Guard implements AutoCloseable {
    private final Lock lock;

    public Guard(Lock lock) {
        this.lock = lock;
        lock.lock();
    }

    @Override
    public void close() {
        lock.unlock();
    }
}
try(Guard g = new Guard(myLock)) {
    // do stuff
}

如果您与其他程序员一起工作,您可能需要向一些人解释它的含义,但如果它让您满意,我个人认为这没有问题。

我不推荐的是编写像

这样的奇怪代码
try(AutoCloseable a = () -> lock.unlock()) {
    lock.lock();
    // do stuff
}

这肯定会在代码审查中产生 WTF。

2. How risky is it to ignore these warnings?

没有风险。警告实际上只是一个通知。你知道,万一你不知道

要消除警告,您可以尝试:

try(@SuppressWarnings("unused")
    MyResource myVar = new MyResource())

或者也可以参见 'How do you get *ant* to not print out javac warnings?'

IDE 应该让您可以选择全局或仅针对单个语句(没有注释)抑制特定警告。

扩展 Radiodef 的回答。我认为带有 try-with-resources 的 RAII 是 java 完全可以接受的模式。但是要真正抑制警告

  • 您需要使用 @SuppressWarnings("try") 而不是 @SuppressWarnings("unused")
  • 并在方法上添加注释而不是变量声明

应用以上几点的示例:

   @SuppressWarnings("try")
   void myMethod1() {
       try(MyResource myVar = new MyResource(..)) {
           //I am not using myVar here 
       }
   }

扩展模式本身。我广泛使用它来管理读写锁并且效果很好。

在我的代码中,我使用了一些技巧来抢先解锁一些资源以增加并发性,如下所示:

try (Guard g1 = new Guard(myLock1)) {
    someStuffThatRequiresOnlyLock1();
    try (Guard g2 = new Guard(myLock2)) {
        someStuffThatRequiresBothLocks();
        if (isSomething) {
            g1.close();
            someMoreSuffThatRequiresOnlyLock2()
        } else {
            someMoreSuffThatRequiresBothLocks();
        }
    }
}

始终以相同的顺序获取锁,但根据需要执行解锁,尽可能多地 space 用于并发处理。使其与读写锁一起工作的调整是修改Guard class 允许重复关闭:

public class Guard implements AutoCloseable {
    private final Lock lock;
    private boolean isClosed = false;

    public Guard(Lock lock) {
        this.lock = lock;
        lock.lock();
    }

    @Override
    public void close() {
        if (!isClosed) {
           isClosed = true;
           lock.unlock();
        }
    }
}

更新: 自 Java 9 以来,您无需在 Java 中抑制针对此模式的任何警告。您可以在 try 子句中引用单个有效的最终变量,这将不会产生警告:

    Guard g1 = new Guard(myLock1);
    try (g1) {
        someStuffThatRequiresOnlyLock1();
        Guard g2 = new Guard(myLock2);
        try (g2) {
            someStuffThatRequiresBothLocks();
            if (isSomething) {
                g1.close();
                someMoreSuffThatRequiresOnlyLock2()
            } else {
                someMoreSuffThatRequiresBothLocks();
            }
        }
    }
  1. Is there a simple way for me to overcome this? I am thinking of splitting the constructor call into a simpler constructor and an instance method like this:

try(MyResource myVar = new Resource()) {
   myvar.Initialize()
   ...
}

是的,@DesertIce,我使用了你建议的技术。尽管我鼓励您使用更具描述性的名称,例如 preventCompilerWarning(),这显然是一个存根,而不是 initialize(),它看起来很有用。

另一个可能更好的解决方案是:

try(MyResource myVar = new Resource())
{
   assert(myVar != null); // Prevents compiler warning about unused var.
   ...
}

使用带注释的 assert() 可能更好,因为它:

  1. 未编译成生产代码。
  2. 工作于 类 你没有写。

我从 Hemal's comment on my blog post 得到了这个解决方案。

遗憾的是,@Radiodef 的解决方案@SuppressWarnings("unused") MyResource 不起作用(不再)。 @VictorNazarov 的 @SuppressWarnings("try") 解决方案有效,但压制可能有多个 try 块的整个方法似乎很遗憾。

我很想看到 Java 实现类似于 Kotlin 的东西,它允许您在被迫声明不使用的变量时使用 _ 而不是变量名 -使您正在做的事情明确(简短)。或者至少识别名为 ignoredunused 或其他名称的变量。

如果是我,我可能会将逻辑重构为实用方法:

public static void withLock(Lock lock, Runnable runnable) {
    lock.lock();
    try {
        runnable.run();
    } finally {
        lock.unlock();
    }
}

然后像这样称呼它

withLock(lock, () -> {
    // ... logic here
});

或者,如果您愿意,您仍然可以在实用程序方法中使用 try-with-resources,并抑制警告,但它的好处是不会在代码库中乱扔[抑制]警告。

从 Java 9 开始,您可以在 try-with-resources 语句中使用 有效最终 变量作为资源。

参见Java 9 release notes

所以你可以这样解决“unused variable”警告:

MyResource myVar = new MyResource(..);
try (myVar) {
    // myVar may or may not be referenced here
}

另请参阅 this answer 以了解原始 try-with-resource 功能设计背后的原因。