Comportement indéfini en Java

14

Je lisais cette question sur SO qui discute d'un comportement non défini commun en C ++, et je me suis demandé: Java a-t-il également un comportement non défini?

Si tel est le cas, quelles sont les causes courantes de comportement indéfini en Java?

Sinon, quelles fonctionnalités de Java le rendent exempt de tels comportements et pourquoi les dernières versions de C et C ++ n'ont-elles pas été implémentées avec ces propriétés?

Huit
la source
4
Java est défini de manière très rigide. Vérifiez les spécifications du langage Java.
4
@ user1249, le "comportement indéfini" est également défini de manière assez rigide.
Pacerier
Même chose possible sur SO: stackoverflow.com/questions/376338/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Que dit Java lorsque vous violez un "contrat"? Comme cela se produit lorsque vous surchargez .equals pour être incompatible avec .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Est-ce familièrement indéfini, mais pas techniquement de la même manière que C ++?
Mooing Duck

Réponses:

18

En Java, vous pouvez considérer que le comportement d'un programme mal synchronisé n'est pas défini.

Java 7 JLS utilise le mot "non défini" une fois, en 17.4.8. Exécutions et conditions de causalité :

Nous utilisons f|dpour désigner la fonction donnée en restreignant le domaine de fà d. Pour tous xdans d, f|d(x) = f(x)et pour tous les xpas d, f|d(x)est indéfini ...

La documentation de l'API Java spécifie certains cas où les résultats ne sont pas définis - par exemple, dans le constructeur (obsolète) Date (int année, mois int, jour int) :

Le résultat n'est pas défini si un argument donné est hors limites ...

État des Javadocs pour ExecutorService.invokeAll (Collection) :

Les résultats de cette méthode ne sont pas définis si la collection donnée est modifiée pendant que cette opération est en cours ...

Un type moins formel de comportement "non défini" peut être trouvé par exemple dans ConcurrentModificationException , où les documents API utilisent le terme "meilleur effort":

Notez que le comportement de défaillance rapide ne peut pas être garanti car il est généralement impossible de faire des garanties dures en présence d'une modification simultanée non synchronisée. Les opérations rapides échouent ConcurrentModificationExceptionau mieux . Par conséquent, il serait erroné d'écrire un programme qui dépend de cette exception pour sa justesse ...


appendice

L'un des commentaires de la question fait référence à un article d'Eric Lippert qui fournit une introduction utile aux sujets: le comportement défini par l'implémentation .

Je recommande cet article pour le raisonnement indépendant du langage, même s'il convient de garder à l'esprit que l'auteur cible C #, pas Java.

Traditionnellement, nous disons qu'un idiome de langage de programmation a un comportement indéfini si l'utilisation de cet idiome peut avoir un quelconque effet; il peut fonctionner comme vous le souhaitez ou il peut effacer votre disque dur ou planter votre machine. De plus, l'auteur du compilateur n'a aucune obligation de vous avertir du comportement indéfini. (Et en fait, il existe des langages dans lesquels les programmes qui utilisent des idiomes de "comportement indéfini" sont autorisés par la spécification du langage à planter le compilateur!) ...

En revanche, un idiome qui a un comportement défini par l'implémentation est un comportement où l'auteur du compilateur a plusieurs choix sur la façon d'implémenter la fonctionnalité et doit en choisir un. Comme son nom l'indique, le comportement défini par l'implémentation est au moins défini. Par exemple, C # permet à une implémentation de lever une exception ou de produire une valeur lorsqu'une division entière déborde, mais l'implémentation doit en choisir une. Il ne peut pas effacer votre disque dur ...

Quels sont certains des facteurs qui poussent un comité de conception linguistique à laisser certains idiomes linguistiques comme comportements indéfinis ou définis par la mise en œuvre?

Le premier facteur majeur est: y a-t-il deux implémentations existantes de la langue sur le marché qui sont en désaccord sur le comportement d'un programme particulier? ...

Le prochain facteur majeur est: la fonctionnalité présente-t-elle naturellement de nombreuses possibilités de mise en œuvre, dont certaines sont clairement meilleures que d'autres? ...

