Программирование-по-Контракту в Java

  • Tutorial
Добрый день.
В рамках детальной проработки курса удаленного образования «Java Core» я пишу серию публикаций и делаю несколько переводов наиболее популярных статей.

Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

Сейчас я предлагаю Вам на рассмотрение мой перевод «Programming With Assertions» с некоторыми комментариями.

Оригинальная публикация не только детально объясняет варианты использования ключевого слова assert в Java и то, как реализована поддержка данного механизма на уровне загрузки классов, но также является достаточно неформальным введением в Разработку-по-Контракту (Design-by-Contract).




P.S. Автор признает, что для него лично русский язык намного сложнее языка Java, с благодарностью выслушает в личку о всех замеченных ошибках и постарается их в кратчайшие сроки исправить.



Программирование с утверждениями (oracle.com: Programming With Assertions)

Утверждение (assert) — это оператор (statement) языка программирования Java, который позволяет вам проверить свои предположения о программе. Например, если вы пишете метод, вычисляющий скорость частицы, можно «утверждать», что расчетная скорость меньше скорости света.

Каждое утверждение содержит логическое выражение, которое, по вашему мнению, будет верным в момент выполнения. В противном случае, система выбросит исключение. Проверяя, что логическое выражение на самом деле верно, утверждение (assert) подтверждает ваши предположения (ожидания) о поведении программы, увеличивая уверенность в том, что программа не содержит ошибок.

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

Данная статья показывает, как программировать с утверждениями. Она охватывает следующие темы:
Введение
Внедрение утверждений в код
Компиляция файлов, использующие утверждения
Включение и отключение утверждений
Совместимость с существующими программами
Часто задаваемые вопросы по дизайну


Введение


Оператор assert имеет две формы. Первая, более простая форма – это:
assert Expression1;

где Expression1 – это boolean-выражение. Когда система проверяет утверждение, она вычисляет Expression1 и если оно ложно (равно false), то система бросает java.lang.AssertionError без детализированного сообщения об ошибке.
Вторая форма утверждения – это:
assert Expression1 : Expression2;

где:
  • Expression1 – логическое выражение.
  • Expression2 – это выражение, которое имеет значение (не может быть вызовом void метода).

Используйте эту версию assert-оператора для того, чтобы предоставить детализированное сообщение об ошибке. Система передает значение Expression2 в соответствующий конструктор AssertionError для использования строкового представления значения в качестве подробного сообщение об ошибке.
Комментарий переводчика
AssertError содержит отдельные конструкторы для каждого типа данных (short и byte автоматически приводятся к int, для них не предоставили конструкторов)
package java.lang;

public class AssertionError extends Error {
    ...
    public AssertionError(Object detailMessage) {...}
    public AssertionError(boolean detailMessage) {...}
    public AssertionError(char detailMessage) {...}
    public AssertionError(int detailMessage) {...}
    public AssertionError(long detailMessage) {...}
    public AssertionError(float detailMessage) {...}
    public AssertionError(double detailMessage) {...}
    ...
}

это позволяет в качестве Expression2 использовать выражение любого примитивного или ссылочного типа:
public class App {
    public static void main(String[] args) {
        assert args != null : 1;
        assert args != null : 1.0;
        assert args != null : false;
        assert args != null : "Hello!";
        assert args != null : new int[] {10, 20, 30};
    }

или вызовы произвольных методов, которые что-то возвращают
public class App {
    public static void main(String[] args) {
        assert args != null : f0(); 
        assert args != null : f1(); 
        assert args != null : f2(); 
        assert args != null : f3(); 
        assert args != null : f4(); 
    }
    
    private static byte f0() {return 0;}
    private static double f1() {return 0.0;}
    private static boolean f2() {return true;}
    private static String f3() {return "Hello!";}
    private static int[] f4() {return new int[] {10, 20, 30};}
}

но не вызов метода, возвращающего void
public class App {
    public static void main(String[] args) {
        assert args != null : f(); // ошибка компиляции тут
    }   
    private static void f() {}
}

>> COMPILATION ERROR: 'void' type is not allowed here



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

Как и все необработанные исключения, сбои утверждения, как правило, содержат трассировку стека (stack trace) с номером файла и строки, из которой они были брошены. Вторая форма утверждения должна использоваться в предпочтении к первой только тогда, когда программа имеет некую дополнительную информацию, которая может помочь диагностировать сбой. Например, если Expression1 предполагает отношения между двумя переменными х и у, то должна быть использована вторая форма. В этих условиях, разумной переменной для Expression2 будет «х: » + х + ", у: " + у.
Комментарий переводчика
Имеется в виду, например, следующий пример
class App {
    public static void f(){
        int x, y;
        // некоторые вычисления
        assert x > y;
    }
}

просто выбросит AsertionError в случае нарушения утверждения (x > y).
Но такой пример
class App {
    public static void f(){
        int x, y;
        // некоторые вычисления
        assert x > y : "x: " + х + ", у: " + у;
    }
}

выбросит AsertionError с сообщением, допустим, «x: 0, y: 123» что позволит программисту проанализировать конкретные недопустимые значения x и y на момент вычисления.


В некоторых случаях Expression1 может быть дорогостоящим для вычисления. Например, предположим, что при написании метода ищущего минимальный элемент в неотсортированном списке добавляется утверждение проверяющее, что выбранный элемент действительно является минимальным. В данном случае проверка утверждение будет такой же «дорогой», как и выполнение самого метода. Для того чтобы убедиться, что утверждения не ухудшают производительность приложения, утверждения могут быть включены или отключены в момент запуска программы. По умолчанию они отключены. Отключение утверждения полностью исключает потерю в производительности. Как только они отключены, они эквивалентны пустому оператору в семантике и производительности. Для получения дополнительной информации смотрите: Включение и отключение утверждений.
Комментарий переводчика
В методе ArrayUtils.min(int[]) две проверки постинварианта и обе по сложности соизмеримы со сложность поиска минимального элемента (O(N))
public class ArrayUtils {
    public static int min(int[] array) {
        int min;
        // вычисление в результате которых 
        // в min - минимальное значение из array
        
        assert checkMin(array, min);
        assert checkContains(array, min);
        return min;
    }

