Санкт-Петербургская группа тестирования JVM


« Взаимодействие VM с... | Main | Новый блог »
20070525 пятница Май 25, 2007

Проверка утверждений (assertions) и условная компиляция

Начиная с JDK 1.4, в языке Java появилась встроенная поддержка проверка утверждений (assertions). Утверждение—это код, используемый, как правило, только во время разработки, с помощью которого программа проверяет правильность своего выполнения. Использование утверждений является очень эффективным и дешёвым способом обнаружения ошибок в логике программы. Думаю, что принципы использования утверждений хорошо всем известны, поэтому в этом посте я остановлюсь на интересных особенностях реализации механизма проверки утверждений в Java (документация JDK содержит хорошее описание механизма утверждений, в частности, там описана такая полезная возможность, как включение/выключение утверждений для отдельных классов и иерархий пакетов—Programming With Assertions).

На уровне языка проверка утверждений реализована с помощью введения нового оператора assert, который имеет форму

    assert [Выражение типа boolean];

или

    assert [Выражение типа boolean] : [Выражение любого типа, кроме void];

Во время выполнения программы в том случае, если поверка утверждений включена, вычисляется значение булевского выражения, и если его результат false, то генерируется исключение, имеющее тип java.lang.AssertionError (подкласс java.lang.Error). В случае использования второй формы оператора assert выражение после двоеточия задаёт детальное сообщение о произошедшей ошибке (вычисленное выражение будет преобразовано в строку и передано конструктору AssertionError).

Проверка утверждений во время выполнения программы предполагает выполнение произвольных (и, возможно, длительных) вычислений, которые могут серьёзно повлиять на производительность приложения. Одна из самых привлекательных особенностей механизма утверждений—это возможность отключения проверки утверждений в промышленной версии приложения. Таким образом, утверждения это простой и удобный механизм для поиска ошибок во время разработки, который не оказывает никакого влияния на работу готового продукта (конечно, это справедливо только в случае корректного использования утверждений).

На самом деле, заявление, что механизм проверки утверждений после его выключения не оказывает абсолютно никакого влияния на выполнение приложения не совсем верно. Это связано с выбранным способом реализации этого механизма: для реализации поддержки утверждений был выбран способ, требующий минимальных изменений в виртуальной машине и никак не затрагивающий формат класс-файла.

Описание деталей реализации можно прочитать в документе A Simple Assertion Facility For the Java Programming Language. Вкратце, механизм таков: компилятор Java вставляет в каждый класс, содержащий операторы assert, специальное поле:

 if (!$assertionsDisabled) {
if (!Expression1)
throw new AssertionError(Expression2);
}

Например, если скомпилировать следующий класс:

public class TestAssertions {
public static void main(String[] args) {
assert false : "Error";
}
}

то для метода main генерируется следующий байт-код:

    0 getstatic #16 <TestAssertions.$assertionsDisabled>
3 ifne 16

6 new #27 <java/lang/AssertionError>
9 dup
10 ldc #29 <Error>
12 invokespecial #31 <java/lang/AssertionError.<init>>
15 athrow

16 return

Возможно, тот факт, что даже при выключенной проверке утверждений приложение должно постоянно выполнять проверку условия if ( ! $assertionsDisabled ), вызывает опасения, не окажет ли это влияния на производительность. Не лучше ли было бы, если утверждения выключены, удалять этот код из класса сразу после его загрузки? На самом деле, в этом нет необходимоти: поле $assertionsDisabled является константой и не меняется после инициализации класса. Компилятор JIT легко может определить, что операторы после if ( ! $assertionsDisabled )никогда не выполняются и удалить их из генерируемого кода. Cовременные компиляторы (включая HotSpot) выполняют эту простую оптимизацию одной левой.

Другой особенностью механизма утверждений в Java, вызвающей вопросы, является то, что не существует прямого способа полностью удалить информацию об утверждениях из класс-файла (например, опцией компилятора). Отсутствие такой функциональности—не случайный просчёт разработчиков, а намеренное решение, которое направлено на повышение надёжности Java-приложений и облегчение их сопровождения (кроме того, особенности спецификации механизма утверждений в Java не позволяют удалять утверждения из класс-файла). В тоже время, невозможность удаления кода утверждений и, следовательно, вызванное этим увеличение размера класс-файлов, может быть препятствием для использования утверждений при написании программ для устройств с ограниченными ресурсами, для которых размер приложения имеет большое значение.

Спецификация языка Java позволяет реализовать функциональность оператора assert (отключение вычисления отладочных выражений в промышленной версии продукта) с помощью так называемой условной компиляции, при этом условная компиляция помогает избежать упомянутого недостатка оператора assert.

Условная компиляция возможна в языке Java благодаря тому, что спецификация позволяет особым образом компилировать оператор if, принимающий в качестве операнда константу, значение которой известно на этапе компиляции (JLS 14.21).

Рассмотрим следующий пример:

// Constants.java
public class Constants {
public static final boolean DEBUG = true;
}

// TestConstants.java
public class TestConstants {
public static void main(String[] args) {
if (Constants.DEBUG) {
System.out.println("Вычисление утверждений включено");
} else {
System.out.println("Вычисление утверждений выключено");
}
}
}

Скомпилируем эти классы и посмотрим, как выглядит код метода TestConstants.main:

    0 getstatic #16 <java/lang/System.out>
3 ldc #22 <Вычисление утверждений включено>
5 invokevirtual #24 <java/io/PrintStream.println>
8 return

Думаю, что даже человеку не знакомому с форматом байт-кода очевидно, что скомпилированная версия TestConstants.main не содержит условного оператора, и байт-код содержит только одну ветку if, ту, которая выполняется в том случае, когда поле Constants.DEBUG имеет значение true.

Очевидно, что раз метод TestConstants.main вообще не обращается к полю Constants.DEBUG, то если изменить и перекомпилировать только класс Constants.DEBUG:

// Constants.java
public class Constants {
public static final boolean DEBUG = false;
}
%javac Constants.java
%

это никак не скажется на работе TestConstants.main, и этот метод будет по-прежнему вести себя так, как будто Constants.DEBUG = true:

% java TestConstants
Вычисление утверждений включено
%

Такое поведение компилятора может показаться неожиданным или даже некорректным, но возможность такого поведения—это часть спецификации языка Java. Согласно JLS 13.4.9 в случае изменения значения константы, все классы, использующие эту константу, должны быть перекомпилированы, иначе эти классы будут продолжать использовать старое значение. Об этом следует помнить и объявлять константами только те значения, которые на самом деле никогда не будут меняться (более подробное обсуждение этого вопроса—JLS 13.4.9).

Мне кажется, что упомянутая особенность языка Java не должна вызывать опасений: если использовать механизмы, предоставляемые языком корректно, никаких проблем возникать не должно, например, использование условной компиляции для устранения утверждений из кода готового продукта абсолютно безопасно. Для этого каждый оператор assert необходимо использовать вместе с условным оператором:

    if (Constants.DEBUG) {
assert expression : "Error message";
}

В результате получаем все преимущества оператора assert с возможностью удаления кода проверки утверждений из класс-файла готового продукта (главное—не забыть заново откомпилировать всё приложение после изменения значения константы Constants.DEBUG).

В заключении хочу отметить, что описанный способ использования оператора assert вместе с условной компиляцией ни в коем случае не является рекомендуемым, и его следует применять только в том случае, если увеличение класс-файла за счёт утверждений на самом деле очень критично для вашего приложения.

Ссылки:

Семён Бойков

опубликовал vmrobot ( май 25 2007, 06:53:00 PM MSD ) Permalink Комментарии [0]

Trackback URL: http://blogs.sun.com/vmrobot/entry/%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D1%80%D0%BA%D0%B0_%D1%83%D1%82%D0%B2%D0%B5%D1%80%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B9_assertions_%D0%B8_%D1%83%D1%81%D0%BB%D0%BE%D0%B2%D0%BD%D0%B0%D1%8F
Комментарии:

Опубликовать комментарий:

Имя
E-Mail:
URL:

Ваш комментарий:

HTML Syntax: Отключен

Хиты страниц за сегодня: 72