Статьи

Поддержка исключенных исключений в Java 7

Новый конструктор и два новых метода были добавлены в класс Throwable (родительский класс для исключений и ошибок ) в JDK 7 . Новый конструктор и два новых метода были добавлены для поддержки «подавленных исключений» (не путать с плохой практикой глотания или игнорирования исключений). В этой статье я расскажу, почему эти методы были введены и как они используются в JDK 7. Я коротко расскажу о том, как подавленные исключения отличаются от связанных в цепочке исключений.

Подавленные исключения играют важную роль в выполнении нового оператора Java 7 try-with-resources (также известного как Automatic Resource Management [ ARM ]). Предоставление поддержки API для этой новой возможности управления ресурсами, по-видимому, было основным драйвером для нового конструктора и методов класса Throwable, которые обеспечивают доступ к подавленным исключениям , но API поддерживают подавленные исключения, используемые вне оператора try-with-resources ,

Возможно, наиболее распространенный вариант использования для обнаружения исключенных исключений — это когда оператор try-with-resources (который является одним из типов try, для которого не требуется выражение catch или finally согласно спецификации языка Java Java SE 7 Edition, раздел 14.20 ) встречает исключение внутри блока try, а затем обнаруживает другое исключение при неявной попытке закрыть связанный ресурс.

Чтобы проиллюстрировать этот случай, я создаю свой собственный «ресурс», который можно использовать в операторе try-with-resource, поскольку он реализует интерфейс java.lang.AutoCloseable . Этот «непослушный ресурс» преднамеренно генерирует исключение, когда код, использующий его, пытается использовать его, а затем продолжает свое плохое состояние, генерируя другое исключение, когда вызывается переопределенный метод close (единственный, предписанный интерфейсом AutoCloseable ). Листинг кода для NaughtyResource показан ниже.

NaughtyResource.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package dustin.examples;
 
/**
 * Resource that throws exceptions both in its use and its closure and is only
 * intended for use in demonstrating Java 7's suppressed exceptions APIs. This
 * is not a well-behaved class.
 *
 * @author Dustin
 */
public class NaughtyResource implements AutoCloseable
{
   /**
    * Method that intentionally throws an exception.
    *
    * @throws RuntimeException Thrown no matter how you call me.
    */
   public void doNothingGood()
   {
      throw new RuntimeException("Nothing good can come of this.");
   }
 
   /**
    * The overridden closure method from AutoCloseable interface.
    *
    * @throws Exception Exception that might be thrown during closure of this
    *    resource.
    */
   @Override
   public void close() throws Exception
   {
      throw new UnsupportedOperationException("Not supported yet.");
   }
}

Теперь, когда доступен непослушный ресурс, пришло время использовать непослушный ресурс и продемонстрировать API подавленных исключений. На следующем рисунке показано, что происходит, если кто-то пытается использовать этот ресурс, не перехватывая исключение, неявно созданное методом close, и не объявляя метод как выбрасывающий его.

Это сообщение об ошибке, предоставленное javac, как показано на следующем снимке экрана.

Я показал два предыдущих снимка экрана, чтобы подчеркнуть неявный вызов close, выполняемый на ресурсе. Сообщение об ошибке, отображаемое в редакторе NetBeans и в консоли, четко указывает на то, что это так.

Следующий листинг кода содержит первую версию класса SuppressedExceptions, который компилируется.

SuppressedExceptions.java (Версия 1)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package dustin.examples;
 
/**
 * Demonstrate JDK 7's Suppressed Exceptions API support.
 *
 * @author Dustin
 */
public class SuppressedExceptions
{
   /**
    * Executable function demonstrating suppressed exceptions.
    *
    * @param arguments The command line arguments; none expected.
    */
   public static void main(String[] arguments) throws Exception
   {
      try (NaughtyResource naughty = new NaughtyResource())
      {
         naughty.doNothingGood();
      }
   }
}

Хотя при выполнении вышеуказанного кода действительно встречаются два исключения (одно из блока try при вызове NaughtyResource.doNothingGood () и одно из неявно вызванного метода close), только одна просачивается вверх и отображается, когда приложение это запустить. Это продемонстрировано на следующем снимке экрана.

Как показывает последнее изображение, отображается только исключение, обнаруженное в блоке try try оператора try-with-resources. Вот где пригодится возможность задать Throwable (в данном случае Исключение) о его исключенных исключениях. Чтобы продемонстрировать это, следующая версия 2 класса SuppressedExceptions (которая использует Throwable.getSuppressed () ) показана ниже.

SuppressedExceptions.java (Версия 2)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package dustin.examples;
 
import static java.lang.System.err;
 
/**
 * Demonstrate JDK 7's Suppressed Exceptions API support.
 *
 * @author Dustin
 */