    // проверим, что в массиве нет еще меньшего элемента
    private static boolean checkMin(int[] array, int min) {
        for (int elem : array) {
            if (elem < min) {
                return false;
            }
        }
        return true;        
    }

    // проверим, что в массиве присутствует "найденный"
    private static boolean checkContains(int[] array, int min) {
        for (int elem : array) {
            if (elem == min) {
                return true;
            }
        }
        return false;
    }    
}


Комментарий переводчика
под пустым оператором, видимо, подразумевается оператор «точка с запятой» (;)
public class App {
    public static void main(String[] args) {
           ;;;;
         ;;;;;;;;
        ;;; ;; ;;;
        ;;;;;;;;;;
        ;;      ;;
         ;;;;;;;;
           ;;;;
        for (int k = 0; k < 10; k++) {
            ;
        }
    }
}



Добавление ключевого слова assert в Java имеет последствия для существующего кода. Для получения дополнительной информации смотрите раздел Совместимость с существующими программами.


Внедрение утверждений в код


Существует множество ситуаций, когда полезно использовать утверждения, в том числе:


Также существуют ситуации, когда не следует их использовать:
  • Не используйте утверждения для проверки аргументов публичных (public) методов.

    Как правило, проверка аргумента – это часть опубликованной спецификаций (или контракта) метода, и это поведение должно соблюдаться в независимости включены или выключены утверждения. Другой проблемой использования утверждений для проверки аргументов является то, что ошибочные аргументы должны привести к соответствующем исключениям при выполнении (таким как IllegalArgumentException, IndexOutOfBoundsException или NullPointerException). Сбой утверждения не выдаст соответствующего исключения.
  • Не используйте утверждения для выполнения задачи, результат которой ожидается вашим приложением для корректной работы.

    Утверждения могут быть отключены и программы не должны исходить из того, что содержащееся в утверждении логическое выражение будет вычисляться (evaluate). Нарушение этого правила может иметь тяжелые последствия. Предположим, необходимо удалить все нулевые элементами из списка имен, зная, что список содержит как минимум один или несколько таких элементов. Было бы неправильно сделать так:
    // Не правильно! - Действие содержится в утверждении
    assert names.remove(null);
    

    Программа будет работать нормально при условии, что утверждения были включены, но произойдет сбой, если они были отключены. Т.к. в таком случае не будут удалять нулевые элементы из списка. Правильным подходом будет выполнение действия до проверки утверждения, а затем проверка утверждения, что действие выполнено:
    // Правильно! - действие выполняется перед проверкой утверждения
    boolean nullsRemoved = names.remove(null); // Работает в независимости включены утверждения  или нет.
    assert nullsRemoved; 
    

