Статьи

Редкое использование «ControlFlowException»

Потоки управления являются «пережитком» императивного программирования , которое просочилось в различные другие парадигмы программирования, включая объектно-ориентированную парадигму Java. Помимо полезных и вездесущих ветвящихся и петлевых структур, существуют также примитивы (например, GOTO) и нелокальные (например, исключения). Давайте подробнее рассмотрим эти противоречивые методы управления потоком.

ПЕРЕЙТИ К

gotoзарезервированное слово в языке Java. goto также является допустимой инструкцией в байт-коде JVM . Тем не менее, в Java нелегко выполнить операции goto . Один пример, взятый из этого вопроса переполнения стека, можно увидеть здесь:

Прыгать вперед

1
2
3
4
5
label: {
    // do stuff
    if (check) break label;
    // do more stuff
}

В байт-коде:

1
2
3
2  iload_1 [check]
    3  ifeq 6          // Jumping forward
    6  ..

Прыгать назад

1
2
3
4
5
6
label: do {
    // do stuff
    if (check) continue label;
    // do more stuff
    break label;
} while(true);

В байт-коде:

1
2
3
4
2  iload_1 [check]
     3  ifeq 9
     6  goto 2          // Jumping backward
     9  ..

Конечно, эти уловки полезны только в очень редких случаях, и даже тогда вы можете захотеть пересмотреть. Потому что мы все знаем, что происходит, когда мы используем goto в нашем коде:

Чертеж взят из xkcd: http://xkcd.com/292/

Выход из-под контроля потоков с исключениями

Исключения являются хорошим инструментом для выхода из структуры потока управления в случае ошибки или сбоя. Но регулярный переход вниз (без ошибок или сбоев) также может быть выполнен с использованием исключений:

1
2
3
4
5
6
try {
    // Do stuff
    if (check) throw new Exception();
    // Do more stuff
}
catch (Exception notReallyAnException) {}

Это кажется таким же грязным, как и уловки с ярлыками, упомянутыми ранее.

Законное использование исключений для потока управления:

Однако есть и другие очень редкие случаи, когда исключения являются хорошим инструментом для выхода из сложного, вложенного потока управления (без ошибок или сбоев). Это может иметь место, когда вы анализируете XML-документ с использованием SAXParser . Возможно, ваша логика будет проверять наличие как минимум трех элементов <check/> , в случае чего вы можете пропустить синтаксический анализ остальной части документа. Вот как это реализовать:

Создайте ControlFlowException :

1
2
3
4
package com.example;
 
public class ControlFlowException
extends SAXException {}

Обратите внимание, что обычно для этого вы можете предпочесть RuntimeException , но контракты SAX требуют, чтобы реализации обработчика вместо этого генерировали SAXException .

Используйте это ControlFlowException в обработчике SAX:

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
package com.example;
 
import java.io.File;
 
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
 
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
 
public class Parse {
  public static void main(String[] args)
  throws Exception {
    SAXParser parser = SAXParserFactory
        .newInstance()
        .newSAXParser();
 
    try {
      parser.parse(new File("test.xml"),
          new Handler());
      System.out.println(
          "Less than 3 <check/> elements found.");
    } catch (ControlFlowException e) {
      System.out.println(
          "3 or more <check/> elements found.");
    }
  }
 
  private static class Handler
  extends DefaultHandler {
 
    int count;
 
    @Override
    public void startElement(
        String uri,
        String localName,
        String qName,
        Attributes attributes) {
 
      if ("check".equals(qName) && ++count >= 3)
        throw new ControlFlowException();
    }
  }
}

Когда использовать исключения для потока управления:

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

  • Вы хотите выйти из сложного алгоритма (в отличие от простого блока).
  • Вы можете реализовать «обработчики» для введения поведения в сложные алгоритмы.
  • Эти «обработчики» явно разрешают создавать исключения в своих контрактах.
  • Ваш вариант использования не тяготеет к фактическому рефакторингу сложного алгоритма.

