Comportamento indefinido em Java

14

Eu estava lendo esta pergunta no SO, que discute algum comportamento indefinido comum em C ++, e me perguntei: o Java também tem um comportamento indefinido?

Se for esse o caso, quais são algumas das causas comuns de comportamento indefinido em Java?

Caso contrário, quais recursos do Java o libertam de tais comportamentos e por que as versões mais recentes do C e C ++ não foram implementadas com essas propriedades?

Oito
fonte
4
Java é muito rigidamente definido. Verifique a especificação da linguagem Java.
4
@ user1249, "comportamento indefinido" também é bastante rígido.
Pacerier
Possível mesmo no SO: stackoverflow.com/questions/376338/…
Ciro Santilli (/)
O que o Java diz sobre quando você viola um "Contrato"? Tal como acontece quando você sobrecarrega. É igual a incompatível com .hashCode? docs.oracle.com/javase/7/docs/api/java/lang/… Isso é coloquialmente indefinido, mas não tecnicamente da mesma maneira que o C ++?
precisa

Respostas:

18

Em Java, você pode considerar indefinido o comportamento do programa sincronizado incorretamente.

O Java 7 JLS usa a palavra "indefinido" uma vez, no 17.4.8. Execuções e requisitos de causalidade :

Usamos f|dpara denotar a função fornecida restringindo o domínio de fpara d. Para todos xem d, f|d(x) = f(x)e para todos que xnão estão d, f|d(x)é indefinido ...

A documentação da API Java especifica alguns casos em que os resultados são indefinidos - por exemplo, no construtor (descontinuado) Date (int year, int month, int day) :

O resultado é indefinido se um determinado argumento estiver fora dos limites ...

Javadocs para o estado ExecutorService.invokeAll (Collection) :

Os resultados deste método são indefinidos se a coleção fornecida for modificada enquanto esta operação estiver em andamento ...

Um tipo menos formal de comportamento "indefinido" pode ser encontrado, por exemplo, em ConcurrentModificationException , em que os documentos da API usam o termo "melhor esforço":

Observe que o comportamento à prova de falhas não pode ser garantido, pois, em geral, é impossível fazer garantias concretas na presença de modificação simultânea não sincronizada. As operações à prova de falhas são realizadas ConcurrentModificationExceptioncom o melhor esforço . Portanto, seria errado escrever um programa que dependesse dessa exceção para sua correção ...


Apêndice

Um dos comentários da pergunta refere-se a um artigo de Eric Lippert, que fornece uma introdução útil aos tópicos: comportamento definido pela implementação .

Eu recomendo este artigo pelo raciocínio independente de idioma, embora valha a pena lembrar que o autor tem como alvo C #, não Java.

Tradicionalmente, dizemos que um idioma da linguagem de programação tem um comportamento indefinido se o uso desse idioma puder ter algum efeito; ele pode funcionar da maneira que você espera, ou pode apagar seu disco rígido ou travar sua máquina. Além disso, o autor do compilador não tem obrigação de avisá-lo sobre o comportamento indefinido. (E, de fato, há algumas linguagens nas quais os programas que usam expressões idiomáticas de "comportamento indefinido" são permitidos pela especificação da linguagem para travar o compilador!) ...

Por outro lado, um idioma que possui comportamento definido pela implementação é o comportamento em que o autor do compilador tem várias opções sobre como implementar o recurso e deve escolher um. Como o nome indica, o comportamento definido pela implementação é pelo menos definido. Por exemplo, o C # permite que uma implementação lance uma exceção ou produza um valor quando uma divisão inteira exceder o limite, mas a implementação deve escolher uma. Não pode apagar o seu disco rígido ...

Quais são alguns dos fatores que levam um comitê de design de idiomas a deixar certos idiomas como comportamentos indefinidos ou definidos pela implementação?

O primeiro fator importante é: existem duas implementações existentes da linguagem no mercado que discordam do comportamento de um programa específico? ...

