Используйте assertions (утверждения или ассерты) Java для проверки корректности кода и для ускорения тестирования и отладки ваших программ.
Джефф Фризен
Написание программ, которые правильно работают, может оказаться сложной задачей. Поскольку наши предположения о том, как будет вести себя код при выполнении, часто ошибочны. Использование ассертов в Java — это один из способов проверить правильность логики программирования.
В этом руководстве рассказывается об ассертах в Java. Сначала вы узнаете, что такое ассерты и как их определять и использовать в коде. Далее вы узнаете, как использовать ассерты для обеспечения выполнения предварительных и постусловий. Наконец, вы сравните ассерты с исключениями и выясните, зачем в коде нужно и то и другое.
Загрузите исходный код примеров для этой статьи. Создано Джеффом Фризеном для JavaWorld.
Что такое ассерты в Java?
До JDK 1.4 разработчики часто использовали комментарии для документирования предположений о правильном использовании методов программы. Однако комментарии бесполезны как механизм для проверки и отладки предположений. Компилятор игнорирует комментарии, поэтому нет возможности использовать их для обнаружения ошибок. Разработчики также часто не обновляют комментарии при изменении кода.
В JDK 1.4 ассерты были введены как новый механизм для тестирования и отладки кода. По сути, ассерты — это компилируемые сущности, которые выполняются в runtime, если вы включили их для тестирования программы. Вы можете запрограммировать ассерты так, чтобы они уведомляли вас об ошибках ещё до того как они произошли и программа “упала”, что значительно сокращает время на отладку неисправной программы.
Ассерты используются для определения требований, исполнение которых делают программу правильной. И делается это при помощи проверки условий (логических выражений) на true. Если же выражение выдаёт false, разработчик получает уведомление об этом. Использование утверждений может значительно повысить вашу уверенность в правильности кода.
Как написать ассерт на Java
Ассерты реализуются с помощью оператора assert и класса java.lang.AssertionError. Этот оператор начинается с ключевого слова assert и продолжается логическим выражением. Синтаксически это выражается следующим образом:
assert BooleanExpr;
Если значение BooleanExpr истинно, ничего не происходит и выполнение продолжается. Однако, если выражение оценивается как ложное, бросается AssertionError, как, например, в этом коде:
Листинг 1: AssertDemo.java (версия 1)
public class AssertDemo { public static void main(String[] args) { int x = -1; assert x >= 0; } }
ассерт здесь говорит о том, что по мнению разработчика переменная x должна быть больше или равна 0. Однако это явно не так; выполнение оператора assert приводит к выбросу AssertionError.
Скомпилируйте этот код (javac AssertDemo.java) и запустите его с включенными ассертами (java -ea AssertDemo) (прим.пер.: все популярные среды программирования позволяют включать ассерты через настройку). Вы увидите следующий результат:
Exception in thread "main" java.lang.AssertionError at AssertDemo.main(AssertDemo.java:6)
Это сообщение несколько загадочно, так как не указывает, что привело к AssertionError. Если вам нужно более информативное сообщение, используйте assert с таким синтаксисом:
assert BooleanExpr : expr;
Здесь expr любое выражение, включая вызов метода (прим.пер.: в соответствии с лучшими современными практиками рекомендуется отказаться от вызовов методов из ассертов), которое должно возвращать значение — вы не можете вызвать метод void. Удобно использовать просто строку, описывающую причину сбоя, как показано ниже:
Листинг 2: AssertDemo.java (версия 2)
public class AssertDemo { public static void main(String[] args) { int x = -1; assert x >= 0: "x < 0"; } }
Запустите этот код с включенными ассертами. На этот раз вы увидите чуть больше информации, объясняющей причину выброса AssertionError:
Exception in thread "main" java.lang.AssertionError: x < 0 at AssertDemo.main(AssertDemo.java:6)
Для любого из этих примеров, выполнение AssertDemo без -ea опции (enable assertions) не приведет к бросанию исключения. Если ассерты отключены, они не выполняются, хотя и присутствуют в файле класса.
Предварительные условия и постусловия
Ассерты проверяют предварительные и постусловия на true, и предупреждают разработчика, когда происходит нарушение:
- Предварительное условие — это условие, которое должно оцениваться как истинное перед выполнением некоторого метода. Выполнение предварительных условий гарантирует, что вызывающие методы соблюдают контракты с вызываемыми методами.
- Постусловие предполагает проверку на true после выполнения некоторой кодовой последовательности. Постусловия гарантируют, что вызываемые методы соблюдают свои контракты с вызывающими.
Предварительные условия
Вы можете обеспечить выполнение предварительных условий для общедоступных конструкторов и методов, выполнив явные проверки и выбрасывая исключения при необходимости. Для private вспомогательных методов вы можете обеспечить выполнение предварительных условий, используя ассерты. Рассмотрим листинг 3.
Листинг 3: AssertDemo.java (версия 3)
import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; class PNG { /** * Create a PNG instance, read specified PNG file, and decode * it into suitable structures. * * @param filespec path and name of PNG file to read * * @throws NullPointerException when <code>filespec</code> is * <code>null</code> */ PNG(String filespec) throws IOException { // Enforce preconditions in non-private constructors and // methods. if (filespec == null) throw new NullPointerException("filespec is null"); try (FileInputStream fis = new FileInputStream(filespec)) { readHeader(fis); } } private void readHeader(InputStream is) throws IOException { // Confirm that precondition is satisfied in private // helper methods. assert is != null : "null passed to is"; } } public class AssertDemo { public static void main(String[] args) throws IOException { PNG png = new PNG((args.length == 0) ? null : args[0]); } }
Создание экземпляра класса PNG
в этом коде является необходимым для чтения и декодирования PNG (Portable Network Graphics) файлов изображений. Конструктор явно сравнивает filespec
с null
, выбрасывая NullPointerException
, если он равен null
. Смысл в том, чтобы обеспечить соблюдение предусловия, которое предполагает, что filespec
не может быть равным null
.
Однако нецелесообразно указывать assert filespec != null;
, потому что предварительное условие, упомянутое в документации Javadoc конструктора, не будет (технически) проверяться, когда ассерты отключены (в данном случае, эта проверка будет просто страховкой, потому что FileInputStream()
и без нас бросит NullPointerException
, но негоже зависеть от недокументированного поведения).
С другой стороны, assert
будет хорош во вспомогательном private методе readHeader()
, который в конечном итоге завершится чтением и декодированием 8-байтового заголовка PNG файла. Предварительное условие здесь также состоит в том, что параметр is
не должен быть равен null
.
Постусловия
Постусловия обычно указываются через ассерты, независимо от того, является ли метод (или конструктор) общедоступным. Рассмотрим такой код:
Листинг 4: AssertDemo.java (версия 4)
public class AssertDemo { public static void main(String[] args) { int[] array = { 20, 91, -6, 16, 0, 7, 51, 42, 3, 1 }; sort(array); for (int element: array) System.out.printf("%d ", element); System.out.println(); } private static boolean isSorted(int[] x) { for (int i = 0; i < x.length - 1; i++) if (x[i] > x[i + 1]) return false; return true; } private static void sort(int[] x) { int j, a; // For all integer values except the leftmost value ... for (int i = 1; i < x.length; i++) { // Get integer value a. a = x[i]; // Get index of a. This is the initial insert position, which is // used if a is larger than all values in the sorted section. j = i; // While values exist to the left of a's insert position and the // value immediately to the left of that insert position is // numerically greater than a's value ... while (j > 0 && x[j - 1] > a) { // Shift left value -- x[j - 1] -- one position to its right -- // x[j]. x[j] = x[j - 1]; // Update insert position to shifted value's original position // (one position to the left). j--; } // Insert a at insert position (which is either the initial insert // position or the final insert position), where a is greater than // or equal to all values to its left. x[j] = a; } assert isSorted(x): "array not sorted"; } }
В этом коде представлен вспомогательный метод sort()
, который использует алгоритм сортировки вставкой для массива целочисленных значений. Я имел обыкновение проверять постусловие сортировки x через assert
перед возвратом методом sort()
результата вызывающему коду.
Этот пример демонстрирует важную характеристику ассертов — их выполнение, обычно, ресурсозатратно. По этой причине в промышленной версии программы ассерты обычно отключены. В методе isSorted()
необходимо сканировать весь массив, что может занять много времени в случае большого массива.
Ассерты против исключений в Java
Разработчики используют ассерты для документирования логически запрещённых ситуаций и для обнаружения ошибок в логике программы. Во время её выполнения включенный ассерт предупреждает разработчика о логической ошибке. Разработчик меняет исходный код, чтобы исправить логическую ошибку, а затем перекомпилирует этот код.
Разработчики используют механизм исключений Java для ответа на нефатальные (например, нехватку памяти) ошибки, которые могут быть вызваны факторами окружающей среды, такими как несуществующий файл, или плохо написанным кодом, например попыткой разделить на 0. Обработчик исключений часто пишется так, чтобы после ошибки программа могла продолжить работу.
Ассерты не заменяют исключения. В отличие от исключений, ассерты не поддерживают восстановление после ошибок (ассерты обычно немедленно останавливают выполнение программы — AssertionError не предназначены для перехвата); они часто отключены в промышленном коде; и они обычно не отображают удобные для пользователя сообщения об ошибках (хотя это не проблема assert). Важно знать, когда использовать исключения, а не ассерты.
Когда использовать исключения
Предположим, вы написали sqrt()метод, который вычисляет квадратный корень из своего параметра. В контексте действительных чисел невозможно извлечь квадратный корень из отрицательного числа. Следовательно, вы используете ассерт для отказа от исполнения метода, если аргумент отрицательный. Рассмотрим следующий фрагмент кода:
public double sqrt(double x) { assert x >= 0 : "x is negative"; // ... }
Неуместно использовать ассерт для проверки аргумента в этом public методе. Ассерт предназначен для обнаружения ошибок в логике программирования, а не для защиты метода от ошибочных значений параметров. Кроме того, если ассерты отключены, невозможно решить проблему отрицательного аргумента. Лучше создать исключение следующим образом:
public double sqrt(double x) { if (x < 0) throw new IllegalArgumentException("x is negative"); // ... }
Разработчик может выбрать, обрабатывать исключение недопустимого аргумента или пробросить его, и тогда сообщение об ошибке отображается инструментом, запускающим программу. Прочитав сообщение об ошибке, разработчик может исправить код, вызвавший исключение.
Вы могли заметить тонкую разницу между ассертом и условием обнаружения ошибок. Ассерт требует x >= 0
, а условие обнаружения ошибок: x < 0
. Ассерт оптимистичен: мы предполагаем, что аргумент в порядке. Напротив, условие обнаружения ошибок пессимистично: мы предполагаем, что аргумент неверен. Ассерты документируют правильную логику, тогда как исключения документируют неправильное поведение во время выполнения.
В этом руководстве вы узнали, как использовать ассерты для документирования правильной логики программы. Вы также узнали, почему ассерты не заменяют исключения, и вы видели пример, в котором использование исключения было бы более эффективным.
Этот рассказ «Как использовать ассерты в Java» был первоначально опубликован JavaWorld .
Перевод Академии Progwards