Comportamiento indefinido en Java

14

Estaba leyendo esta pregunta en SO, que analiza algunos comportamientos indefinidos comunes en C ++, y me preguntaba: ¿Java también tiene un comportamiento indefinido?

Si ese es el caso, ¿cuáles son algunas causas comunes de comportamiento indefinido en Java?

Si no, ¿qué características de Java lo liberan de tales comportamientos y por qué no se han implementado las últimas versiones de C y C ++ con estas propiedades?

Ocho
fuente
44
Java está muy rígidamente definido. Verifique la especificación del lenguaje Java.
44
@ user1249, el "comportamiento indefinido" también está definido de manera bastante rígida.
Pacerier
Posible lo mismo en SO: stackoverflow.com/questions/376338/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
¿Qué dice Java sobre cuando viola un "Contrato"? ¿Como sucede cuando sobrecargas .equals para ser incompatible con .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… ¿Está coloquialmente indefinido, pero no técnicamente de la misma manera que C ++?
Mooing Duck

Respuestas:

18

En Java, puede considerar el comportamiento del programa sincronizado incorrectamente indefinido.

Java 7 JLS usa la palabra "indefinido" una vez, en 17.4.8. Ejecuciones y requisitos de causalidad :

Usamos f|dpara denotar la función dada restringiendo el dominio de fto d. Para todos xen d, f|d(x) = f(x)y para todos los que xno están d, nof|d(x) está definido ...

La documentación de la API de Java especifica algunos casos en que los resultados no están definidos, por ejemplo, en la fecha del constructor (en desuso) (int año, int mes, int día) :

El resultado no está definido si un argumento dado está fuera de los límites ...

Javadocs para el estado ExecutorService.invokeAll (Collection) :

Los resultados de este método no están definidos si la colección dada se modifica mientras esta operación está en progreso ...

Se puede encontrar un tipo menos formal de comportamiento "indefinido", por ejemplo, en ConcurrentModificationException , donde los documentos API utilizan el término "mejor esfuerzo":

Tenga en cuenta que el comportamiento a prueba de fallas no puede garantizarse ya que, en términos generales, es imposible hacer garantías duras en presencia de modificaciones concurrentes no sincronizadas. Las operaciones a prueba de fallas se basan ConcurrentModificationExceptionen el mejor esfuerzo . Por lo tanto, sería un error escribir un programa que dependiera de esta excepción para su corrección ...


Apéndice

Uno de los comentarios de las preguntas se refiere a un artículo de Eric Lippert que proporciona una introducción útil a los temas: comportamiento definido por la implementación .

Recomiendo este artículo para el razonamiento independiente del lenguaje, aunque vale la pena tener en cuenta que el autor se dirige a C #, no a Java.

Tradicionalmente decimos que un idioma de lenguaje de programación tiene un comportamiento indefinido si el uso de ese idioma puede tener algún efecto; puede funcionar de la manera que espera o puede borrar su disco duro o bloquear su máquina. Además, el autor del compilador no tiene la obligación de advertirle sobre el comportamiento indefinido. (¡Y de hecho, hay algunos lenguajes en los que la especificación del lenguaje permite que los programas que usan modismos de "comportamiento indefinido" bloqueen el compilador!) ...

Por el contrario, un idioma que tiene un comportamiento definido por la implementación es aquel en el que el autor del compilador tiene varias opciones sobre cómo implementar la característica y debe elegir una. Como su nombre lo indica, el comportamiento definido por la implementación está al menos definido. Por ejemplo, C # permite que una implementación arroje una excepción o produzca un valor cuando una división entera se desborda, pero la implementación debe elegir una. No puede borrar su disco duro ...

¿Cuáles son algunos de los factores que llevan a un comité de diseño del lenguaje a dejar ciertas expresiones idiomáticas como comportamientos indefinidos o definidos por la implementación?

El primer factor importante es: ¿hay dos implementaciones existentes del lenguaje en el mercado que no están de acuerdo con el comportamiento de un programa en particular? ...