    Как правило, выражения, содержащиеся в утверждениях должны быть свободны от побочных эффектов (side effects): вычисление выражения (expression evaluations) не должно влиять на какое-либо состояние, которое используется после завершения вычисления. Единственное исключение из этого правила: утверждения могут модифицировать состояние, которое используется исключительно внутри других утверждений. Использование данной идиомы продемонстрировано ниже в тексте.

Комментарий переводчика
Побочный эффект (side effects) — всякое действие, меняющее «видимый мир».
Подразумевается, что просто выполнение
tmp x = 1 + 2 + 3;
Не меняет «видимый мир» (если никто ниже в программе не использует идентификатор tmp), хотя «невидимый программисту мир» может и поменялся (байткоды загружены, память в стеке под локальную переменную выделена, процессор просуммировал 1, 2 и 3)

Выполнение
System.out.println(«Hello!»);
меняет «видимый мир» (обладает побочными эффектами) — программа с такой строчкой очевидно печатает в консоль «больше», но все же это не влияет на программу ниже — мы не меняем читаемые данные.

Но строчка
tmp = 2 * tmp;
влияет на программу ниже (обладает побочными эффектами), если ниже мы читаем и используем значение переменной tmp.



Внутренние инварианты (internal invariants)

Прежде чем утверждения стали доступны в языке, многие программисты использовали комментарии для «высказывания» своих предположений относительно поведения программы. Например, чтобы раскрыть свои предположения об else-секции в длинной цепочке if-elseif-elseif-…. Ранее писали что-то вроде этого:
if (i % 3 == 0) {
    ...
} else if (i % 3 == 1) {
...
} else{ // мы знаем что (i % 3 == 2)
    ...
}

Теперь стоит использовать утверждение вместо каждого комментария с «очевидно истинным утверждением». Например, стоит переписать предыдущий код так:
if (i % 3 == 0) {
    ...
} else if (i % 3 == 1) {
    ...
} else {
    assert i % 3 == 2 : i;
...
}

Стоит отметить, что утверждение в приведенном выше примере может дать сбой, если 'i' отрицательно, так как оператор % вычисляет остаток от деления (remainder), который может быть отрицательным.
Комментарий переводчика
пример

Еще одним кандидатом на использования утверждения является switch-оператор default-секции. Отсутствие default-секции обычно указывает на уверенность программиста в том, что одна из case-секций всегда будет выбрана. Предположение, что переменная будет иметь одно из небольшого числа значений, является инвариантом и должно быть проверено с помощью утверждения. Допустим, следующий switch-оператор появляется в программе, которая имеет дело с карточной игрой:
switch(suit) {
    caseSuit.CLUBS:         // бубна
        ...
        break;
    caseSuit.DIAMONDS:  // трефа
        ...
        break;
    caseSuit.HEARTS:      // черва
        ...
        break;
    caseSuit.SPADES:      // пика
        ...
}

Вероятнее всего это указывает на предположение, что переменная типа масть будет иметь одно из четырех значений. Для тестирования этого предположения, следует добавить следующую default-секцию:
default:
    assert false : suit;

Если набор переменных принимает другое значение и утверждения включены, утверждение даст сбой и выбросит исключение AssertionError.
Приемлемым вариантом будет:
default:
    throw new AssertionError(suit); 

Данная идиома предлагает защиту, даже в случае отключенных утверждений. И эта дополнительная защита ничего не стоит: оператор throw выполняться только в том случае, если программа дала сбой. Кроме того этот вариант допустим при определенных обстоятельствах, когда не допустим assert. Скажем если метод метод возвращает значение, каждый case в операторе switch содержит return и нет возвращаемого результата после switch, то первый вариант приведет к синтаксической ошибке.
Комментарий переводчика
Вот эта программа компилируется
public class App {
    public static int twice(int arg) {
        switch (arg) {
            case 0:
                return 0;
            case 1:
                return 2;
            case 2:
                return 4;
            case 3:
                return 6;
            default:
                throw new AssertionError(arg);
        }       
    }    
}

а вот эта — нет
class App {
    public static int twice(int arg) {
        switch (arg) {
            case 0:
                return 0;
            case 1:
                return 2;
            case 2:
                return 4;
            case 3:
                return 6;
            default:
                assert false : arg;
        }
    }    
}

COMPILATION ERROR: Missing return statement




Инварианты потока выполнения (control-flow invariants)


В предыдущем примере тестируется не только инвариант, он также проверяет предположение о потоке выполнения. Автором исходного оператора switch, вероятно, предполагается не только что переменная suit всегда будет иметь одно из четырех значений, но также и то, что мы «зайдем» в одну из case-секций. Это указывает на другую область, где вы должны использовать утверждения: располагайте утверждение в любом месте, которое предполагается НИКОГДА не будет достигнуто. Необходимо использовать следующее утверждение:
assert false;

Предположим, вы имеете следующий метод:
void foo() {
    for (...) {
        if (...) {return;}
        ...
    }
    // выполнение никогда не достигнет этой точки!!!
}

Необходимо заменить окончательный комментарий так, чтобы код выглядел следующим образом:
void foo() {
    for (...) {
        if (...) {return;}
    }
    assert false; // выполнение никогда не достигнет этой точки!!!
}


Примечание: Используйте этот метод с осторожностью. Если состояние будет определено как недоступное согласно спецификации Java, то вы получите ошибку времени компиляции, если попытаетесь расположить там утверждение. Опять же, приемлемой альтернативой является просто бросить AssertionError.
Комментарий переводчика
Согласно спецификации Java компилятору предписывается проверять операторы на «недоступность». В случае, если если обнаружены «недоступные» операторы — происходит ошибка компиляции. Предпосылка: программа с операторами, которые никогда не выполняются — скорее всего результат ошибки программиста
public class App {
    public static void main(String[] args) {
        return;
        return; // тут ошибка компиляции
    }    
}

>> COMPILATION ERROR: Unreachable statement

Правила проверки жестко прописаны и не включают всех возможных ситуаций. Скажем вот так легко «обмануть» компилятор:
public class App {
    public static void main(String[] args) {
        if (true) {return;}
        return; // все равно недостижим, но компилятор пропускает
    }    
}




Предусловия (preconditions), постусловия (postconditions) инварианты класса (class invariants)


В то время как конструкция assert не предоставляет полноценной возможности для Программирования-по-Контракту (design-by-contract), она может помочь поддержать неформальный design-by-contract стиль программирования. В этом разделе показано, как использовать утверждения для таких ситуаций как



Предусловия (preconditions)

В соответствии с соглашениеми, предусловия в открытых (public) методах производят проверки, которые бросают конкретные определенные исключения. Например:
/**
  * Установить частоту обновления (refresh rate).
  *
  * @param rate частота обновления, выражается в кадрах в секунду.
  * @throws IllegalArgumentException если (rate <= 0) или (rate > MAX_REFRESH_RATE).
*/
public void setRefreshRate(int rate) {
    // Проверяем определенное предусловие в открытом методе
    if (rate <= 0 || rate > MAX_REFRESH_RATE) 
        throw new IllegalArgumentException("Illegal rate: " + rate);
    setRefreshInterval(1000 / rate);
}

Данное соглашение не изменилось при добавлении оператора assert. Не используйте утверждения чтобы проверить параметры открытого (public) метода. Использование утверждений в этом случае неуместно, так как метод гарантирует, что он всегда будет проверять аргумент. Он должен проверить аргументы, в независимости включены ли утверждения. Кроме того, конструкции assert нельзя указать выбрасывать исключения указанного типа. Она может генерировать лишь AssertionError.

Однако можно использовать утверждения для тестирования предусловий непубличного (nonpublic) метода, которые, как высчитаете, будут верны в независимости от того, что клиент делает с классом. Например, утверждение подходит в следующем вспомогательном методе, который вызывается предыдущим публичным методом:
/**
  * Установить интервал обновления (соответствующий корректной частоте смены кадров)
  *
  * @param interval интервал обновления в миллисекундах.
*/
private void setRefreshInterval(int interval) {
    // проверка предусловия в непубличном (nonpublic) методе
    assert (interval > 0) && (interval <= 1000 / MAX_REFRESH_RATE) : interval;
    ... // установить интервал обновления
} 

Примечание: указанное утверждение даст сбой, если MAX_REFRESH_RATE больше 1000 или клиент выбирает частоту обновления более чем 1000. Это, по сути, указывает на ошибку в библиотеке!


Предусловия статуса блокировки (lock-status preconditions)


Классы, предназначенные для многопоточного использования, часто имеют непубличные (non-public) методы с предусловиями, заключающимися в том, что некоторые блокировки захвачены или наоборот свободны. Далеко не редкость увидеть что-то вроде этого:
privateObject[] a;
public synchronized int find(Object key) {
    return find(key, a, 0, a.length);
}
/ / Рекурсивный вспомогательный метод - всегда вызывается c захваченной блокировкой на данном объекте
private int find(Object key, Object[] arr, int start, int len) {
...
} 

Статический метод, который называется holdsLock(Object), был добавлен классу Thread для тестирования того, владеет ли блокировкой на указанный объект текущий поток. Этот метод может быть использован в сочетании с утверждением для дополнения комментария, как показано в следующем примере:
/ / Рекурсивный вспомогательный метод всегда вызывается с блокировкой на следующем:
private int find(Object key, Object[] arr, int start, intlen) {
    assert Thread.holdsLock(this); // lock-status assertion 
    ...
} 

Обратите внимание, также возможно написать утверждение, заключающееся в том, что некоторая блокировка не захвачена!
Комментарий переводчика
Аналогичные проверки возможны для многих классов из java.util.concurrent:
ReentrantLock.isHeldByCurrentThread() — точный аналог
ReentrantReadWriteLock.isWriteLockedByCurrentThread() — точный аналог
Semaphore.availablePermits() — может использоваться для похожего анализа




Постусловия (postconditions)


Вы можете проверить постусловие с помощью утверждений как в открытых (public) так и в закрытых (nonpublic) методах. Например, следующий публичный метод использует утверждение для проверки постусловия:
Возвращает BigInteger значение которого (this-1 mod m).
 /**
  * @param m делитель
  * @return this^(-1) mod m.
  * @throws ArithmeticException  если m <= 0 или этот BigInteger
  * не имеет обратного элемента относительно деления 
  * mod m (этот BigInteger не прост относительно m).
  */
public BigInteger modInverse(BigInteger m) {
    if (m.signum <= 0) {
        throw new ArithmeticException("Modulus not positive: " + m);
    }
    ... // производим вычисления
    assert this.multiply(result).mod(m).equals(ONE) : this;
    return result;
}

Комментарий переводчика
Немножко алгебры: в кольце вычетов по основанию N (ZN) всякий ненулевой элемент x имеет (и при чем единственный) обратный элемент, если x взаимно просто с N. Если же x не взаимно прост с N, то x не имеет обратного элемента.

Пример #1 (обратные в кольце Z5):
для 1 — обратный = 1, 1 * 1 mod 5 = 1
для 2 — обратный = 3, 2 * 3 mod 5 = 1
для 3 — обратный = 2, 3 * 2 mod 5 = 1
для 4 — обратный = 4, 4 * 4 mod 5 = 1

Пример #2 (обратные в кольце Z6):
для 1 — обратный = 1, 1 * 1 mod 6 = 1
для 2 — обратных нет
для 3 — обратных нет
для 4 — обратных нет
для 5 — обратный = 5, 5 * 5 mod 6 = 1

232.x
Иногда необходимо сохранить некоторые данные до выполнения вычислений связанных с проверкой постусловия. Вы можете сделать это с помощью двух утверждений и простого внутреннего (inner) класса, который сохраняет состояние одной или нескольких переменных, чтобы они могли быть проверены (или перепроверены) после вычислений. Например, предположим, у вас есть часть кода, который выглядит следующим образом:
 void foo(int[] array) {
      // проводим манипуляции над массивом
      ...

      // в этой точке массив содержит в точности те же int-ы
      // что были до манипуляции, в том же порядке
 }


Вот как можно изменить указанный выше метод для того, чтобы превратить текстовое утверждение постусловия в «функциональное»:
void foo(final int[] array) {
    // внутренний (inner) класс который сохраняет состояние и выполняет проверку согласованности
    class DataCopy {
        private int[] arrayCopy;
        DataCopy() { arrayCopy = (int[]) array.clone();}
        boolean isConsistent() {return Arrays.equals(array, arrayCopy);}
    }
    DataCopy copy = null;
    // всегда успешно; имеет побочный эффект в виде создания копии массива
    assert ((copy = new DataCopy()) != null);
    ... // манипуляции над массивом array

    // удостоверимся, что содержимое array не изменилось
    assert copy.isConsistent();
} 

Вы можете легко обобщить этот подход для сохранения более одного элемента данных и для тестирования утверждений произвольной сложности.

Можно было бы попытаться заменить первое утверждение (которое выполняется исключительно для побочного эффекта) следующим, более выразительным:
copy = new DataCopy(); 

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


Инварианты класса (class invariants)


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

Механизм утверждение не навязывает никакой конкретный стиль для проверки инвариантов. Однако иногда удобно объединить выражения, проверяющие необходимые ограничения, в единый внутренний метод, который может вызвать утверждение. В продолжение примера сбалансированного дерева, было бы целесообразно реализовать private-метод, который бы проверял, что дерево действительно сбалансировано в соответствии с требованиями структуры данных:
// Возвращает true если это дерево сбалансировано
private boolean balanced() {
    ...
}

Так как этот метод проверяет ограничение, которое должно быть истинно до и после выполнения любого метода, то каждый публичных метод и конструктор должны содержать следующую строку непосредственно перед его возвращением:
assert balanced(); 

Как правило, нет необходимости размещать аналогичные проверки в заголовке каждого открытого метода, только если структура данных не реализуется через native-методы. В этом случае не исключено, что повреждения памяти (memory corruption bug) может повредить структуру данных вне рамок «кучи» Java («native peer») между вызовами методов. Сбой утверждения в начале метода будет означать, что произошло повреждения памяти такого рода.
Комментарий переводчика
Имеется в виду, что класс реализован через Java native Interface (JNI)


Так же может быть целесообразным включить проверку инвариантов класса в начале каждого метода класса, если состояние класса может изменяться другими классами. Все же лучше использовать, такой дизайн классов, что состояние не является непосредственно видимым для других классов!
Комментарий переводчика
Согласно «нормам инкапсуляции» внутреннее состояние (поля экземпляра) меняются «клиентом экземпляра» исключительно через вызовы публичных методов. В таком случае достаточно проверить инварианты класса по выходу из всех публичных методов. Ведь при «входе» в любой публичный метод мы имеем состояние, которое нам оставил «выход» из какого-то публичного метода, а там проверку уже сделали.



«Продвинутые» техники


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

Устранение утверждений из class-файлов

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

Механизм утверждений не предлагает прямую поддержку их удаления из class-файлов. Утверждения, однако, могут быть использованы в сочетании с идиомой «условной компиляции» («conditional compilation»). Этот подход описан в спецификации Java и позволяет компилятору устранить все «упоминания» об утверждениях в генерируемых class-файлах:
 static final boolean asserts = ... ; // false для удаления утверждений