O próximo fator importante é: o recurso apresenta naturalmente muitas possibilidades diferentes de implementação, algumas das quais são claramente melhores que outras? ...

Um terceiro fator é: o recurso é tão complexo que seria difícil ou caro especificar um detalhamento exato de seu comportamento? ...

Um quarto fator é: o recurso impõe um alto ônus ao compilador para analisar? ...

Um quinto fator é: o recurso impõe uma carga alta ao ambiente de tempo de execução? ...

Um sexto fator é: fazer o comportamento definido impede alguma otimização importante? ...

Esses são apenas alguns fatores que vêm à mente; é claro que existem muitos outros fatores que os comitês de design de idiomas debatem antes de tornar um recurso "implementação definida" ou "indefinida".

Acima é apenas uma cobertura muito breve; o artigo completo contém explicações e exemplos para os pontos mencionados neste trecho; é muito vale a pena ler. Por exemplo, detalhes fornecidos para o "sexto fator" podem fornecer uma visão da motivação de muitas instruções no Java Memory Model ( JSR 133 ), ajudando a entender por que algumas otimizações são permitidas, levando a um comportamento indefinido enquanto outras são proibidas, levando a limitações como acontecer antes e requisitos de causalidade .

Nenhum dos materiais do artigo é particularmente novo para mim, mas ficarei condenado se o tiver visto de uma maneira tão elegante, consistente e compreensível. Surpreendente.

mosquito
fonte
Vou acrescentar que a JMM = subjacente hardware e o resultado final de um programa em execução com relação à concorrência pode variar de dizer uma WinIntel vs um Solaris!
Martijn Verburg
2
@MartijnVerburg esse é um ponto muito bom. Única razão pela qual eu hesite em marcá-lo como "indefinido" é que representa modelo de memória restrições gostar acontecer, antes e causalidade na execução de programa corretamente sincronizados
mosquito
É verdade que os define especificações como ele deve se comportar sob a JMM, no entanto, a Intel et al nem sempre concordam ;-)
Martijn Verburg
@MartijnVerburg Acho que o ponto principal do JMM é evitar vazamentos otimizados demais de "discordar" dos fabricantes de processadores. Até onde eu entendo Java antes da versão 5.0, esse tipo de dor de cabeça com o DEC Alpha, quando gravações especulativas feitas sob o capô podiam vazar para um programa como "do nada" - portanto, o requisito de causalidade foi para o JSR 133 (JMM)
gnat
9
@MartinVerburg - é um trabalho do implementador da JVM garantir que a JVM se comporte de acordo com a especificação JLS / JMM em qualquer plataforma de hardware suportada. Se um hardware diferente se comportar de maneira diferente, é tarefa do implementador da JVM lidar com ele ... e fazê-lo funcionar.
Stephen C
10

Em primeiro lugar, não acho que exista um comportamento indefinido em Java, pelo menos não no mesmo sentido que em C ++.

A razão para isso é que existe uma filosofia diferente por trás do Java e por trás do C ++. Um dos principais objetivos do design do Java era permitir que os programas rodassem inalterados entre as plataformas, por isso a especificação define tudo muito explicitamente.

Por outro lado, uma das principais metas de design do C e C ++ é a eficiência: não deve haver nenhum recurso (incluindo independência da plataforma) que custe o desempenho, mesmo que você não precise deles. Para esse fim, a especificação deliberadamente não define alguns comportamentos, pois defini-los causaria trabalho extra em algumas plataformas e, assim, reduziria o desempenho, mesmo para pessoas que escrevem programas especificamente para uma plataforma e conhecem todas as suas idiossincrasias.