Un troisième facteur est le suivant: la caractéristique est-elle si complexe qu'une ventilation détaillée de son comportement exact serait difficile ou coûteuse à spécifier? ...

Un quatrième facteur est: la fonctionnalité impose-t-elle une charge élevée au compilateur à analyser? ...

Un cinquième facteur est le suivant: la fonctionnalité impose-t-elle une charge élevée à l'environnement d'exécution? ...

Un sixième facteur est: la définition du comportement empêche-t-elle une optimisation majeure? ...

Ce ne sont là que quelques facteurs qui me viennent à l'esprit; il y a bien sûr de nombreux autres facteurs dont les comités de conception linguistique débattent avant de faire une caractéristique «mise en œuvre définie» ou «non définie».

Ci-dessus est seulement une couverture très brève; l'article complet contient des explications et des exemples pour les points mentionnés dans cet extrait; cela vaut la peine d' être lu. Par exemple, les détails fournis pour le "sixième facteur" peuvent donner un aperçu de la motivation de nombreuses instructions dans le modèle de mémoire Java ( JSR 133 ), aidant à comprendre pourquoi certaines optimisations sont autorisées, conduisant à un comportement indéfini tandis que d'autres sont interdites, conduisant à limitations telles que les conditions de survenance et de causalité .

Aucun des matériaux de l'article n'est particulièrement nouveau pour moi, mais je serai damné si jamais je le voyais présenté d'une manière aussi élégante, concise et compréhensible. Incroyable.

moucheron
la source
J'ajouterai que le matériel sous-jacent JMM! = Et le résultat final d'un programme en cours d'exécution en ce qui concerne la concurrence peuvent varier, disons un WinIntel vs un Solaris
Martijn Verburg
2
@MartijnVerburg c'est un assez bon point. La seule raison pour laquelle j'hésite à le marquer comme "indéfini" est que le modèle de mémoire pose des contraintes telles que le survenance et la causalité lors de l'exécution d'un programme correctement synchronisé
gnat
Certes, la spécification définit comment il doit se comporter sous le JMM, cependant, Intel et al ne sont pas toujours d'accord ;-)
Martijn Verburg
@MartijnVerburg Je pense que le principal objectif de JMM est d'empêcher les fuites de sur-optimisation des fabricants de processeurs "en désaccord". Pour autant que je sache, Java avant 5.0 a eu ce genre de maux de tête avec DEC Alpha, lorsque des écritures spéculatives effectuées sous le capot pouvaient s'infiltrer dans un programme comme "de nulle part " - par conséquent, l' exigence de causalité est entrée dans JSR 133 (JMM)
gnat
9
@MartinVerburg - c'est le travail d'un implémenteur JVM de s'assurer que la JVM se comporte conformément aux spécifications JLS / JMM sur n'importe quelle plate-forme matérielle prise en charge. Si un matériel différent se comporte différemment, c'est le travail de l'implémenteur JVM de le gérer ... et de le faire fonctionner.
Stephen C
10

Du haut de ma tête, je ne pense pas qu'il y ait un comportement indéfini en Java, du moins pas dans le même sens qu'en C ++.

La raison en est qu'il y a une philosophie différente derrière Java que derrière C ++. L'un des principaux objectifs de conception de Java était de permettre aux programmes de fonctionner sans changement sur toutes les plateformes, de sorte que la spécification définit tout de manière très explicite.

En revanche, un objectif de conception de base de C et C ++ est l'efficacité: il ne devrait pas y avoir de fonctionnalités (y compris l'indépendance de la plate-forme) qui coûtent les performances même si vous n'en avez pas besoin. À cette fin, la spécification ne définit pas délibérément certains comportements, car leur définition entraînerait un travail supplémentaire sur certaines plates-formes et réduirait ainsi les performances, même pour les personnes qui écrivent des programmes spécifiquement pour une plate-forme et connaissent toutes ses particularités.

Il y a même un exemple où Java a été contraint d'introduire rétroactivement une forme limitée de comportement indéfini pour exactement cette raison: le mot clé strictfp a été introduit dans Java 1.2 pour permettre aux calculs en virgule flottante de s'écarter de suivre exactement la norme IEEE 754 comme la spécification l'avait précédemment demandé , car cela nécessitait un travail supplémentaire et rendait tous les calculs à virgule flottante plus lents sur certains processeurs courants, tout en produisant des résultats moins bons dans certains cas.