 if (asserts)
    assert <expr> ;

Комментарий переводчика
«Условная компиляция» упоминается в спецификации в нескольких местах. Думаю авторы имеют в виду следующую цитату отсюда:
«An optimizing compiler may realize that the statement… will never be executed and may choose to omit the code for that statement from the generated class file ...»

И если (а я так полагаю, что так и есть) javac является тем самым «optimizing compiler», то он преобразует вот такую программу
public class App {
    static final boolean asserts = false; 
    public static void main(String[] args) {
        // код А
        if (asserts) {
            // код B
        }
        // код C
    }
}

вот в такую
public class App {
    static final boolean asserts = false; 
    public static void main(String[] args) {
        // код А
        // код C
    }
}



Как требовать включения утверждений

Программисты некоторых критически важных систем, возможно, пожелают, чтобы их код работал исключительно с включенными утверждениями. Следующий подход на основе статической инициализации предотвращается инициализацию класса, если его утверждения были отключены:
static {
    boolean assertsEnabled = false;
    assert assertsEnabled = true; // умышленный побочный эффект!!!
    if (!assertsEnabled)
        throw new RuntimeException("Утверждения должны быть включены!!!");
 } 

Поместите этот статический инициализатор в верхнюю часть объявления вашего класса.
Комментарий переводчика
Предлагают поместить «в верх» объявления класса, так как согласно спецификации порядок инициализации статических полей и вызовов статических инициализаторов — сверху вниз. Т.е. в слуе запуска следующей программы
class App {
    static int x = f();
    static {
        System.err.println("static{}");
    }
    static int y = g();
    static {
        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (!assertsEnabled)
            throw new RuntimeException();
    }