Existe até um exemplo em que o Java foi forçado a introduzir retroativamente uma forma limitada de comportamento indefinido exatamente por esse motivo: a palavra-chave strictfp foi introduzida no Java 1.2 para permitir que os cálculos de ponto flutuante se desviem de seguir exatamente o padrão IEEE 754, conforme exigido anteriormente pelas especificações. , porque isso exigia trabalho extra e tornava todos os cálculos de ponto flutuante mais lentos em algumas CPUs comuns, enquanto produzia resultados piores em alguns casos.

Michael Borgwardt
fonte
2
Eu acho importante observar o outro objetivo principal do Java: segurança e isolamento. Eu acho que isso também é uma razão para a falta de comportamento "indefinido" (como em C ++).
K.Steff
3
@ K.Steff: C / C ++ hiper-moderno é totalmente inadequado para qualquer coisa remotamente relacionada à segurança. Dada int x=-1; foo(); x<<=1;a filosofia hipermoderna favoreceria a reescrita, de foomodo que qualquer caminho que não saia deve ser inacessível. Isso, se foofor if (should_launch_missiles) { launch_missiles(); exit(1); }um compilador, poderia (e de acordo com algumas pessoas) simplificar isso de maneira simples launch_missiles(); exit(1);. O UB tradicional era a execução aleatória de código, mas isso costumava ser limitado pelas leis do tempo e da causalidade. O novo UB aprimorado não está vinculado a nenhum dos dois.
supercat
3

Java tenta bastante exterminar comportamentos indefinidos, precisamente por causa das lições das linguagens anteriores. Por exemplo, variáveis ​​em nível de classe são inicializadas automaticamente; as variáveis ​​locais não são inicializadas automaticamente por motivos de desempenho, mas há uma sofisticada análise de fluxo de dados para impedir que alguém escreva um programa capaz de detectar isso. As referências não são ponteiros, portanto, referências inválidas não podem existir e a desreferenciação nullcausa uma exceção específica.

Obviamente, existem alguns comportamentos que não são totalmente especificados e você pode escrever programas não confiáveis, se assumir que são. Por exemplo, se você iterar sobre um normal (não classificado) Set, o idioma garante que você verá cada elemento exatamente uma vez, mas não na ordem em que os verá. A ordem pode ser a mesma em execuções sucessivas ou pode mudar; ou pode permanecer o mesmo desde que nenhuma outra alocação ocorra, ou desde que você não atualize seu JDK etc. É quase impossível se livrar de todos esses efeitos; por exemplo, você precisaria ordenar ou aleatoriamente explicitamente todas as operações de coleções, e isso simplesmente não vale o pequeno adicional indefinido.

Kilian Foth
fonte
As referências são indicadores com outro nome
curiousguy
@curiousguy - geralmente se supõe que "referências" não permitam o uso de manipulação aritmética de seu valor numérico, o que geralmente é permitido para "ponteiros". O primeiro é, portanto, uma construção mais segura do que o segundo; combinado com um sistema de gerenciamento de memória que não permite que o armazenamento de um objeto seja reutilizado enquanto existir uma referência válida a ele, as referências evitam erros de uso de memória. Os ponteiros não podem fazer isso, mesmo quando o gerenciamento de memória apropriado é usado.
Jules
@Jules Então é uma questão de terminologia: você pode chamar uma coisa de ponteiro ou referência e decidir usar "referência" em idiomas "seguros" e "ponteiro" em idiomas que permitem o uso da aritmética dos ponteiros e do gerenciamento manual de memória. (AFAIK "ponteiro aritmética" só é feito em C / C ++.)
curiousguy
2

Você precisa entender o "comportamento indefinido" e sua origem.

Comportamento indefinido significa um comportamento que não é definido pelos padrões. O C / C ++ possui muitas implementações diferentes de compilador e recursos adicionais. Esses recursos adicionais vincularam o código ao compilador. Isso ocorreu porque não havia desenvolvimento de linguagem centralizado. Portanto, alguns dos recursos avançados de alguns compiladores se tornaram "comportamentos indefinidos".