El siguiente factor importante es: ¿la característica presenta naturalmente muchas posibilidades diferentes para la implementación, algunas de las cuales son claramente mejores que otras? ...

Un tercer factor es: ¿ es la característica tan compleja que sería difícil o costoso especificar un desglose detallado de su comportamiento exacto? ...

Un cuarto factor es: ¿la función impone una gran carga al compilador para analizar? ...

Un quinto factor es: ¿la característica impone una gran carga en el entorno de tiempo de ejecución? ...

Un sexto factor es: ¿definir el comportamiento impide alguna optimización importante? ...

Esos son solo algunos factores que vienen a la mente; Por supuesto, hay muchos, muchos otros factores que los comités de diseño de lenguaje debaten antes de hacer una característica "implementación definida" o "indefinida".

Lo anterior es solo una cobertura muy breve; El artículo completo contiene explicaciones y ejemplos de los puntos mencionados en este extracto; es mucho vale la pena leer. Por ejemplo, los detalles dados para el "sexto factor" pueden dar una idea de la motivación para muchas declaraciones en el Modelo de memoria Java ( JSR 133 ), lo que ayuda a comprender por qué se permiten algunas optimizaciones, lo que lleva a un comportamiento indefinido mientras que otras están prohibidas, lo que lleva a limitaciones como suceder antes y requisitos de causalidad .

Ninguno de los materiales del artículo es particularmente nuevo para mí, pero me condenaría si alguna vez lo viera presentado de una manera tan elegante, concisa y comprensible. Increíble.

mosquito
fuente
Agregaré que el hardware subyacente JMM! = Y el resultado final de un programa en ejecución con respecto a la concurrencia pueden variar de, por ejemplo, un WinIntel frente a un Solaris
Martijn Verburg
2
@MartijnVerburg ese es un muy buen punto. Sólo por lo que me atrevo a etiquetar como "indefinido" es que las limitaciones de Actitudes del modelo de memoria como ocurrirá antes y la causalidad en la ejecución del programa correctamente sincronizado
mosquito
Es cierto que la especificación define cómo debería comportarse bajo el JMM, sin embargo, Intel y otros no siempre están de acuerdo ;-)
Martijn Verburg
@MartijnVerburg Creo que el punto principal de JMM es evitar que las fugas de exceso de optimización "no estén de acuerdo" con los fabricantes de procesadores. En lo que a entender de Java antes de 5.0 tenía este tipo de dolor de cabeza con DEC Alpha, cuando escribe especulativas realizadas bajo el capó podrían filtrarse en programa como "de la nada" - por lo tanto, la causalidad requisito entró en JSR 133 (JMM)
mosquito
99
@MartinVerburg: el trabajo de un implementador de JVM es asegurarse de que el JVM se comporte de acuerdo con las especificaciones JLS / JMM en cualquier plataforma de hardware compatible. Si un hardware diferente se comporta de manera diferente, es el trabajo del implementador de JVM lidiar con él ... y hacerlo funcionar.
Stephen C
10

Fuera de mi cabeza, no creo que haya ningún comportamiento indefinido en Java, al menos no en el mismo sentido que en C ++.

La razón de esto es que hay una filosofía diferente detrás de Java que detrás de C ++. Un objetivo de diseño central de Java era permitir que los programas se ejecuten sin cambios en las plataformas, por lo que la especificación define todo de manera muy explícita.

En contraste, un objetivo central de diseño de C y C ++ es la eficiencia: no debe haber ninguna característica (incluida la independencia de la plataforma) que cueste el rendimiento, incluso si no las necesita. Con este fin, la especificación deliberadamente no define algunos comportamientos porque definirlos causaría un trabajo adicional en algunas plataformas y, por lo tanto, reduciría el rendimiento incluso para las personas que escriben programas específicamente para una plataforma y conocen todas sus idiosincrasias.

Incluso hay un ejemplo en el que Java se vio obligado a introducir retroactivamente una forma limitada de comportamiento indefinido por exactamente esa razón: la palabra clave estricta fp se introdujo en Java 1.2 para permitir que los cálculos de coma flotante se desviaran de seguir exactamente el estándar IEEE 754 como la especificación había exigido previamente , porque hacerlo requería trabajo adicional e hizo que todos los cálculos de punto flotante fueran más lentos en algunas CPU comunes, mientras que en realidad producía peores resultados en algunos casos.

Michael Borgwardt
fuente
2
Creo que es importante tener en cuenta el otro objetivo principal de Java: seguridad y aislamiento. Creo que esto también es una razón para la falta de comportamiento 'indefinido' (como en C ++).
K.Steff
3
@ K.Steff: C / C ++ hipermoderno es totalmente inadecuado para cualquier cosa remotamente relacionada con la seguridad. Dada int x=-1; foo(); x<<=1;la filosofía hipermoderna favorecería la reescritura foopara que cualquier camino que no salga sea inalcanzable. Esto, si se footrata de if (should_launch_missiles) { launch_missiles(); exit(1); }un compilador, podría (y según algunas personas debería) simplificar eso simplemente launch_missiles(); exit(1);. El UB tradicional era la ejecución de código aleatorio, pero eso solía estar sujeto a las leyes del tiempo y la causalidad. Nueva UB mejorada no está vinculada por ninguno de los dos.
supercat
3

Java se esfuerza bastante por exterminar el comportamiento indefinido, precisamente debido a las lecciones de los lenguajes anteriores. Por ejemplo, las variables de nivel de clase se inicializan automáticamente; Las variables locales no se inicializan automáticamente por razones de rendimiento, pero hay un sofisticado análisis de flujo de datos para evitar que alguien escriba un programa que pueda detectar esto. Las referencias no son punteros, por lo que no pueden existir referencias no válidas, y la desreferenciación nullcausa una excepción específica.

Por supuesto, quedan algunos comportamientos que no están completamente especificados, y puede escribir programas poco confiables si supone que sí. Por ejemplo, si itera sobre un valor normal (no ordenado) Set, el lenguaje garantiza que verá cada elemento exactamente una vez, pero no en qué orden los verá. El orden puede ser el mismo en ejecuciones sucesivas o puede cambiar; o puede permanecer igual mientras no ocurran otras asignaciones, o mientras no actualice su JDK, etc. Es casi imposible deshacerse de todos esos efectos; por ejemplo, tendrías que ordenar o aleatorizar explícitamente todas las operaciones de Colecciones, y eso simplemente no vale la pena la pequeña indefinición adicional.

Kilian Foth
fuente
Las referencias son punteros con otro nombre
curiosoguy
@curiousguy: generalmente se supone que las "referencias" no permiten el uso de la manipulación aritmética de su valor numérico, que a menudo se permite para "punteros". La primera es, por lo tanto, una construcción más segura que la segunda; combinado con un sistema de administración de memoria que no permite que el almacenamiento de un objeto se reutilice mientras exista una referencia válida, las referencias evitan errores de uso de memoria. Los punteros no pueden hacerlo, incluso cuando se utiliza la gestión de memoria adecuada.
Julio
@Jules Entonces es una cuestión de terminología: puede llamar a una cosa puntero o referencia, y decidir usar "referencia" en idiomas "seguros" y "puntero" en idiomas que permitan el uso de aritmética de puntero y administración manual de memoria. (AFAIK "puntero aritmético" solo se hace en C / C ++.)
curioso
2

Tienes que entender el "Comportamiento indefinido" y su origen.

Comportamiento indefinido significa un comportamiento que no está definido por los estándares. C / C ++ tiene demasiadas implementaciones de compilador diferentes y características adicionales. Estas características adicionales vinculaban el código al compilador. Esto se debió a que no había un desarrollo centralizado del lenguaje. Por lo tanto, algunas de las funciones avanzadas de algunos de los compiladores se convirtieron en "comportamientos indefinidos".

Mientras que en Java la especificación del lenguaje está controlada por Sun-Oracle y nadie más está tratando de hacer especificaciones y, por lo tanto, no hay comportamientos indefinidos.