    public static void main(String[] args) {}
    
    static int f() {
        System.err.println("f()");
        return 0;
    }
    static int g() {
        System.err.println("g()");
        return 0;
    }    
}

>> f()
>> static{}
>> g()
>> Exception in thread "main" java.lang.ExceptionInInitializerError
>>     ...

ДО запуска проверки на включенность утверждений будет уже запуск двух методов и одного статического инициализатора.



Компиляция файлов, использующие утверждения


Для того, чтобы компилятор javac принял код, содержащий утверждения, необходимо использовать параметр командной строки -source 1.4, как в следующем примере:
javac -source 1.4 MyClass.java

Этот флаг необходим для того, чтобы не вызвать проблему совместимости на уровне исходного кода.
Комментарий переводчика
javac может получать при компиляции пару флагов (-source и -target)
javac -source 1.3 -target 1.4
указывающих на версию исходного кода и версию генерируемых class-файлов. Оба этих значения должны быть не выше, чем версия компилятора. Т.е. можно приказать компилятору, который уже понимает Java 5 брать исходники и анализировать их с позиции Java 1.3, но байт-код генерировать для версии Java 1.4.
Версия исходников 1.3 указывает, скажем, не воспринимать assert как ключевое слово, отказываться компилировать generics, varargs,…
Версия class-файлов 1.4 указывает, скажем, как минимум указать в class-файле версию класса соответствующую Java 1.4. Формат class-файла требует указывать пару версий (minor_version, major_version)
ClassFile {
    ...
    u2             minor_version;
    u2             major_version;
    ...
}

Oracle's Java Virtual Machine implementation in JDK release 1.0.2 supports class file format versions 45.0 through 45.3 inclusive. JDK releases 1.1.* support class file format versions in the range 45.0 through 45.65535 inclusive. For k ≥ 2, JDK release 1.k supports class file format versions in the range 45.0 through 44+k.0 inclusive.




Включение и отключение утверждений


По умолчанию утверждения отключены во время выполнения. У нас есть два «переключателя» командной строки.
Чтобы включить утверждения на различных уровнях детализации, используйте -enableassertions или -ea. Чтобы отключить утверждения на различных уровнях детализации, используйте -disableassertions или -da. Вы указываете детализацию аргументами флага:

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

