Comportamento indefinito in Java


14

Stavo leggendo questa domanda su SO che discute alcuni comportamenti indefiniti comuni in C ++ e mi chiedevo: Java ha anche un comportamento indefinito?

In tal caso, quali sono alcune cause comuni di comportamento indefinito in Java?

In caso contrario, quali funzionalità di Java lo rendono libero da tali comportamenti e perché le ultime versioni di C e C ++ non sono state implementate con queste proprietà?


4
Java è definito in modo molto rigido. Controllare le specifiche del linguaggio Java.


4
@ user1249, il "comportamento indefinito" è in realtà anche definito in modo abbastanza rigido.
Pacerier,


Cosa dice Java quando si viola un "Contratto"? Come accade quando si sovraccarica .equals per essere incompatibile con .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… È colloquialmente indefinito, ma tecnicamente allo stesso modo di C ++?
Mooing Duck,

Risposte:


18

In Java, è possibile considerare indefinito il comportamento del programma sincronizzato in modo errato.

Java 7 JLS utilizza una volta la parola "non definito", in 17.4.8. Esecuzioni e requisiti di causalità :

Usiamo f|dper indicare la funzione data limitando il dominio di fad . Per tutti xin d, f|d(x) = f(x)e per tutti xnon in d, nonf|d(x) è definito ...

La documentazione dell'API Java specifica alcuni casi in cui i risultati non sono definiti - ad esempio, nella data di costruzione (obsoleta) (int year, int month, int day) :

Il risultato non è definito se un determinato argomento è fuori limite ...

Javadocs per ExecutorService.invokeAll (Collection) dichiara:

I risultati di questo metodo non sono definiti se la raccolta data viene modificata mentre questa operazione è in corso ...

Un tipo meno formale di comportamento "non definito" può essere trovato ad esempio in ConcurrentModificationException , dove i documenti API usano il termine "miglior sforzo":

Si noti che il comportamento fail-fast non può essere garantito in quanto, in generale, è impossibile fornire garanzie concrete in presenza di modifiche simultanee non sincronizzate. Le operazioni fail-fast danno ConcurrentModificationExceptionil massimo . Pertanto, sarebbe sbagliato scrivere un programma che dipendesse da questa eccezione per la sua correttezza ...


Appendice

Uno dei commenti alle domande fa riferimento a un articolo di Eric Lippert che fornisce un'introduzione utile in argomenti: comportamento definito dall'implementazione .

Consiglio questo articolo per il ragionamento indipendente dal linguaggio, anche se vale la pena ricordare che l'autore ha come target C #, non Java.

Tradizionalmente diciamo che un linguaggio del linguaggio di programmazione ha un comportamento indefinito se l'uso di quel linguaggio può avere qualche effetto; può funzionare nel modo previsto o può cancellare il disco rigido o arrestare in modo anomalo il computer. Inoltre, l'autore del compilatore non ha l'obbligo di avvisarti del comportamento indefinito. (E in effetti, ci sono alcune lingue in cui i programmi che usano idiomi di "comportamento indefinito" sono consentiti dalle specifiche della lingua per bloccare il compilatore!) ...

Al contrario, un linguaggio che ha un comportamento definito dall'implementazione è un comportamento in cui l'autore del compilatore ha diverse scelte su come implementare la funzione e deve sceglierne una. Come suggerisce il nome, il comportamento definito dall'implementazione è almeno definito. Ad esempio, C # consente a un'implementazione di generare un'eccezione o produrre un valore quando trabocca una divisione intera, ma l'implementazione deve sceglierne una. Non è possibile cancellare il disco rigido ...

Quali sono alcuni dei fattori che portano un comitato di progettazione linguistica a lasciare determinati idiomi linguistici come comportamenti indefiniti o definiti dall'implementazione?

Il primo fattore importante è: sul mercato esistono due implementazioni esistenti che non sono d'accordo sul comportamento di un determinato programma?...

Il prossimo fattore importante è: la funzionalità presenta naturalmente molte diverse possibilità di implementazione, alcune delle quali sono chiaramente migliori di altre? ...

