Quels sont quelques exemples réels pour comprendre le rôle clé des assertions?
Les assertions (au moyen du mot clé assert) ont été ajoutées à Java 1.4. Ils sont utilisés pour vérifier l'exactitude d'un invariant dans le code. Ils ne doivent jamais être déclenchés dans le code de production et sont révélateurs d'un bug ou d'une mauvaise utilisation d'un chemin de code. Ils peuvent être activés au moment de l'exécution à l'aide de l'option -ea
de la commande Java
, mais ils ne sont pas activés par défaut.
Un exemple:
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;
}
Supposons que vous êtes censé écrire un programme pour contrôler une centrale nucléaire. Il est assez évident que même les erreurs les plus mineures pourraient avoir des résultats catastrophiques. Par conséquent, votre code doit être exempt de bogues (en supposant que la machine virtuelle Java soit exempte de bogues par souci d'argument).
Java n'est pas un langage vérifiable, ce qui signifie que vous ne pouvez pas calculer que le résultat de votre opération sera parfait. La raison principale en est les pointeurs: ils peuvent pointer n'importe où ou nulle part, ils ne peuvent donc pas être calculés pour être de cette valeur exacte, du moins pas dans une plage de code raisonnable. Compte tenu de ce problème, il n’ya aucun moyen de prouver que votre code est correct dans son ensemble. Mais ce que vous pouvez faire, c'est prouver que vous trouvez au moins tous les bogues quand cela se produit.
Cette idée est basée sur le paradigme Conception par contrat (DbC): vous définissez d’abord (avec une précision mathématique) ce que votre méthode est censée faire, puis vous le vérifiez en le testant au cours de son exécution réelle. Exemple:
// Calculates the sum of a (int) + b (int) and returns the result (int).
int sum(int a, int b) {
return a + b;
}
Bien que ce soit assez évident de bien fonctionner, la plupart des programmeurs ne verront pas le bogue caché à l'intérieur de celui-ci (indice: Ariane V s'est écrasée à cause d'un bogue similaire). Maintenant, DbC définit que vous devez toujours vérifier l’entrée et la sortie d’une fonction pour vérifier son bon fonctionnement. Java peut le faire à travers des assertions:
// 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;
}
Si cette fonction devait maintenant échouer, vous le remarquerez. Vous saurez qu'il y a un problème dans votre code, vous savez où il se trouve et vous savez quelle en est la cause (semblable à Exceptions). Et ce qui est encore plus important: vous arrêtez d’exécuter correctement lorsque vous empêchez tout code supplémentaire de fonctionner avec des valeurs incorrectes et d’endommager potentiellement tout ce qu’il contrôle.
Les exceptions Java sont un concept similaire, mais elles ne parviennent pas à tout vérifier. Si vous voulez encore plus de contrôles (au prix de la rapidité d'exécution), vous devez utiliser des assertions. Cela encombrerait votre code, mais vous pourrez finalement livrer un produit avec un temps de développement étonnamment court (plus tôt vous corrigez un bogue, moins le coût est élevé). Et en plus: s'il y a un bug dans votre code, vous le détecterez. Il n’ya aucun moyen de laisser passer un bogue et de causer des problèmes plus tard.
Ce n’est toujours pas une garantie pour un code exempt de bogues, mais c’est beaucoup plus proche de cela que les programmes habituels.
Les assertions sont un outil en phase de développement permettant de détecter les bogues dans votre code. Ils sont conçus pour être facilement supprimés, ils n'existeront donc pas dans le code de production. Les assertions ne font donc pas partie de la "solution" que vous livrez au client. Ce sont des contrôles internes pour vous assurer que les hypothèses que vous avancez sont correctes. L'exemple le plus courant consiste à tester null. Beaucoup de méthodes sont écrites comme ceci:
void doSomething(Widget widget) {
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
Très souvent, dans une méthode comme celle-ci, le widget ne doit tout simplement jamais être null. Donc, si elle est nulle, il y a un bogue dans votre code que vous devez localiser. Mais le code ci-dessus ne vous le dira jamais. Donc, dans un effort bien intentionné pour écrire du code "sûr", vous cachez également un bogue. C'est beaucoup mieux d'écrire un code comme ça:
/**
* @param Widget widget Should never be null
*/
void doSomething(Widget widget) {
assert widget != null;
widget.someMethod(); // ...
... // do more stuff with this widget
}
De cette façon, vous serez sûr d'attraper ce bogue tôt. (Il est également utile de spécifier dans le contrat que ce paramètre ne doit jamais être nul.) Assurez-vous d'activer les assertions lorsque vous testez votre code pendant le développement. (Et il est souvent difficile de persuader vos collègues de le faire, ce que je trouve très agaçant.)
Désormais, certains de vos collègues s’opposeront à ce code, arguant que vous devez toujours activer le contrôle null pour empêcher une exception en production. Dans ce cas, l'assertion est toujours utile. Vous pouvez l'écrire comme ceci:
void doSomething(Widget widget) {
assert widget != null;
if (widget != null) {
widget.someMethod(); // ...
... // do more stuff with this widget
}
}
De cette façon, vos collègues seront heureux que le contrôle de nullité existe pour le code de production, mais lors du développement, vous ne cachez plus le bogue lorsque le widget est nul.
Voici un exemple concret: j’ai une fois écrit une méthode qui comparait deux valeurs arbitraires pour l’égalité, où l’une ou l’autre des valeurs pouvait être nulle:
/**
* 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;
}
Ce code délègue le travail de la méthode equals()
dans le cas où thisValue n'est pas null. Mais il suppose que la méthode equals()
remplit correctement le contrat de equals()
en gérant correctement un paramètre null.
Un collègue s'est opposé à mon code, me disant que beaucoup de nos classes ont des méthodes boguées equals()
qui ne testent pas la valeur null. Je devrais donc mettre cette vérification dans cette méthode. C'est discutable si c'est sage, ou si nous devons forcer l'erreur, afin que nous puissions la repérer et la corriger, mais j'ai reporté à mon collègue et fait un chèque nul, que j'ai marqué avec un commentaire:
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;
}
La vérification supplémentaire ici, other != null
, n'est nécessaire que si la méthode equals()
ne parvient pas à rechercher la valeur null comme l'exige son contrat.
Plutôt que d'engager un débat stérile avec mon collègue sur l'opportunité de laisser le code buggy rester dans notre base de code, j'ai simplement ajouté deux affirmations dans le code. Ces affirmations me permettront de savoir, au cours de la phase de développement, si l'une de nos classes ne parvient pas à implémenter correctement equals()
, afin que je puisse résoudre ce problème:
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;
}
Les points importants à garder à l’esprit sont les suivants:
Les assertions sont des outils en phase de développement uniquement.
Le but d'une assertion est de vous faire savoir s'il y a un bogue, pas seulement dans votre code, mais dans votre code base . (Les assertions ici marqueront en fait des bogues dans d'autres classes.)
Même si ma collègue était convaincue que nos cours étaient bien écrits, les affirmations seraient toujours utiles. De nouvelles classes seront ajoutées qui risquent de ne pas pouvoir tester null, et cette méthode peut signaler ces bogues pour nous.
En développement, vous devez toujours activer les assertions, même si le code que vous avez écrit n'utilise pas d'assertions. Mon IDE est configuré pour toujours le faire par défaut pour tout nouvel exécutable.
Les assertions ne modifient pas le comportement du code en production. Mon collègue est donc heureux que la vérification de la valeur NULL soit présente et que cette méthode s'exécute correctement même si la méthode equals()
est boguée. Je suis content parce que je vais attraper n'importe quelle méthode buggy equals()
en développement.
En outre, vous devez tester votre stratégie d'assertion en mettant en place une assertion temporaire qui échouera, afin de pouvoir être certain que vous en êtes averti, via le fichier journal ou une trace de pile dans le flux de sortie.
Beaucoup de bonnes réponses expliquant ce que fait le mot clé assert
, mais peu de réponses à la vraie question, "quand le mot clé assert
doit-il être utilisé dans la vie réelle?"
La réponse: presque jamais.
Les affirmations, en tant que concept, sont merveilleuses. Bon code a beaucoup de déclarations if (...) throw ...
(et leurs proches comme Objects.requireNonNull
et Math.addExact
). Cependant, certaines décisions de conception ont considérablement limité l'utilité du assert
mot-clé lui-même.
L'idée directrice derrière le mot clé assert
est l'optimisation prématurée, et la principale caractéristique est de pouvoir facilement désactiver toutes les vérifications. En fait, les vérifications assert
sont désactivées par défaut.
Cependant, il est essentiel que les contrôles invariants continuent d'être effectués en production. En effet, une couverture de test parfaite est impossible et tout code de production comportera des bogues que les assertions devraient aider à diagnostiquer et à atténuer.
Par conséquent, l'utilisation de if (...) throw ...
devrait être préférée, tout comme elle est requise pour la vérification des valeurs de paramètre des méthodes publiques et pour le lancement de IllegalArgumentException
.
De temps en temps, on peut être tenté d’écrire un contrôle invariant dont le traitement prend un temps excessivement long (et est appelé suffisamment souvent pour que cela ait de l’importance). Cependant, ces contrôles ralentiront les tests, ce qui n’est pas souhaitable non plus. Ces contrôles qui prennent beaucoup de temps sont généralement écrits sous forme de tests unitaires. Néanmoins, il peut parfois être utile d’utiliser assert
pour cette raison.
N'utilisez pas assert
simplement parce que c'est plus propre et plus joli que if (...) throw ...
(et je le dis avec douleur, parce que j'aime être propre et joli). Si vous ne pouvez simplement pas vous aider et que vous pouvez contrôler le mode de lancement de votre application, n'hésitez pas à utiliser assert
mais toujours activer les assertions dans la production. Certes, c'est ce que j'ai tendance à faire. Je préconise une annotation Lombok qui fera que assert
agira davantage comme if (...) throw ...
. Votez pour cela ici.
(Rant: les développeurs de la machine virtuelle Java constituaient un groupe de codeurs terribles qui optimisaient prématurément. C’est pourquoi vous avez entendu parler de tant de problèmes de sécurité dans le plug-in Java et la machine virtuelle Java. payer le prix.)
Voici le cas d'utilisation le plus courant. Supposons que vous basculiez sur une valeur enum:
switch (fruit) {
case Apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
}
Tant que vous gérez tous les cas, vous allez bien. Mais un jour, quelqu'un ajoutera fig à votre enum et oubliera de l'ajouter à votre déclaration switch. Cela produit un bug qui peut être difficile à détecter, car les effets ne seront pas ressentis tant que vous n’aurez pas quitté l’instruction switch. Mais si vous écrivez votre commutateur comme ceci, vous pouvez le saisir immédiatement:
switch (fruit) {
case Apple:
// do something
break;
case pear:
// do something
break;
case banana:
// do something
break;
default:
assert false : "Missing enum value: " + fruit;
}
Les assertions sont utilisées pour vérifier les post-conditions et les pré-conditions "ne doit jamais échouer". Un code correct ne doit jamais échouer une assertion; quand ils se déclenchent, ils devraient indiquer un bogue (heureusement à un endroit proche du lieu où se trouve le problème).
Un exemple d'assertion pourrait consister à vérifier qu'un groupe particulier de méthodes est appelé dans le bon ordre (par exemple, que hasNext()
est appelé avant next()
dans une Iterator
).
Que fait le mot clé assert en Java?
Regardons le bytecode compilé.
Nous conclurons que:
public class Assert {
public static void main(String[] args) {
assert System.currentTimeMillis() == 0L;
}
}
génère presque exactement le même bytecode que:
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();
}
}
}
}
où Assert.class.desiredAssertionStatus()
est true
lorsque -ea
est passé sur la ligne de commande et false sinon.
Nous utilisons System.currentTimeMillis()
pour nous assurer qu'il ne sera pas optimisé (assert true;
l'a fait).
Le champ synthétique est généré de sorte que Java n’a à appeler que Assert.class.desiredAssertionStatus()
une fois au moment du chargement et met ensuite le résultat en mémoire cache. Voir aussi: Que signifie "synthétique synthétique"?
Nous pouvons vérifier cela avec:
javac Assert.Java
javap -c -constants -private -verbose Assert.class
Avec Oracle JDK 1.8.0_45, un champ statique synthétique a été généré (voir aussi: Que signifie "synthèse synthétique"? ):
static final boolean $assertionsDisabled;
descriptor: Z
flags: ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
avec un initialiseur statique:
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
et la méthode principale est:
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
Nous concluons que:
assert
n'est pas pris en charge au niveau du bytecode: il s'agit d'un concept de langage Javaassert
pourrait très bien être émulé avec les propriétés système -Pcom.me.assert=true
pour remplacer -ea
sur la ligne de commande et un throw new AssertionError()
.Un exemple concret tiré d'une classe Stack (from Assertion in Java Articles )
public int pop() {
// precondition
assert !isEmpty() : "Stack is empty";
return stack[--num];
}
Une assertion permet de détecter des défauts dans le code. Vous pouvez activer les assertions pour les tests et le débogage tout en les laissant désactivées lorsque votre programme est en production.
Pourquoi affirmer quelque chose quand vous savez que c'est vrai? Ce n'est vrai que lorsque tout fonctionne correctement. Si le programme a un défaut, il est possible que ce ne soit pas le cas. Détecter cela plus tôt dans le processus vous permet de savoir que quelque chose ne va pas.
Une instruction assert
contient cette instruction avec un message optionnel String
.
La syntaxe d'une instruction assert a deux formes:
assert boolean_expression;
assert boolean_expression: error_message;
Voici quelques règles de base qui régissent où les assertions doivent être utilisées et où elles ne doivent pas être utilisées. Les assertions devraient devraient être utilisées pour:
Validation des paramètres d'entrée d'une méthode privée.PASpour les méthodes publiques. Les méthodes public
doivent générer des exceptions régulières lorsqu'elles sont transmises à de mauvais paramètres.
N'importe où dans le programme pour assurer la validité d'un fait qui est presque certainement vrai.
Par exemple, si vous êtes sûr que ce ne sera que 1 ou 2, vous pouvez utiliser une assertion comme celle-ci:
...
if (i == 1) {
...
}
else if (i == 2) {
...
} else {
assert false : "cannot happen. i is " + i;
}
...
Les assertions ne devraient pas devraient être utilisées pour:
Validation des paramètres d'entrée d'une méthode publique. Etant donné que les assertions ne peuvent pas toujours être exécutées, le mécanisme des exceptions régulières doit être utilisé.
Valider les contraintes sur quelque chose qui est entré par l'utilisateur. Comme ci-dessus.
Ne devrait pas être utilisé pour des effets secondaires.
Par exemple, l'utilisation n'est pas appropriée car l'assertion est utilisée pour son effet secondaire d'appeler la méthode doSomething()
.
public boolean doSomething() {
...
}
public void someMethod() {
assert doSomething();
}
Le seul cas où cela pourrait être justifié est lorsque vous essayez de savoir si les assertions sont activées dans votre code:
boolean enabled = false;
assert enabled = true;
if (enabled) {
System.out.println("Assertions are enabled");
} else {
System.out.println("Assertions are disabled");
}
En plus de toutes les bonnes réponses fournies ici, le guide de programmation officiel de Java SE 7 contient un manuel assez concis sur l’utilisation de assert
; avec plusieurs exemples précis de cas où il est judicieux (et, surtout, mauvais) d'utiliser des assertions, et en quoi c'est différent des exceptions.
Assert est très utile lors du développement. Vous l'utilisez quand quelque chose se passe ne peut pas se produire si votre code fonctionne correctement. Il est facile à utiliser et peut rester dans le code pour toujours, car il sera désactivé dans la vie réelle.
S'il y a une chance que la condition puisse se produire dans la vie réelle, alors vous devez la gérer.
Je l'aime, mais je ne sais pas comment l'activer dans Eclipse/Android/ADT. Il semble être éteint même lors du débogage. (Il existe un fil à ce sujet, mais il fait référence à la «version Java» qui n'apparaît pas dans la configuration d'exécution ADT).
Les assertions sont désactivées par défaut. Pour les activer, nous devons exécuter le programme avec les options -ea
(la granularité peut être modifiée). Par exemple, Java -ea AssertionsDemo
.
Il existe deux formats pour utiliser des assertions:
assert 1==2; // This will raise an AssertionError
.assert 1==2: "no way.. 1 is not equal to 2";
Ceci soulèvera une AssertionError avec le message affiché aussi et est donc meilleur. Bien que la syntaxe réelle soit assert expr1:expr2
, où expr2 peut être toute expression renvoyant une valeur, je l’utilise plus souvent pour imprimer un message.Les assertions sont essentiellement utilisées pour déboguer l'application ou sont utilisées en remplacement du traitement des exceptions pour certaines applications afin de vérifier la validité d'une application.
L'assertion fonctionne au moment de l'exécution. Voici un exemple simple, expliquant très simplement le concept dans son ensemble: - Que fait le mot clé assert en Java? (WikiAnswers).
Pour récapituler (et cela est vrai de nombreuses langues et pas seulement de Java):
"assert" est principalement utilisé comme aide au débogage par les développeurs de logiciels au cours du processus de débogage. Les messages d'assertion ne doivent jamais apparaître. De nombreux langages fournissent une option de compilation qui fera en sorte que tous les "assertions" soient ignorés, pour être utilisés dans la génération de code "de production".
Les "exceptions" sont un moyen pratique de gérer toutes sortes de conditions d'erreur, qu'elles représentent ou non des erreurs de logique, car si vous rencontrez une condition d'erreur telle que vous ne pouvez plus continuer, vous pouvez simplement les "jeter en l'air", "Où que vous soyez, attendez-vous à ce que quelqu'un d'autre soit prêt à les" attraper ". Le contrôle est transféré en une étape, directement du code qui a jeté l'exception, directement au gant du receveur. (Et le receveur peut voir la trace complète des appels qui ont eu lieu.)
En outre, les appelants de ce sous-programme ne doivent pas vérifier si le sous-programme a réussi: "si nous sommes ici maintenant, il doit avoir réussi, car sinon il aurait généré une exception et nous ne serions pas ici maintenant. ! " Cette stratégie simple facilite beaucoup la conception de code et le débogage.
Les exceptions permettent aisément aux conditions d'erreur fatale d'être ce qu'elles sont: "des exceptions à la règle". Et, pour qu'ils soient gérés par un chemin de code qui est aussi "une exception à la règle ..." fly ball! "
Fondamentalement, "assert true" passera et "assert false" échouera. Voyons comment cela fonctionnera:
public static void main(String[] args)
{
String s1 = "Hello";
assert checkInteger(s1);
}
private static boolean checkInteger(String s)
{
try {
Integer.parseInt(s);
return true;
}
catch(Exception e)
{
return false;
}
}
Les assertions sont des vérifications qui peuvent être désactivées. Ils sont rarement utilisés. Pourquoi?
result != null
, car ces vérifications sont très rapides et il n’ya pratiquement rien à enregistrer.Alors, que reste-t-il? Cher vérifie si les conditions sont vraiment attendues pour être vraies. Un bon exemple serait les invariants d’une structure de données telle que RB-tree. En fait, dans ConcurrentHashMap
de JDK8, il y a quelques affirmations de ce type pour TreeNodes
.