  • Включает или выключает утверждения в безымянном пакете в текущем рабочем каталоге
  • className
    Включает или выключает утверждения в указанном классе


Например, следующая команда запускает программу BatTutor с включенными утверждениями только для пакета com.wombat.fruitbat и его подпакетов:
java -ea:com.wombat.fruitbat... BatTutor


Если в командной строке содержится несколько таких «переключателей», то они обрабатываются по порядку перед загрузкой любого класса. Например, следующая команда запускает программу BatTutor с включенными утверждениями пакета com.wombat.fruitbat, но с отключенными для класса com.wombat.fruitbat.Brickbat:
java -ea:com.wombat.fruitbat... -da:com.wombat.fruitbat.Brickbat BatTutor


Вышеуказанные флаги командной строки применяются ко всем загрузчикам классов за одним исключением. Речь идет о системном ClassLoader. Исключение касается переключателей без аргументов, которые (как указано выше) не применяются к системным классам. Данное поведения позволяет легко включить утверждение во всех классах за исключением системных классов, что обычно желательно.

Чтобы включить утверждения во всех системных классах, используйте другой флаг: -enablesystemassertions или -esa. Точно так же, чтобы отключить утверждения в системных классах используйте -disablesystemassertions или -dsa.

Например, следующая команда запускает программу BatTutor с утверждениями, включенными как в системных классах, так и в пакете com.wombat.fruitbat и его подпакетах:
java -esa -ea:com.wombat.fruitbat... 


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

Если утверждение выполняется до инициализации класса, то выполнение должно вести себя так, как будто утверждения были включены в классе. Эта тема подробно обсуждается в спецификации Java.
Комментарий переводчика
Вот это место в спецификации:
An assert statement that is executed before its class has completed initialization is enabled.

This rule is motivated by a case that demands special treatment. Recall that the assertion status of a class is set no later than the time it is initialized. It is possible, though generally not desirable, to execute methods or constructors prior to initialization. This can happen when a class hierarchy contains a circularity in its static initialization, as in the following example:
public class Foo {
    public static void main(String[] args) {
        Baz.testAsserts(); 
        // Will execute after Baz is initialized.
    }
}
class Bar {
    static {
        Baz.testAsserts(); 
        // Will execute before Baz is initialized!
    }
}
class Baz extends Bar {
    static void testAsserts() {
        boolean enabled = false;
        assert  enabled = true;
        System.out.println("Asserts " + 
			   (enabled ? "enabled" : "disabled"));
    }
}

>> Asserts enabled
>> Asserts disabled

Invoking Baz.testAsserts() causes Baz to be initialized. Before this can happen, Bar must be initialized. Bar's static initializer again invokes Baz.testAsserts(). Because initialization of Baz is already in progress by the current thread, the second invocation executes immediately, though Baz is not initialized (§12.4.2).

Because of the rule above, if the program above is executed without enabling assertions.



Совместимость с существующими программами


Добавление ключевого слова assert в Java не вызывает никаких проблем с уже существующими двоичными файлами (.class файлы). Если вы попытаетесь скомпилировать приложение, которое использует assert в качестве идентификатора, то вы получите предупреждение компилятора или сообщение об ошибке. Для того чтобы облегчить переход от мира, где утверждение является допустимым идентификатором к миру, где это не так, компилятор поддерживает два режима работы в этой версии:
  • Режим исходников 1.3 (по умолчанию) – компилятор принимает программы, использующие assert в качестве идентификатора, но выдает предупреждения. В этом режиме программам не разрешается использовать утверждение.
  • Режим исходников 1.4 – компилятор генерирует сообщение об ошибке, если программа использует assert в качестве идентификатора. В данном режиме программам разрешается использовать утверждение.

Пока вы специально не запросите режима исходников 1.4 с флагом -source 1.4, компилятор работает в режиме исходников 1.3. Если вы забыли использовать этот флаг, программы, использующие утверждение не будут компилироваться. Решение заставить компилятор использовать старую семантику как поведение по умолчанию (то есть, позволяя assert использовать в качестве идентификатора) было сделано для максимально благоприятного режима совместимости. Режим исходников 1.3, скорее всего, будет работать в течение долгого времени.
Комментарий переводчика
Написанное было справедливо на момент написание статьи (времена Java 1.4), но вряд ли справедливо сейчас. Т.е. вряд ли в javac из JDK 7 или JDK 8 по умолчанию считается, что -source 1.3



Дизайн и часто задаваемые вопросы


В этом разделе собраны часто задаваемые вопросы, касающиеся внутреннего устройства assert-ов.
Общие вопросы
Совместимость
Синтаксис и семантика
Класс AssertionError
Включение и отключение утверждений


Общие вопросы

Зачем обеспечивать возможность утверждения, учитывая, что можно запрограммировать утверждения в поверх языка программирования Java, без специальной поддержки?
Хотя специальные реализации возможны, они зачастую либо некрасивы (требуя if для каждого утверждения), либо неэффективны (вычисление условия, даже если утверждения отключены). Кроме того, каждая специальная реализация по своему реализует включения и отключения утверждения, что уменьшает полезность этих реализаций, особенно в режиме отладки. В результате этих недостатков, утверждения никогда не были частью культуры программистов на Java. Добавление поддержки утверждений в платформу имеет хорошие шансы исправить эту ситуацию.

Почему введение этого средства осуществлено через расширение языка, а не в виде библиотеки?
Мы осознаем, что изменение языка является серьезным шагом и к этому не следует относиться легкомысленно. Подход через библиотеку рассматривался. Однако, считается важным, что бы стоимостью выполнения утверждений можно было пренебречь, если они выключены. Для того чтобы добиться этого в случае с библиотекой, программист вынужден жестко кодировать каждое утверждение как if-оператор. Многие программисты не делали бы этого. Они либо будут опускать if-оператор и будет страдать производительность, либо же они будут полностью игнорировать утверждения. Отметим также, что утверждения содержатся в оригинальной спецификации Java от Джеймса Гослинга. Утверждения были удалены из спецификации Oak из-за нехватки времени на удовлетворительную разработку и реализацию.
Комментарий переводчика
Первоначальная версия языка Java называлась Oak в честь дуба под окном у Дж. Гослинга:).


Почему бы не обеспечить полную поддержку Разработки-по-Контракту (design-by-contract) с предусловиями, постусловиями и инвариантами классов, как в языке программирования Eiffel?
Мы рассматривали такую возможность, но не смогли убедить себя, что можно «привить» это к Java без массовых изменений в библиотеках платформы и массового нарушения обратной совместимости между старыми и новыми библиотеками. Кроме того, мы не были уверены, что такая возможность сможет сохранить простоту языка, что является отличительной чертой Java. В итоге мы пришли к выводу, что простое условное утверждение (boolean assertion facility) будет довольно понятным решением и гораздо менее рискованным. Стоит отметить, что добавление условного утверждения в язык не исключает добавления полноценной поддержки Разработки-по-Контракту (design-by-contract) когда-нибудь в будущем.