Un terzo fattore è: la funzionalità è così complessa che una suddivisione dettagliata del suo comportamento esatto sarebbe difficile o costosa da specificare? ...

Un quarto fattore è: la funzione comporta un onere elevato per il compilatore da analizzare? ...

Un quinto fattore è: la funzione impone un onere elevato all'ambiente di runtime? ...

Un sesto fattore è: rendere il comportamento definito preclude alcune importanti ottimizzazioni? ...

Questi sono solo alcuni dei fattori che mi vengono in mente; ci sono ovviamente molti, molti altri fattori su cui i comitati di progettazione linguistica discutono prima di creare una funzione "implementazione definita" o "non definita".

Sopra è solo una breve copertura; l'articolo completo contiene spiegazioni ed esempi per i punti menzionati in questo estratto; è molto più lettura vale la pena. Ad esempio, i dettagli forniti per il "sesto fattore" possono fornire uno spaccato della motivazione per molte affermazioni nel modello di memoria Java ( JSR 133 ), aiutando a capire perché alcune ottimizzazioni sono consentite, portando a comportamenti indefiniti mentre altri sono proibiti, portando a limiti come accadono prima e requisiti di causalità .

Nessuno dei materiali dell'articolo è particolarmente nuovo per me, ma sarò dannato se lo avessi mai visto presentato in un modo così elegante, conciso e comprensibile. Sorprendente.


Aggiungerò che il JMM! = Hardware sottostante e il risultato finale di un programma in esecuzione in termini di concorrenza possono variare dal dire un WinIntel contro un Solaris
Martijn Verburg il

2
@MartijnVerburg è un bel punto. L'unico motivo per cui esito a etichettarlo come "indefinito" è che il modello di memoria pone vincoli come accaduto prima e causalità nell'esecuzione di un programma correttamente sincronizzato
moscerino del

È vero, le specifiche definiscono come dovrebbe comportarsi in base al JMM, tuttavia Intel e altri non sono sempre d'accordo ;-)
Martijn Verburg,

@MartijnVerburg Penso che il punto principale di JMM sia prevenire perdite eccessive da produttori di processori "in disaccordo". Per quanto ho capito, Java prima del 5.0 aveva questo tipo di mal di testa con DEC Alpha, quando le scritture speculative fatte sotto il cofano potevano entrare nel programma come "dal nulla" - quindi, il requisito di causalità è andato in JSR 133 (JMM)
gnat

9
@MartinVerburg: è compito dell'implementatore JVM assicurarsi che JVM si comporti in base alle specifiche JLS / JMM su qualsiasi piattaforma hardware supportata. Se hardware diverso si comporta diversamente, è compito dell'implementatore JVM gestirlo ... e farlo funzionare.
Stephen C,

10

In cima alla mia testa, non penso che ci sia un comportamento indefinito in Java, almeno non nello stesso senso di C ++.

La ragione di ciò è che c'è una filosofia diversa dietro Java rispetto a C ++. Un obiettivo progettuale di base di Java era consentire ai programmi di funzionare invariati su più piattaforme, quindi le specifiche definiscono tutto in modo molto esplicito.

Al contrario, un obiettivo di progettazione di base di C e C ++ è l'efficienza: non dovrebbero esserci caratteristiche (inclusa l'indipendenza dalla piattaforma) che costino prestazioni anche se non sono necessarie. A tal fine, la specifica non definisce deliberatamente alcuni comportamenti perché la loro definizione comporterebbe un lavoro extra su alcune piattaforme e ridurrebbe così le prestazioni anche per le persone che scrivono programmi appositamente per una piattaforma e sono consapevoli di tutte le sue idiosincrasie.

C'è anche un esempio in cui Java è stato costretto a introdurre retroattivamente una forma limitata di comportamento indefinito proprio per quel motivo: la parola chiave strictfp è stata introdotta in Java 1.2 per consentire ai calcoli in virgola mobile di deviare dal seguire esattamente lo standard IEEE 754 come precedentemente richiesto dalla specifica , poiché ciò richiedeva un lavoro extra e rendeva più lenti tutti i calcoli in virgola mobile su alcune CPU comuni, mentre in alcuni casi produceva risultati peggiori.


