Статьи

Тонкое автозаменчивое изменение контракта между Java 7 и Java 8

Приятной особенностью оператора Java 7 try-with-resources и типа AutoCloseable который был представлен для работы с этим оператором, является тот факт, что инструменты статического анализа кода могут обнаруживать утечки ресурсов. Например, Eclipse:

Ресурс утечек

Если у вас есть вышеуказанная конфигурация и вы попытаетесь запустить следующую программу, вы получите три предупреждения:

1
2
3
4
5
6
7
8
9
public static void main(String[] args)
throws Exception {
    Connection c = DriverManager.getConnection(
         "jdbc:h2:~/test", "sa", "");
    Statement s = c.createStatement();
    ResultSet r = s.executeQuery("SELECT 1 + 1");
    r.next();
    System.out.println(r.getInt(1));
}

Выход есть, тривиально

1
2

Предупреждения выпущены на всех c , s , r . Быстрое исправление (не делайте этого!) Состоит в том, чтобы подавить предупреждение с помощью специфичного для Eclipse параметра SuppressWarnings :

1
2
3
4
5
@SuppressWarnings("resource")
public static void main(String[] args)
throws Exception {
    ...
}

В конце концов, WeKnowWhatWeReDoing ™, и это всего лишь простой пример, верно?

Неправильно!

Правильный способ исправить это, даже для простых примеров (по крайней мере, после Java 7), состоит в использовании простого оператора try-with-resources.

01
02
03
04
05
06
07
08
09
10
11
public static void main(String[] args)
throws Exception {
    try (Connection c = DriverManager.getConnection(
             "jdbc:h2:~/test", "sa", "");
         Statement s = c.createStatement();
         ResultSet r = s.executeQuery("SELECT 1 + 1")) {
 
        r.next();
        System.out.println(r.getInt(1));
    }
}

На самом деле было бы замечательно, если бы Eclipse мог автоматически исправить это предупреждение и обернуть все отдельные операторы в оператор try-with-resources. Подтвердите этот запрос, пожалуйста!

Отлично, мы это знаем. Что за дело с Java 8?

В Java 8 контракт на AutoCloseable изменился очень тонко (или прямо, в зависимости от вашей точки зрения).

Версия Java 7

Ресурс, который должен быть закрыт, когда он больше не нужен.

Обратите внимание на слово "must" .

Версия Java 8

Объект, который может содержать ресурсы (например, дескрипторы файлов или сокетов), пока не будет закрыт. Метод close () объекта AutoCloseable вызывается автоматически при выходе из блока try-with-resources, для которого объект был объявлен в заголовке спецификации ресурса. Эта конструкция обеспечивает быстрое освобождение, избегая исключений исчерпания ресурсов и ошибок, которые могут возникнуть в противном случае.

API Примечание:

Для базового класса возможно и фактически является общим для реализации AutoCloseable, даже если не все его подклассы или экземпляры будут содержать высвобождаемые ресурсы. Для кода, который должен работать в полной общности, или когда известно, что экземпляр AutoCloseable требует освобождения ресурса, рекомендуется использовать конструкции try-with-resources. Однако при использовании таких средств, как Stream, которые поддерживают формы как на основе ввода-вывода, так и не на основе ввода-вывода, блоки пробного использования ресурсов обычно не нужны при использовании форм не на основе ввода-вывода.

Короче говоря, начиная с Java 8, AutoCloseable — это скорее подсказка о том, что вы можете использовать ресурс, который необходимо закрыть, но это не всегда так.

Это похоже на контракт Iterable , в котором не говорится, можете ли вы выполнить итерацию только один раз или несколько раз по Iterable , но он накладывает контракт, необходимый для цикла foreach .

Когда у нас есть «необязательно закрываемые» ресурсы?

Взять, к примеру, JOOQ . В отличие от JDBC, jOOQ Query ( который был сделан AutoCloseable в jOOQ 3.7 ) может представлять или не представлять ресурс, в зависимости от того, как вы его выполняете. По умолчанию это не ресурс:

01
02
03
04
05
06
07
08
09
10
try (Connection c = DriverManager.getConnection(
        "jdbc:h2:~/test", "sa", "")) {
 
    // No new resources created here:
    ResultQuery<Record> query =
        DSL.using(c).resultQuery("SELECT 1 + 1");
 
    // Resources created and closed immediately
    System.out.println(query.fetch());
}

Выход снова:

1
2
3
4
5
+----+
|   2|
+----+
|   2|
+----+

Но теперь у нас снова есть предупреждение Eclipse о переменной query , в котором говорится, что существует ресурс, который необходимо закрыть, даже если при использовании jOOQ таким образом мы знаем, что это не так. Единственный ресурс в вышеприведенном коде — это Connection JDBC, и оно правильно обрабатывается. JOOQ-внутренний PreparedStatement и ResultSet полностью обрабатываются и охотно закрываются jOOQ.

Тогда зачем в первую очередь реализовывать AutoCloseable?

jOOQ инвертирует поведение JDBC по умолчанию.

  • В JDBC по умолчанию все выполняется лениво, и ресурсы должны быть явно закрыты.
  • В jOOQ все делается по умолчанию с нетерпением, и, при желании, ресурсы могут быть сохранены в явном виде.

Например, следующий код будет держать открытыми PreparedStatement и ResultSet :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
try (Connection c = DriverManager.getConnection(
        "jdbc:h2:~/test", "sa", "");
 
     // We "keep" the statement open in the ResultQuery
     ResultQuery<Record> query =
         DSL.using(c)
            .resultQuery("SELECT 1 + 1")
            .keepStatement(true)) {
 
    // We keep the ResultSet open in the Cursor
    try (Cursor<Record> cursor = query.fetchLazy()) {
        System.out.println(cursor.fetchOne());
    }
}

В этой версии у нас больше нет предупреждений в Eclipse, но вышеприведенная версия действительно является исключением при использовании jOOQ API.

То же самое относится и к Stream API Java 8. Интересно, что Eclipse не выдает здесь никаких предупреждений:

1
2
Stream<Integer> stream = Arrays.asList(1, 2, 3).stream();
stream.forEach(System.out::println);

Вывод

Поначалу обнаружение утечки ресурсов кажется хорошей функцией IDE / компилятора. Но избежать ложных срабатываний сложно. В частности, поскольку Java 8 изменила контракты для AutoCloseable , разработчикам разрешается реализовывать контракт AutoCloseable для простого удобства, а не в качестве четкого индикатора присутствия ресурса, который ДОЛЖЕН быть закрыт.

Это делает очень трудным, если не невозможным, для IDE обнаружение утечек ресурсов сторонних API (не-JDK API), где эти контракты обычно не известны. Решение, как всегда с инструментами статического анализа кода, состоит в том, чтобы просто отключить обнаружение утечки потенциальных ресурсов:

Ресурс-просачивание раствор