Простое утверждение действительно позволяет использовать ограниченную форму Разработки-по-Контракту. Утверждение (assert) подходит для проверки внутренних (nonpublic) предусловий, постусловий и инвариантов классов. Внешняя (public) проверка предусловий все также может быть выполнена проверками внутри методов, которые приводят, в частности, к документированным исключениям, таким как IllegalArgumentException и IllegalStateException.

В дополнение к условным утверждениям (boolean assertions), почему бы не предоставить assert-подобную конструкцию для отключения выполнения блока кода, если утверждения отключены?
Предоставление такой конструкции будет стимулировать программистов делать сложные встроенные (inline) утверждения, хотя их лучше выносить в отдельные методы.


Совместимость

Не станет ли новое ключевое слово проблемой совместимости с существующими программами, которые используют 'assert' в качестве идентификатора?
Да, на уровне исходных файлов (двоичные файлы классов, использующих 'assert' в качестве идентификатора будут продолжать работать корректно.) Чтобы облегчить переход, мы реализовали стратегию, согласно которой разработчики могут продолжать использовать 'assert' в качестве идентификатора в течение переходного периода.

Разве данное средство не приводит к появлению class-файлов, которые не могут выполняться на старых JRE?
Да, приводит. class-файлы будут содержать вызовы новых методов классов ClassLoader и Class таких как desiredAssertionStatus(). Если class-файл, содержащий вызовы этих методов, исполняется исполняется старым JRE (чей класс ClassLoader не имеет таких методы), то программа не будет выполнена и «выбросит» NoSuchMethodError. Это именно тот случай, когда программы, использующие новые возможности языка, не совместимы с более старыми версиями.


Синтаксис и семантика

Почему позволительны примитивные типы в Expression2?
Нет веских причин для ограничения типа этого выражения. Разрешение произвольных типов обеспечивает удобство для разработчиков, которые, например, хотят связать уникальный целочисленный код с каждым утверждением. Кроме того, это заставляет выражение «чувствовать себя» как аргумент System.out.println(...), что выглядит желательным.


Класс AssertionError

Когда AssertionError сгенерирован утверждением, в котором отсутствует Expression2, почему программа не сообщает о состоянии утверждения в детализированном текстовом сообщении, например «height < maxHeight»?
Хотя в некоторых случаях «готовая к использованию полезность» (out-of-the-box usefulness) увеличится, но это не оправдывает стоимость добавления всех этих строковых констант в class-файлы и представления классов времени выполнения (runtime images).

Почему AssertionError не предоставляет доступ к объекту, который бросил это исключение? Аналогично, почему бы не передать произвольный объект из утверждения в конструктор AssertionError вместо сообщения об ошибке (строкового)?
Доступ к этим объектам будет стимулировать программистов попытаться восстановить работу системы после сбоя в утверждении, что противоречит назначению данной возможности языка.

Почему бы не предоставить контекстные методы доступа (такие как getFile(), getLine(), getMethod()) для AssertionError?
Это средство уже обеспечивается для Throwable. Поэтому оно может быть использовано для всех исключительных ситуация (throwables), а не только для ошибок утверждений (assertion errors). Мы имеем такой метод у Throwable.getStackTrace() чтобы обеспечить эту функциональность.
Комментарий переводчика
Поддержка утверждений (ключевой слово assert и AssertionError) появилась в той же версии языка, что и метод Throwable.getStackTrace() — в версии 1.4.
Поэтому авторы языка добавили поддержку возможности найти файл+метод+строку-кода выбросившие произвольного наследника Throwable вместо возможности только для AssertionError.

Комментарий переводчика
Рассмотрите следующий пример
public class App {
    public static void main(String[] args) {
        try {
            f();
        } catch (Throwable t) {
            StackTraceElement[] stack = t.getStackTrace();
            StackTraceElement frame = stack[0];
            System.out.println("FileName:   " + frame.getFileName());
            System.out.println("ClassName:  " + frame.getClassName());
            System.out.println("MethodName: " + frame.getMethodName());
            System.out.println("LineNumber: " + frame.getLineNumber());
        }
    }

    public static void f() {
        throw new RuntimeException();
    }
}

>> FileName:   App.java
>> ClassName:  App
>> MethodName: f
>> LineNumber: 16



Почему AssertionError подкласс Error, а не RuntimeException?
Это спорный вопрос. Группа экспертов обсуждала его и пришла к выводу, что Error — более подходящий предок, чтобы избавить программистов от попыток восстановить работоспособность программы после AssertionError. В общем довольно трудно или даже невозможно локализовать источник assert-ошибки. Такой отказ означает, что программа «движется в неизвестном направлении» («outside of known space») и попытки продолжить выполнение, вероятно, будут губительны. Кроме того, соглашения о кодировании (convention) требуют, чтобы методы указывали большинство исключений времени исполнения (имеются в виду наследники Exception, но не Error), которые они могут выбросить ( @throws в javadoc). Вряд ли имеет смысл включать в спецификацию метода обстоятельства при которых он может генерировать assert-ошибки. Такая информация может рассматриваться как деталь реализации, которая может изменяться от реализации к реализации и от релиза к релизу.
Комментарий переводчика
Больше вы можете прочитать в следующей статье «How to Write Doc Comments for the Javadoc Tool» в разделе «Documenting Exceptions with @throws Tag».



Включение и отключение утверждений

Почему команды, которые включают и выключат утверждения используют семантику пакетов-как-деревьев (package-tree) вместо более традиционной семантики пакетов?
Контроль над иерархией может быть полезен, так как программисты действительно используют пакеты в качестве иерархических структур для организации своего кода. Например, семантика пакетов-как-деревьев позволяет включить или выключить утверждения во всем Swing на одно действие.
Комментарий переводчика
Хотя пакеты на первый взгляд образуют иерархию в Java, но это буквально ни на что не влияет (кроме схемы именования).
Пример: модификаторы области видимости.
Т.е. классы в пакете aaa.bbb.ccc настолько же «далеки» от классов в пакете aaa.bbb как и от классов в пакете xxx.yyy.zzz. С точки зрения модификаторов области видимости (public, protected, (package private), private) есть только два важных свойства: находится в том же самом пакете или быть наследником. Нет никаких особых привилегий у «дочерних пакетов».