Michael Borgwardt
la source
2
Je pense qu'il est important de noter l'autre objectif principal de Java: la sécurité et l'isolement. Je pense que cela aussi est une raison du manque de comportement "non défini" (comme en C ++).
K.Steff
3
@ K.Steff: Hyper-moderne C / C ++ est totalement inapproprié pour tout ce qui concerne la sécurité à distance. Étant donné int x=-1; foo(); x<<=1;la philosophie hyper-moderne, il serait préférable de réécrire fooafin que tout chemin qui ne quitte pas soit inaccessible. Ce, si fooest if (should_launch_missiles) { launch_missiles(); exit(1); }un compilateur pourrait (et selon certaines personnes devraient) simplifier que simplement launch_missiles(); exit(1);. L'UB traditionnel était l'exécution de code aléatoire, mais celle-ci était liée par les lois du temps et de la causalité. Le nouvel UB amélioré n'est lié par aucun des deux.
supercat
3

Java essaie assez fort d'exterminer les comportements indéfinis, précisément à cause des leçons des langages antérieurs. Par exemple, les variables de niveau classe sont automatiquement initialisées; les variables locales ne sont pas auto-initialisées pour des raisons de performances, mais il existe une analyse de flux de données sophistiquée pour empêcher quiconque d'écrire un programme qui serait capable de détecter cela. Les références ne sont pas des pointeurs, les références non valides ne peuvent donc pas exister et le déréférencement nullprovoque une exception spécifique.

Bien sûr, certains comportements ne sont pas entièrement spécifiés et vous pouvez écrire des programmes peu fiables si vous supposez qu'ils le sont. Par exemple, si vous parcourez une normale (non triée) Set, le langage garantit que vous verrez chaque élément exactement une fois, mais pas dans quel ordre vous les verrez. L'ordre peut être le même sur des séries successives, ou il peut changer; ou il peut rester le même tant qu'aucune autre allocation ne se produit, ou tant que vous ne mettez pas à jour votre JDK, etc. Il est presque impossible de se débarrasser de tous ces effets; par exemple, vous devrez explicitement ordonner ou randomiser toutes les opérations de collections, et cela ne vaut tout simplement pas le petit un-undefined-ness supplémentaire.

Kilian Foth
la source
Les références sont des pointeurs sous un autre nom
curiousguy
@curiousguy - les "références" sont généralement supposées ne pas permettre l'utilisation de la manipulation arithmétique de leur valeur numérique, ce qui est souvent autorisé pour les "pointeurs". Le premier est donc une construction plus sûre que le second; combiné avec un système de gestion de la mémoire qui ne permet pas de réutiliser le stockage d'un objet tant qu'il existe une référence valide, les références empêchent les erreurs d'utilisation de la mémoire. Les pointeurs ne peuvent pas le faire, même lorsque la gestion de la mémoire appropriée est utilisée.
Jules
@Jules Ensuite, c'est une question de terminologie: vous pouvez appeler une chose un pointeur ou une référence, et décider d'utiliser "référence" dans les langues "sûres" et "pointeur" dans les langues qui permettent l'utilisation de l'arithmétique des pointeurs et la gestion manuelle de la mémoire. (AFAIK "arithmetic pointeur" se fait uniquement en C / C ++.)
curiousguy
2

Vous devez comprendre le "comportement indéfini" et son origine.

Un comportement indéfini signifie un comportement qui n'est pas défini par les normes. C / C ++ a trop d'implémentations de compilateur différentes et de fonctionnalités supplémentaires. Ces fonctionnalités supplémentaires ont lié le code au compilateur. En effet, il n'y avait pas de développement de langage centralisé. Ainsi, certaines des fonctionnalités avancées de certains compilateurs sont devenues des "comportements non définis".

Alors qu'en Java la spécification du langage est contrôlée par Sun-Oracle et personne d'autre n'essaye de faire des spécifications et donc aucun comportement indéfini.

Édité répondant spécifiquement à la question

  1. Java est exempt de comportements non définis car les normes ont été créées avant les compilateurs
  2. Les compilateurs C / C ++ modernes ont plus / moins standardisé les implémentations, mais les fonctionnalités implémentées avant la normalisation restent étiquetées comme "comportement indéfini" car l'ISO est resté muet sur ces aspects.
Sarvex
la source
2
Vous avez peut-être raison de dire qu'il n'y a pas d'UB en Java, mais même lorsqu'une entité contrôle tout, il peut y avoir des raisons d'avoir UB, donc la raison que vous donnez ne mène pas à la conclusion.
AProgrammer
2
De plus, C et C ++ sont normalisés par l'ISO. Bien qu'il puisse y avoir plusieurs compilateurs, il n'y a qu'une seule norme à la fois.
MSalters
1
@SarvexJatasra, je ne suis pas d'accord pour dire que c'est la seule source d'UB. Par exemple, un UB déréférence le pointeur pendant et il y a de bonnes raisons de le laisser un UB dans n'importe quelle langue qui n'a pas de GC, même si vous démarrez votre spécification maintenant. Et ces raisons n'ont rien à voir avec la pratique existante ou les compilateurs existants.
AProgrammer
2
@SarvexJatasra, le débordement signé est UB parce que la norme le dit explicitement (c'est même l'exemple donné avec la définition de UB). Déréférencer un pointeur invalide est également un UB pour la même raison, le standard le dit.
AProgrammer
2
@ bames53: Aucun des avantages cités ne nécessiterait le niveau de latitude que les compilateurs hypermodernes prennent avec UB. À l'exception des accès à la mémoire hors limites et des débordements de pile, qui peuvent induire "naturellement" l'exécution de code aléatoire, je ne peux penser à aucune optimisation utile qui nécessiterait une latitude plus large que de dire que la plupart des opérations UB-ish donnent un résultat indéterminé valeurs (qui peuvent se comporter comme si elles avaient des "bits supplémentaires") et ne peuvent avoir des conséquences au-delà de cela que si les documents d'une implémentation se réservent expressément le droit de les imposer; les documents peuvent donner un "comportement sans
contrainte
1

Java élimine essentiellement tous les comportements non définis trouvés en C / C ++. (Par exemple: débordement d'entier signé, division par zéro, variables non initialisées, déréférence de pointeur nul, décalage de plus de largeur de bit, double-free, même "pas de nouvelle ligne à la fin du code source".) Mais Java a quelques comportements obscurs non définis qui sont rarement rencontrés par les programmeurs.

  • Java Native Interface (JNI), un moyen pour Java d'appeler du code C ou C ++. Il existe de nombreuses façons de bousiller JNI, comme se tromper de signature de fonction, passer des appels invalides aux services JVM, corrompre la mémoire, allouer / libérer des choses de manière incorrecte, etc. J'ai déjà fait ces erreurs, et généralement la JVM entière se bloque lorsqu'un thread exécutant du code JNI commet une erreur.

  • Thread.stop(), qui est obsolète. Citation:

    Pourquoi est Thread.stopobsolète?

    Parce qu'il est intrinsèquement dangereux. L'arrêt d'un thread provoque le déverrouillage de tous les moniteurs qu'il a verrouillés. (Les moniteurs sont déverrouillés lorsque l' ThreadDeathexception se propage dans la pile.) Si l'un des objets précédemment protégés par ces moniteurs était dans un état incohérent, d'autres threads peuvent désormais afficher ces objets dans un état incohérent. Ces objets seraient endommagés. Lorsque les threads opèrent sur des objets endommagés, un comportement arbitraire peut en résulter. Ce comportement peut être subtil et difficile à détecter, ou il peut être prononcé. Contrairement à d'autres exceptions non contrôlées, ThreadDeathtue les threads en silence; ainsi, l'utilisateur n'a aucun avertissement que son programme peut être corrompu. La corruption peut se manifester à tout moment après que le dommage réel se soit produit, même des heures ou des jours à l'avenir.

    https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/threadPrimitiveDeprecation.html

Nayuki
la source