Enquanto em Java a especificação da linguagem é controlada pela Sun-Oracle e não há mais ninguém tentando fazer especificações e, portanto, nenhum comportamento indefinido.

Editado Respondendo especificamente à pergunta

  1. Java está livre de comportamentos indefinidos porque os padrões foram criados antes dos compiladores
  2. Os compiladores C / C ++ modernos padronizaram mais / menos as implementações, mas os recursos implementados antes da padronização ainda permanecem marcados como "comportamento indefinido", porque a ISO se manteve atenta a esses aspectos.
Sarvex
fonte
2
Você pode estar certo de que não há UB em Java, mas mesmo quando uma entidade controla tudo, pode haver razões para ter UB, portanto, o motivo que você fornece não leva à conclusão.
AProgrammer
2
Além disso, C e C ++ são padronizados pela ISO. Embora possa haver vários compiladores, há apenas um padrão por vez.
MSalters
1
@SarvexJatasra, não concordo que seja a única fonte de UB. Por exemplo, um UB está desreferenciando o ponteiro oscilante e há boas razões para deixá-lo um UB em qualquer idioma que não tenha um GC, mesmo se você iniciar suas especificações agora. E esses motivos não têm nada a ver com a prática ou compiladores existentes.
AProgrammer
2
@SarvexJatasra, o overflow assinado é UB porque o padrão diz explicitamente (é até o exemplo dado com a definição de UB). Desreferenciar um ponteiro inválido também é um UB pelo mesmo motivo, segundo o padrão.
AProgrammer
2
@ bames53: Nenhuma das vantagens citadas exigiria o nível de compilação hipermoderna da latitude com o UB. Com as exceções de acessos de memória fora dos limites e estouros de pilha, que podem "naturalmente" induzir a execução aleatória de código, não consigo pensar em nenhuma otimização útil que exija latitude mais ampla do que dizer que a maioria das operações UB-ish produz indeterminada valores (que podem se comportar como se tivessem "bits extras") e só podem ter consequências além disso se os documentos de uma implementação se reservarem expressamente o direito de impor tais; documentos podem dar "comportamento irrestrito" ...
supercat 15/15
1

Java elimina essencialmente todo o comportamento indefinido encontrado em C / C ++. (Por exemplo: Estouro de número inteiro assinado, divisão por zero, variáveis ​​não inicializadas, dereferência de ponteiro nulo, deslocamento acima da largura de bit, liberação dupla, até mesmo "nenhuma nova linha no final do código-fonte".) Mas o Java tem alguns comportamentos indefinidos obscuros que raramente são encontrados pelos programadores.

  • Java Native Interface (JNI), uma maneira de o Java chamar código C ou C ++. Há muitas maneiras de estragar a JNI, como errar a assinatura da função, fazer chamadas inválidas para os serviços da JVM, corromper a memória, alocar / liberar coisas incorretamente e muito mais. Eu cometi esses erros antes e geralmente a JVM inteira falha quando qualquer thread que executa o código JNI comete um erro.

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

    Por que está Thread.stopobsoleto?

    Porque é inerentemente inseguro. A interrupção de um encadeamento faz com que ele desbloqueie todos os monitores bloqueados. (Os monitores são desbloqueados comoThreadDeath exceção se propaga pela pilha.) Se algum dos objetos anteriormente protegidos por esses monitores estivesse em um estado inconsistente, outros encadeamentos agora poderão exibir esses objetos em um estado inconsistente. Dizem que esses objetos estão danificados. Quando threads operam em objetos danificados, pode resultar em comportamento arbitrário. Esse comportamento pode ser sutil e difícil de detectar ou pode ser pronunciado. Diferente de outras exceções não verificadas, ThreadDeathmata os threads silenciosamente; portanto, o usuário não tem aviso de que seu programa pode estar corrompido. A corrupção pode se manifestar a qualquer momento após o dano real, mesmo horas ou dias no futuro.

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

Nayuki
fonte