Библиотека Swing расположилась по следующим пакетам
javax.swing.*
javax.swing.border.*
javax.swing.colorchooser.*
javax.swing.event.*
javax.swing.filechooser.*
javax.swing.plaf.*
javax.swing.plaf.basic.*
javax.swing.plaf.metal.*
javax.swing.plaf.multi.*
javax.swing.plaf.nimbus.*
javax.swing.plaf.synth.*
javax.swing.table.*
javax.swing.text.*
javax.swing.text.html.*
javax.swing.text.html.parser.*
javax.swing.text.rtf.*
javax.swing.tree.*
javax.swing.undo.*
javax.swing.*
javax.swing.*

И семантика пакетов-как-деревьев позволяет отключив утверждения для пакета javax.swing.* распространить отключение на все вложенные пакеты.


Почему setClassAssertionStatus() возвращает boolean вместо выброса исключения, если он вызывается слишком поздно, чтобы установить статус утверждения (то есть, если данный класс уже инициализирован)?
Никакое действие (кроме, возможно, предупреждающего сообщения) не является необходимым или желаемым в случае слишком позднего установления флага. Исключение кажется излишне тяжеловесным.

Почему бы не перегружать одно имя метода вместо использования двух: setDefaultAssertionStatus() и setAssertionStatus()?
Ясность в методах именования выполнена для общего блага. Перегрузка методов имеет тенденцию вызывать путаницу.

Почему бы не настроить семантику desiredAssertionStatus так, чтобы сделать его более «дружественным программисту» («programmer friendly»), возвращая текущее состояние вклученности утверждения, если класс уже инициализирован?
Пока не ясно будет ли какая-либо польза для результирующего метода. Этот метод не предназначен для использования прикладным программистом и кажется нецелесообразным делать его медленнее и сложнее, чем это необходимо.

Почему нет никакого RuntimePermission для предотвращения включения\выключения утверждений апплетами?
Хотя нет никаких оснований для вызова апплетом любого из методов ClassLoader для модификации статуса включенности/выключенности утверждений, разрешение им такого вызова кажется безобидным. В худшем случае, апплет может создать легкий вариант denial-of-service атаки включением утверждения в классах, которые еще необходимо инициализировать. Кроме того, апплеты могут влиять только на статус утверждений классов, которые должны будут загружены загрузчиками классов, к которым апплеты могут получить доступ. А тут уже существует RuntimePermission для предотвращения возможности получить доступ к загрузчику классов (getClassLoader) для ненадежного кода.
Комментарий переводчика
Кратко о механизме безопасности «песочницы» Java.
Мы можем установить java.lang.SecurityManager (по умолчанию у обычного приложения — не установлен, у апплетов — установлен) и ему через RuntimePermission указать что разрешено, а что — нет.
// эта программа "повисает"
public class App {
    public static void main(String[] args) {
        while (true);
    }
}

// а эта - нет
public class App {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                System.exit(123);
            }
        }).start();
        
        while (true);
    }
}

>> Process finished with exit code 123

// А эта хотела бы вызвать System.exit(123);
// да мы ей права не дали и она ... "висит"!
public class App {
    public static void main(String[] args) {
        System.setSecurityManager(new SecurityManager() {
            public void checkExit(int status) {
                throw new SecurityException("You haven't permission to exit.");
            }
        });

        new Thread(new Runnable() {
            public void run() {System.exit(123);}
        }).start();
        
        while (true);
    }
}


В предыдущих примерах мы давали/запрещали разрешения программно, а можно это делать декларативно, во внешнем файле. И вот тогда можно запретить программе вызывать следующий код
public class App {
    public static void main(String[] args) {
        ClassLoader loader = App.class.getClassLoader();
        loader.setDefaultAssertionStatus(true);
        loader.setPackageAssertionStatus("", true);
    }
}

Точнее эта программа упадет по SecurityException если мы декларативно в не выдадим ей
grant {
  permission java.lang.RuntimePermission "getClassLoader";
};

в файле java.policy


Почему бы не предоставить конструкцию, которая будет запрашивать состояние утверждения «окружающего» класса?
Такая конструкция будет подталкивать людей строить сложный код утверждений. А мы мы рассматриваем это как плохое решение. Кроме того, если вам необходимо, используйте следующую конструкцию поверх текущего API:
boolean assertsEnabled = false;
assert assertsEnabled = true;  // Намеренный побочный эффект!!!
/ / Теперь assertsEnabled указывает установлен ли флаг утверждений


Почему assert, который выполняется до инициализации класса, ведет себя так, как будто утверждения были включены?
Очень мало программистов знают о том, что конструкторы и методы класса могут работать до его инициализации. Когда это происходит, то вполне вероятно, что инварианты класса еще не были созданы, что может привести к серьезным и тонким ошибкам. Любое утверждение, которое выполняется в этом состоянии, скорее всего, не сработает, предупреждая программиста о проблеме. Таким образом, это в целом полезно для программиста выполнять все утверждения, с которыми сталкивается код в такой нестандартной ситуации.

Контакты



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

skype: GolovachCourses
email: GolovachCourses@gmail.com
  • +20
  • 39,3k
  • 2
GolovachCourses
34,00
Компания
Поделиться публикацией

Похожие публикации

Комментарии 2

    0
    Может быть полезна библиотечка от Google для контрактного программирования, основанная на аннотациях: code.google.com/p/cofoja/ Работает через AnnotationProcessing или Java Agent.
      0
      Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое