Что делает ключевое слово Java assert и когда его следует использовать? - PullRequest
563 голосов
/ 03 мая 2010

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

Ответы [ 18 ]

405 голосов
/ 03 мая 2010

Утверждения (посредством ключевого слова assert ) были добавлены в Java 1.4. Они используются для проверки правильности инварианта в коде. Они никогда не должны запускаться в производственном коде и указывают на ошибку или неправильное использование пути кода. Они могут быть активированы во время выполнения с помощью опции -ea в команде java, но по умолчанию они не включены.

Пример:

public Foo acquireFoo(int id) {
  Foo result = null;
  if (id > 50) {
    result = fooService.read(id);
  } else {
    result = new Foo(id);
  }
  assert result != null;

  return result;
}
311 голосов
/ 17 октября 2013

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

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

Эта идея основана на парадигме Design-by-Contract (DbC): сначала вы определяете (с математической точностью), что должен делать ваш метод, а затем проверяете это, проверяя его во время фактического выполнение. Пример:

// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
  return a + b;
}

Хотя это довольно очевидно, что работает нормально, большинство программистов не увидят скрытую ошибку внутри этой (подсказка: Ariane V разбился из-за аналогичной ошибки). Теперь DbC определяет, что вы должны всегда проверять ввод и вывод функции, чтобы убедиться, что она работает правильно. Java может сделать это с помощью утверждений:

// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
    assert (Integer.MAX_VALUE - a >= b) : "Value of " + a + " + " + b + " is too large to add.";
  final int result = a + b;
    assert (result - a == b) : "Sum of " + a + " + " + b + " returned wrong sum " + result;
  return result;
}

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

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

Это все еще не гарантия для кода без ошибок, но гораздо ближе к этому, чем обычные программы.

60 голосов
/ 18 января 2014

Утверждения - это инструмент фазы разработки для выявления ошибок в вашем коде. Они предназначены для легкого удаления, поэтому они не будут существовать в рабочем коде. Таким образом, утверждения не являются частью «решения», которое вы предоставляете клиенту. Это внутренние проверки, чтобы убедиться, что ваши предположения верны. Наиболее распространенным примером является проверка на ноль. Многие методы написаны так:

void doSomething(Widget widget) {
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}

Очень часто в таком методе виджет никогда не должен быть нулевым. Так что, если он нулевой, в вашем коде есть ошибка, которую нужно отследить. Но код выше никогда не скажет вам этого. Поэтому, стараясь написать «безопасный» код, вы также скрываете ошибку. Гораздо лучше написать такой код:

/**
 * @param Widget widget Should never be null
 */
void doSomething(Widget widget) {
  assert widget != null;
  widget.someMethod(); // ...
    ... // do more stuff with this widget
}

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

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

void doSomething(Widget widget) {
  assert widget != null;
  if (widget != null) {
    widget.someMethod(); // ...
    ... // do more stuff with this widget
  }
}

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

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

/**
 * Compare two values using equals(), after checking for null.
 * @param thisValue (may be null)
 * @param otherValue (may be null)
 * @return True if they are both null or if equals() returns true
 */
public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
  } else {
    result = thisValue.equals(otherValue);
  }
  return result;
}

Этот код делегирует работу метода equals() в случае, когда thisValue не равно NULL. Но предполагается, что метод equals() правильно выполняет контракт equals(), правильно обрабатывая нулевой параметр.

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

public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
  } else {
    result = otherValue != null && thisValue.equals(otherValue); // questionable null check
  }
  return result;
}

Дополнительная проверка здесь, other != null, необходима только в том случае, если метод equals() не может проверить на ноль, как того требует его контракт.

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

public static boolean compare(final Object thisValue, final Object otherValue) {
  boolean result;
  if (thisValue == null) {
    result = otherValue == null;
    assert otherValue == null || otherValue.equals(null) == false;
  } else {
    result = otherValue != null && thisValue.equals(otherValue);
    assert thisValue.equals(null) == false;
  }
  return result;
}

Важно помнить следующее:

  1. Утверждения являются только инструментами этапа разработки.

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

  3. Даже если бы мой коллега был уверен, что наши занятия написаны правильно, утверждения здесь все равно будут полезны. Будут добавлены новые классы, которые могут не пройти проверку на нулевое значение, и этот метод может пометить нам эти ошибки.

  4. В процессе разработки вы всегда должны включать утверждения, даже если написанный вами код не использует утверждения. Моя IDE по умолчанию всегда делает это для любого нового исполняемого файла.

  5. Утверждения не меняют поведение кода в производственной среде, поэтому мой коллега рад, что проверка на ноль есть, и что этот метод будет выполняться правильно, даже если метод equals() содержит ошибки. Я счастлив, потому что я поймаю любой ошибочный метод equals() в разработке.

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

19 голосов
/ 03 июня 2016

Много хороших ответов, объясняющих, что делает ключевое слово assert, но немногие отвечают на реальный вопрос: «когда ключевое слово assert должно использоваться в реальной жизни?"

Ответ: почти никогда .

Утверждения как концепция прекрасны. Хороший код содержит множество if (...) throw ... операторов (и их родственников, таких как Objects.requireNonNull и Math.addExact). Однако некоторые дизайнерские решения значительно ограничивают использование самого ключевого слова assert .

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

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

Поэтому, использование if (...) throw ... должно быть предпочтительным, так же, как это требуется для проверки значений параметров открытых методов и для выброса IllegalArgumentException.

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

Не используйте assert просто потому, что он чище и красивее, чем if (...) throw ... (и я говорю это с большой болью, потому что мне нравится чистота и красота). Если вы просто не можете себе помочь и можете контролировать, как запускается ваше приложение, тогда можете свободно использовать assert, но всегда разрешать утверждения в работе. По общему признанию, это то, что я склонен делать. Я настаиваю на аннотации lombok, которая заставит assert вести себя как if (...) throw .... Проголосуйте за это здесь.

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

13 голосов
/ 17 января 2014

Вот наиболее распространенный вариант использования. Предположим, вы включаете значение enum:

switch (fruit) {
  case apple:
    // do something
    break;
  case pear:
    // do something
    break;
  case banana:
    // do something
    break;
}

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

switch (fruit) {
  case apple:
    // do something
    break;
  case pear:
    // do something
    break;
  case banana:
    // do something
    break;
  default:
    assert false : "Missing enum value: " + fruit;
}
12 голосов
/ 03 мая 2010

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

Примером утверждения может быть проверка того, что определенная группа методов вызывается в правильном порядке (например, что hasNext() вызывается до next() в Iterator).

7 голосов

Что делает ключевое слово assert в Java?

Давайте посмотрим на скомпилированный байт-код.

Мы сделаем вывод, что:

public class Assert {
    public static void main(String[] args) {
        assert System.currentTimeMillis() == 0L;
    }
}

генерирует почти тот же байт-код, что и:

public class Assert {
    static final boolean $assertionsDisabled =
        !Assert.class.desiredAssertionStatus();
    public static void main(String[] args) {
        if (!$assertionsDisabled) {
            if (System.currentTimeMillis() != 0L) {
                throw new AssertionError();
            }
        }
    }
}

где Assert.class.desiredAssertionStatus() равно true, когда -ea передается в командной строке, и false в противном случае.

Мы используем System.currentTimeMillis(), чтобы гарантировать, что он не будет оптимизирован (assert true; сделал).

Синтетическое поле генерируется так, что Java должен вызывать Assert.class.desiredAssertionStatus() только один раз во время загрузки, а затем кэшировать результат там. Смотрите также: Что означает "статический синтетический"?

Мы можем проверить это с помощью:

javac Assert.java
javap -c -constants -private -verbose Assert.class

В Oracle JDK 1.8.0_45 было сгенерировано синтетическое статическое поле (см. Также: Что означает «статическое синтетическое»? ):

static final boolean $assertionsDisabled;
  descriptor: Z
  flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC

вместе со статическим инициализатором:

 0: ldc           #6                  // class Assert
 2: invokevirtual #7                  // Method java/lang Class.desiredAssertionStatus:()Z
 5: ifne          12
 8: iconst_1
 9: goto          13
12: iconst_0
13: putstatic     #2                  // Field $assertionsDisabled:Z
16: return

и основной метод:

 0: getstatic     #2                  // Field $assertionsDisabled:Z
 3: ifne          22
 6: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
 9: lconst_0
10: lcmp
11: ifeq          22
14: new           #4                  // class java/lang/AssertionError
17: dup
18: invokespecial #5                  // Method java/lang/AssertionError."<init>":()V
21: athrow
22: return

Мы заключаем, что:

  • нет поддержки уровня байт-кода для assert: это концепция языка Java
  • assert вполне можно эмулировать с помощью системных свойств -Pcom.me.assert=true для замены -ea в командной строке и throw new AssertionError().
6 голосов
/ 13 июня 2017

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

Зачем утверждать что-то, когда вы знаете, что это правда? Это правда только тогда, когда все работает правильно. Если программа имеет дефект, это может быть на самом деле не так. Обнаружение этого ранее в процессе позволяет понять, что что-то не так.

Оператор assert содержит этот оператор вместе с необязательным сообщением String.

Синтаксис оператора assert имеет две формы:

assert boolean_expression;
assert boolean_expression: error_message;

Вот некоторые основные правила, которые определяют, где следует использовать утверждения и где их не следует использовать. Утверждения следует использовать для:

  1. Проверка входных параметров частного метода. НЕ для открытых методов. Методы public должны выдавать регулярные исключения при передаче неверных параметров.

  2. В любом месте программы, чтобы обеспечить достоверность факта, который почти наверняка является правдой.

Например, если вы уверены, что это будет только 1 или 2, вы можете использовать следующее утверждение:

...
if (i == 1)    {
    ...
}
else if (i == 2)    {
    ...
} else {
    assert false : "cannot happen. i is " + i;
}
...
  1. Проверка почтовых условий в конце любого метода. Это означает, что после выполнения бизнес-логики вы можете использовать утверждения, чтобы гарантировать, что внутреннее состояние ваших переменных или результатов соответствует ожидаемому. Например, метод, который открывает сокет или файл, может использовать утверждение в конце, чтобы убедиться, что сокет или файл действительно открыт.

Утверждения не следует использовать для:

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

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

  3. Не следует использовать для побочных эффектов.

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

public boolean doSomething() {
...    
}
public void someMethod() {       
assert doSomething(); 
}

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

boolean enabled = false;    
assert enabled = true;    
if (enabled) {
    System.out.println("Assertions are enabled");
} else {
    System.out.println("Assertions are disabled");
}
6 голосов
/ 03 мая 2010

Реальный пример из класса Stack (из Утверждение в статьях Java )

public int pop() {
   // precondition
   assert !isEmpty() : "Stack is empty";
   return stack[--num];
}
5 голосов
/ 16 мая 2015

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

Ссылка

...