Editado específicamente respondiendo la pregunta

  1. Java está libre de comportamientos indefinidos porque los estándares se crearon antes que los compiladores
  2. Los compiladores modernos de C / C ++ han estandarizado más / menos las implementaciones, pero las características implementadas antes de la estandarización aún permanecen etiquetadas como "comportamiento indefinido" porque ISO mantuvo la calma en estos aspectos.
Sarvex
fuente
2
Puede tener razón en que no hay UB en Java, pero incluso cuando una entidad controla todo, puede haber razones para tener UB, por lo que la razón que da no lleva a la conclusión.
Programador
2
Además, tanto C como C ++ están estandarizados por ISO. Si bien puede haber múltiples compiladores, solo hay un estándar a la vez.
MSalters
1
@SarvexJatasra, no estoy de acuerdo con que sea la única fuente de UB. Por ejemplo, un UB está desreferenciando el puntero colgante y hay buenas razones para dejarlo como UB en cualquier idioma que no tenga un GC, incluso si comienza su especificación ahora. Y esas razones no tienen nada que ver con la práctica existente o los compiladores existentes.
Programador
2
@SarvexJatasra, el desbordamiento firmado es UB porque el estándar lo dice explícitamente (incluso es el ejemplo dado con la definición de UB). Anular la referencia a un puntero no válido también es un UB por la misma razón, el estándar lo dice.
Programador
2
@ bames53: ninguna de las ventajas citadas requeriría el nivel de latitud que los compiladores hipermodernos están tomando con UB. Con las excepciones de accesos de memoria fuera de límites y desbordamientos de pila, que pueden "naturalmente" inducir la ejecución aleatoria de código, no puedo pensar en ninguna optimización útil que requiera una latitud más amplia que decir que la mayoría de las operaciones UB-ish producen indeterminado valores (que podrían comportarse como si tuvieran "bits adicionales") y solo pueden tener consecuencias más allá de eso si los documentos de una implementación se reservan expresamente el derecho de imponerlos; documentos pueden dar "Comportamiento sin restricciones" ...
supercat
1

Java elimina esencialmente todo el comportamiento indefinido que se encuentra en C / C ++. (Por ejemplo: desbordamiento de entero firmado, división por cero, variables no inicializadas, desreferencia de puntero nulo, desplazamiento de más de ancho de bit, doble libre, incluso "sin nueva línea al final del código fuente".) Pero Java tiene algunos comportamientos oscuros indefinidos que rara vez se encuentran con los programadores.

  • Java Native Interface (JNI), una forma para que Java llame a código C o C ++. Hay muchas formas de fastidiar en JNI, como hacer que la firma de la función sea incorrecta, realizar llamadas no válidas a los servicios de JVM, corromper la memoria, asignar / liberar cosas incorrectamente y más. He cometido estos errores antes y, en general, toda la JVM se bloquea cuando un subproceso que ejecuta código JNI comete un error.

  • Thread.stop(), que está en desuso. Citar:

    ¿Por qué está en Thread.stopdesuso?

    Porque es inherentemente inseguro. Al detener un subproceso, se desbloquean todos los monitores que ha bloqueado. (Los monitores se desbloquean ya que la ThreadDeathexcepción se propaga por la pila). Si alguno de los objetos previamente protegidos por estos monitores estaba en un estado inconsistente, otros subprocesos ahora pueden ver estos objetos en un estado inconsistente. Se dice que tales objetos están dañados. Cuando los hilos operan en objetos dañados, puede producirse un comportamiento arbitrario. Este comportamiento puede ser sutil y difícil de detectar, o puede ser pronunciado. A diferencia de otras excepciones no verificadas, ThreadDeathmata los hilos en silencio; por lo tanto, el usuario no tiene ninguna advertencia de que su programa puede estar dañado. La corrupción puede manifestarse en cualquier momento después de que ocurra el daño real, incluso horas o días en el futuro.

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

Nayuki
fuente