public class SuppressedExceptions
{
   /**
    * Method that uses NaughtyResource with try-with-resource statement.
    *
    * @throws Exception Expected exception for try-with-resource used on the
    *    NaughtyResource.
    */
   public static void performTryWithResource() throws Exception
   {
      try (NaughtyResource naughty = new NaughtyResource())
      {
         naughty.doNothingGood();
      
   }
 
   /**
    * Executable function demonstrating suppressed exceptions.
    *
    * @param arguments The command line arguments; none expected.
    */
   public static void main(String[] arguments)
   {
      try
      {
         performTryWithResource();
      }
      catch (Exception ex)
      {
         err.println("Exception encountered: " + ex.toString());
         final Throwable[] suppressedExceptions = ex.getSuppressed();
         final int numSuppressed = suppressedExceptions.length;
         if (numSuppressed > 0)
         {
            err.println("\tThere are " + numSuppressed + " suppressed exceptions:");
            for (final Throwable exception : suppressedExceptions)
            {
               err.println("\t\t" + exception.toString());
            }
         }
      }
   }
}

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

Как описано в Учебном руководстве по Java , мы видим, что единственное выбрасываемое исключение — это исключение, встречающееся в блоке try оператора try-with-resources, а второе исключение, встречающееся при закрытии ресурса, «подавляется» (но все еще связывается и доступно из собственно выброшенное исключение).

Не нужно использовать try-with-resources для работы с подавленными исключениями. Следующий листинг кода адаптирует SuppressedExceptions для использования более традиционного try-finally.

SuppressedExceptions.java (Версия 3)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package dustin.examples;
 
import static java.lang.System.err;
 
/**
 * Demonstrate JDK 7's Suppressed Exceptions API support.
 *
 * @author Dustin
 */
public class SuppressedExceptions
{
   /**
    * Method that uses NaughtyResource with JDK 7 try-with-resource statement.
    *
    * @throws Exception Expected exception for try-with-resource used on the
    *    NaughtyResource.
    */
   public static void performTryWithResource() throws Exception
   {
      try (NaughtyResource naughty = new NaughtyResource())
      {
         naughty.doNothingGood();
      
   }
 
   /**
    * Method that uses NaughtyResource with traditional try-finally statement.
    *
    * @throws Exception Exception thrown during use of NaughtyResource.
    */
   public static void performTryFinally() throws Exception
   {
      final NaughtyResource naughty = new NaughtyResource();
      try
      {
         naughty.doNothingGood();
      }
      finally
      {
         naughty.close();
      }
   }
 
   /**
    * Executable function demonstrating suppressed exceptions.
    *
    * @param arguments The command line arguments; none expected.
    */
   public static void main(String[] arguments)
   {
      try
      {
//         performTryWithResource();
         performTryFinally();
      }
      catch (Exception ex)
      {
         err.println("Exception encountered: " + ex.toString());
         final Throwable[] suppressedExceptions = ex.getSuppressed();
         final int numSuppressed = suppressedExceptions.length;
         if (numSuppressed > 0)
         {
            err.println("\tThere are " + numSuppressed + " suppressed exceptions:");
            for (final Throwable exception : suppressedExceptions)
            {
               err.println("\t\t" + exception.toString());
            }
         }
         else
         {
            err.println("\tNo Suppressed Exceptions.");
         }
      }
   }
}

Приведенный выше код вызывает метод, использующий try-finally, и его поведение отличается от поведения в примере с ресурсами, как показано на следующем снимке экрана.

В примере try-finally показано исключение, возникшее при попытке закрытия ресурса, а не исключение, возникшее при использовании ресурса (в отличие от попытки с ресурсами, показанной выше). Дополнительным отличием является то, что другое исключение недоступно даже как исключенное исключение в случае try-finally. Это пример широко разрекламированной проблемы « потерянного исключения », касающейся потенциально «тривиального» исключения (закрытие ресурса), скрывающего потенциально более значимое исключение (фактически использующее ресурс).

Если я хочу видеть оба исключения, возникающие при использовании, и закрытие NaughtyResource в сочетании с try-finally, я могу использовать Throwable.addSuppressed (Throwable), как показано в следующем листинге кода. В этом листинге предложения try и finally улучшены, чтобы захватить исключение, выброшенное во время использования NaughtyResource, и добавить это перехваченное исключение к фактически выданному исключению как подавленное исключение.

SuppressedExceptions.java (Версия 4)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package dustin.examples;
 
import static java.lang.System.err;
 
/**
 * Demonstrate JDK 7's Suppressed Exceptions API support.
 *
 * @author Dustin
 */
public class SuppressedExceptions
{
   /**
    * Method that uses NaughtyResource with JDK 7 try-with-resource statement.
    *
    * @throws Exception Expected exception for try-with-resource used on the
    *    NaughtyResource.
    */
   public static void performTryWithResource() throws Exception
   {
      try (NaughtyResource naughty = new NaughtyResource())
      {
         naughty.doNothingGood();
      
   }
 
   /**
    * Method that uses NaughtyResource with traditional try-finally statement.
    *
    * @throws Exception Exception thrown during use of NaughtyResource.
    */
   public static void performTryFinally() throws Exception
   {
      final NaughtyResource naughty = new NaughtyResource();
      Throwable throwable = null;
      try
      {
         naughty.doNothingGood();
      }
      catch (Exception usingEx)
      {
         throwable = usingEx;
      }
      finally
      {
         try
         {
            naughty.close();
         }
         catch (Exception closingEx)
         {
            if (throwable != null)
            {
               closingEx.addSuppressed(throwable);
               throw closingEx;
            }
         }
      }
   }
 
   /**
    * Executable function demonstrating suppressed exceptions.
    *
    * @param arguments The command line arguments; none expected.
    */
   public static void main(String[] arguments)
   {
      try
      {
//         performTryWithResource();
         performTryFinally();
      }
      catch (Exception ex)
      {
         err.println("Exception encountered: " + ex.toString());
         final Throwable[] suppressedExceptions = ex.getSuppressed();
         final int numSuppressed = suppressedExceptions.length;
         if (numSuppressed > 0)
         {
            err.println("\tThere are " + numSuppressed + " suppressed exceptions:");
            for (final Throwable exception : suppressedExceptions)
            {
               err.println("\t\t" + exception.toString());
            }
         }
         else
         {
            err.println("\tNo Suppressed Exceptions.");
         }
      }
   }
}

Вывод измененного класса показан далее. Обратите внимание, что исключение, возникающее в результате использования самого ресурса, теперь связано с брошенным исключением как подавленным исключением . Хотя здесь и не показано, Throwable также теперь предоставляет конструктор, который позволяет посредством логического аргумента указывать, разрешены ли (исключены) ли запрещенные исключения для вновь созданного экземпляра Throwable.

Другая перспектива для рассмотрения исключенных исключений — это трассировка полного стека. Следующие три изображения представляют собой снимки экрана с трассировками полного стека, полученными в результате использования try-with-resources (явно показывает подавленное исключение), использования традиционного оператора try-finally без явного добавления подавленных исключений (и, следовательно, никаких исключенных исключений в трассировке полного стека) и использование традиционного try-finally с явно добавленными подавленными исключениями (и, таким образом, они появляются в полной трассировке стека). Я добавил красную линию к каждому изображению, чтобы отдельно показать, где заканчивается трассировка полного стека, и обвел явную ссылку на подавленное исключение, где это применимо.

Исключенные исключения по сравнению с цепочечными исключениями

Подавленные исключения не совпадают с цепочечными исключениями . Связанные исключения были введены в JDK 1.4 и предназначались для того, чтобы можно было легко отслеживать причинно-следственные связи между исключениями. Как правило, связанные цепочки исключений возникали в результате связывания вновь возникшего исключения с исключением, которое было перехвачено и вызвало создание нового исключения. Например, может быть выдано непроверенное исключение, которое «оборачивает» проверенное исключение, которое было перехвачено, и они могут быть объединены в цепочку.

Подавленные исключения были введены в JDK 7 и касаются не столько причинно-следственных связей, сколько о представлении возможных связанных, но не обязательно причинно-следственных множественных исключительных условий в одном выдвинутом исключении. В моем примере с непослушным ресурсом исключение непослушного ресурса при вызове его единственного метода НЕ было причиной того, что оно вызвало исключение при вызове его метода close. Из-за этого более логично связывать два исключения (через механизм подавленных исключений), чем заставлять одно казаться причиной другого в связанных отношениях.

Может быть проще всего быстро понять разницу между связанными исключениями и подавленными исключениями, сравнивая методы Throwable для доступа к каждому типу. Для связанных исключений вызывается метод конкретного исключения (Throwable). Этот метод возвращает один экземпляр вызывающего Throwable. Возвращенного Throwable можно спросить о его причине, и процесс повторяется по цепочке вызывающих Throwable. Важным наблюдением является то, что у каждого данного Throwable есть один вызывающий Throwable. Это можно противопоставить способности Throwable предоставлять несколько подавленных Throwable (в массиве), когда вызывается его метод getSuppressed () .

NetBeans рекомендует попробовать с ресурсом

Здесь стоит отметить, что NetBeans предупреждает об использовании try-finally и предлагает заменить его на try-with-resources, как обсуждалось в моем посте « Семь советов NetBeans для модернизации кода Java» и на снимке экрана, показанном далее.

Вывод

Полагаю, очевидно, почему NetBeans рекомендует разработчику изменить вариант try-finally для обработки ресурсов на оператор try-with-resources. Часто предпочтительно сначала узнать, какая операция над ресурсом вызвала исключение, но также желательно иметь возможность доступа к исключению при закрытии, если это необходимо. Если бы мне пришлось выбирать, меня, как правило, больше интересовала бы проблема ресурса во время выполнения, потому что проблема закрытия могла бы быть производной от этого. Тем не менее, иметь оба еще лучше. Традиционный try-finally только перечисляет исключение после закрытия без дополнительных усилий для передачи обоих. Оператор try-with-resource не только более лаконичен; это также более полезно благодаря встроенной поддержке включения исключенных исключений.

Справка: поддержка исключенных исключений в Java 7 от нашего партнера по JCG Дастина Маркса в блоге Inspired by Actual Events .