2
Penso che sia importante notare l'altro obiettivo principale di Java: sicurezza e isolamento. Penso che anche questo sia un motivo per la mancanza di comportamenti "indefiniti" (come in C ++).
K.Steff,

3
@ K.Steff: l'ipermoderna C / C ++ è totalmente inadatto per qualsiasi cosa relativa alla sicurezza da remoto. Data int x=-1; foo(); x<<=1;la filosofia ipermoderna favorirebbe la riscrittura in foomodo tale che qualsiasi percorso che non esca debba essere irraggiungibile. Questo, se fooè if (should_launch_missiles) { launch_missiles(); exit(1); }un compilatore, potrebbe (e secondo alcune persone dovrebbe) semplificarlo semplicemente launch_missiles(); exit(1);. L'UB tradizionale era l'esecuzione di codice casuale, ma quella era vincolata dalle leggi del tempo e della causalità. Il nuovo UB migliorato non è vincolato da nessuno dei due.
supercat,

3

Java si sforza piuttosto di sterminare comportamenti indefiniti, proprio a causa delle lezioni di lingue precedenti. Ad esempio, le variabili a livello di classe vengono inizializzate automaticamente; le variabili locali non vengono inizializzate automaticamente per motivi di prestazioni, ma esiste un'analisi sofisticata del flusso di dati per impedire a chiunque di scrivere un programma in grado di rilevarlo. I riferimenti non sono puntatori, quindi non possono esistere riferimenti non validi e dereferenziazionenull provoca un'eccezione specifica.

Naturalmente rimangono alcuni comportamenti che non sono completamente specificati e puoi scrivere programmi inaffidabili se pensi che lo siano. Ad esempio, se si esegue l'iterazione su un normale (non ordinato) Set, la lingua garantisce che vedrai ogni elemento esattamente una volta, ma non nell'ordine in cui li vedrai. L'ordine potrebbe essere lo stesso per le esecuzioni successive o potrebbe cambiare; o potrebbe rimanere lo stesso fintanto che non si verificano altre allocazioni o fintanto che non aggiorni il tuo JDK, ecc. È quasi impossibile sbarazzarsi di tutti questi effetti; per esempio, dovresti ordinare o randomizzare in modo esplicito tutte le operazioni delle Collezioni, e questo semplicemente non vale la piccola non indefinita aggiunta aggiuntiva.


I riferimenti sono puntatori con un altro nome
curiousguy,

@curiousguy - si presume che generalmente i "riferimenti" non consentano l'uso della manipolazione aritmetica del loro valore numerico, che è spesso consentito per i "puntatori". Il primo è quindi un costrutto più sicuro del secondo; combinati con un sistema di gestione della memoria che non consente il riutilizzo della memoria di un oggetto mentre esiste un riferimento valido ad esso, i riferimenti impediscono errori di utilizzo della memoria. I puntatori non possono farlo, anche quando viene utilizzata la gestione della memoria appropriata.
Jules il