Пример из реальной жизни: пакетные запросы с помощью jOOQ

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

Поскольку каждая запись инкапсулирует свой сгенерированный SQL-рендеринг и выполнение для данного вызова store() объектно-ориентированным способом, было бы довольно сложно извлечь алгоритм рендеринга SQL многократно используемым способом, не нарушая (или не подвергая) слишком много вещей. Вместо этого, пакетная операция jOOQ реализует этот простой псевдоалгоритм:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
// Pseudo-code attaching a "handler" that will
// prevent query execution and throw exceptions
// instead:
context.attachQueryCollector();
 
// Collect the SQL for every store operation
for (int i = 0; i < records.length; i++) {
  try {
    records[i].store();
  }
 
  // The attached handler will result in this
  // exception being thrown rather than actually
  // storing records to the database
  catch (QueryCollectorException e) {
 
    // The exception is thrown after the rendered
    // SQL statement is available
    queries.add(e.query());               
  }
}

Пример из реальной жизни: исключительно меняющееся поведение

Другой пример из jOOQ показывает, как этот метод может быть полезен для представления исключительного поведения, которое применимо только в редких случаях. Как объясняется в выпуске № 1520 , некоторые базы данных имеют ограничение на количество возможных значений связывания на оператор. Эти:

  • SQLite: 999
  • Энгр 10.1.0: 1024
  • Sybase ASE 15.5: 2000
  • SQL Server 2008: 2100

Чтобы обойти это ограничение, jOOQ необходимо будет встроить все значения связывания, как только будет достигнут максимум. Поскольку модель запросов jOOQ сильно инкапсулирует рендеринг SQL и поведение привязки переменных путем применения составного шаблона, невозможно узнать количество значений привязки до обхода дерева модели запросов. Более подробную информацию об архитектуре модели запросов jOOQ можно найти в этом предыдущем сообщении в блоге: http://blog.jooq.org/2012/04/10/the-visitor-pattern-re-visited

Таким образом, решение состоит в том, чтобы визуализировать оператор SQL и подсчитать значения привязки, которые будут эффективно отображаться. Каноническая реализация была бы этим псевдокодом:

1
2
3
4
5
6
7
8
9
String sql;
 
query.renderWith(countRenderer);
if (countRenderer.bindValueCount() > maxBindValues) {
  sql = query.renderWithInlinedBindValues();
}
else {
  sql = query.render();
}

Как можно видеть, каноническая реализация должна будет визуализировать оператор SQL дважды. Первый рендеринг используется только для подсчета количества значений связывания, тогда как второй рендеринг генерирует истинный оператор SQL. Проблема здесь заключается в том, что исключительное поведение должно применяться только после возникновения исключительного события (слишком много значений привязки). Гораздо лучшим решением является введение «обработчика», который подсчитывает значения привязки в обычной «попытке рендеринга», ControlFlowException исключение ControlFlowException для тех немногих исключительных «попыток», где число значений привязки превышает максимум:

01
02
03
04
05
06
07
08
09
10
11
12
13
// Pseudo-code attaching a "handler" that will
// abort query rendering once the maximum number
// of bind values was exceeded:
context.attachBindValueCounter();
String sql;
try {
 
  // In most cases, this will succeed:
  sql = query.render();
}
catch (ReRenderWithInlinedVariables e) {
  sql = query.renderWithInlinedBindValues();
}

Второе решение лучше, потому что:

  • Мы переопределяем запрос только в исключительном случае.
  • Мы не заканчиваем рендеринг запроса, чтобы вычислить фактическое количество, но рано прерываем для повторного рендеринга. Т.е. нам все равно, есть ли у нас 2000, 5000 или 100000 значений привязки.

Вывод

Как и во всех исключительных методах, не забывайте использовать их в нужный момент. Если сомневаетесь, подумайте еще раз.

Ссылка: Редкое использование «ControlFlowException» от нашего партнера по JCG Лукаса Эдера в блоге JAVA, SQL и AND JOOQ .