@Jules Quindi è una questione di terminologia: puoi chiamare una cosa un puntatore o un riferimento e decidere di utilizzare "riferimento" in linguaggi "sicuri" e "puntatore" in linguaggi che consentano l'uso dell'aritmetica del puntatore e della gestione manuale della memoria. (L'aritmetica del puntatore AFAIK viene eseguita solo in C / C ++.)
curiousguy,

2

Devi capire il "comportamento indefinito" e la sua origine.

Comportamento indefinito significa un comportamento che non è definito dagli standard. C / C ++ ha troppe implementazioni di compilatore diverse e funzionalità aggiuntive. Queste funzionalità aggiuntive hanno legato il codice al compilatore. Questo perché non c'era uno sviluppo del linguaggio centralizzato. Quindi alcune delle funzionalità avanzate di alcuni compilatori sono diventate "comportamenti indefiniti".

Considerando che in Java la specifica del linguaggio è controllata da Sun-Oracle e non c'è nessun altro che cerca di fare specifiche e quindi nessun comportamento indefinito.

Modificato rispondendo specificamente alla domanda

  1. Java è privo di comportamenti indefiniti perché gli standard sono stati creati prima dei compilatori
  2. I compilatori C / C ++ moderni hanno più / meno standardizzato le implementazioni, ma le funzionalità implementate prima della standardizzazione rimangono ancora contrassegnate come "comportamento indefinito" perché ISO ha mantenuto la mamma su questi aspetti.

2
Potresti avere ragione sul fatto che non ci sia UB in Java, ma anche quando un'entità controlla tutto, ci possono essere ragioni per avere UB, quindi il motivo che dai non porta alla conclusione.
AProgrammer,

2
Inoltre, sia C che C ++ sono standardizzati da ISO. Sebbene possano esserci più compilatori, esiste solo uno standard alla volta.
MSalters il

1
@SarvexJatasra, non sono d'accordo che sia l'unica fonte di UB. Ad esempio, un UB sta dereferenziando il puntatore penzolante e ci sono buone ragioni per lasciarlo un UB in qualsiasi lingua che non ha un GC, anche se inizi le tue specifiche adesso. E questi motivi non hanno nulla a che fare con la pratica esistente o i compilatori esistenti.
AProgrammer,

2
@SarvexJatasra, l'overflow firmato è UB perché lo standard lo dice esplicitamente (è anche l'esempio fornito con la definizione di UB). Dereferenziare un puntatore non valido è anche un UB per lo stesso motivo, lo dice lo standard.
Programmatore il

2
@ bames53: nessuno dei vantaggi citati richiederebbe il livello di compilatori hypermodern di latitudine con UB. Con l'eccezione degli accessi di memoria fuori limite e degli overflow dello stack, che possono "naturalmente" indurre l'esecuzione di codice casuale, non riesco a pensare a nessuna ottimizzazione utile che richiederebbe una latitudine più ampia rispetto a dire che la maggior parte delle operazioni UB-ish producono indeterminate valori (che potrebbero comportarsi come se avessero "bit extra") e potrebbero avere conseguenze oltre a ciò se i documenti di un'implementazione si riservano espressamente il diritto di imporli; i documenti possono dare "Comportamento non vincolato" ...
supercat

1

Java elimina essenzialmente tutto il comportamento indefinito trovato in C / C ++. (Ad esempio: overflow di numeri interi firmati, divisione per zero, variabili non inizializzate, dereference puntatore nullo, spostamento superiore alla larghezza dei bit, double-free, anche "nessuna nuova riga alla fine del codice sorgente".) Ma Java ha alcuni oscuri comportamenti indefiniti che si incontrano raramente dai programmatori.

  • Java Native Interface (JNI), un modo per Java di chiamare il codice C o C ++. Esistono molti modi per rovinare JNI, come sbagliare la firma della funzione, effettuare chiamate non valide ai servizi JVM, corrompere la memoria, allocare / liberare cose in modo errato e altro ancora. Ho già commesso questi errori e in genere l'intero JVM si arresta in modo anomalo quando un thread che esegue il codice JNI commette un errore.

  • Thread.stop(), che è obsoleto. Citazione:

    Perché è Thread.stopdeprecato?

    Perché è intrinsecamente pericoloso. L'arresto di un thread provoca lo sblocco di tutti i monitor che ha bloccato. (I monitor vengono sbloccati quando l' ThreadDeatheccezione si propaga nello stack.) Se uno degli oggetti precedentemente protetti da questi monitor era in uno stato incoerente, altri thread ora possono visualizzare questi oggetti in uno stato incoerente. Si dice che tali oggetti siano danneggiati. Quando i thread operano su oggetti danneggiati, può verificarsi un comportamento arbitrario. Questo comportamento può essere sottile e difficile da rilevare o può essere pronunciato. A differenza di altre eccezioni non controllate, ThreadDeathuccide i thread in silenzio; pertanto, l'utente non ha alcun avviso che il suo programma potrebbe essere danneggiato. La corruzione può manifestarsi in qualsiasi momento dopo che si è verificato il danno effettivo, anche ore o giorni in futuro.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.