Java: coisas que podem parecer curiosas para um desenvolvedor experiente

Boa hora do dia!

O artigo foi escrito na sequência da publicação “Coisas que você [talvez] não sabia sobre Java” por outro autor, que eu classificaria como “para iniciantes”. Lendo e comentando, percebi que havia várias coisas interessantes que aprendi, já programando em java por mais de um ano. Talvez essas coisas pareçam curiosas para outra pessoa.

Os fatos que, do meu ponto de vista, podem ser úteis para iniciantes, retirei nos "spoilers". Algumas coisas ainda podem ser interessantes para as mais experientes. Por exemplo, eu mesmo não sabia até o momento em que escrevi que Boolean.hashCode (true) == 1231 e Boolean.hashCode (false) == 1237.

para iniciantes
  • Boolean.hashCode (true) == 1231
  • Boolean.hashCode (false) == 1237
  • Float.hashCode (value) == Float.floatToIntBits (valor)
  • Double.hashCode (value) - xor das primeiras e segundas meias palavras de 32 bits Double.doubleToLongBits (value)

Object.hashCode () não é mais o endereço de um objeto na memória


Isenção de responsabilidade: Este é um detalhe da jvm da Oracle (HotSpot).

Era uma vez isso.
Em jdk1.2.1 / docs / api / java / lang / Object.html # hashCode ():
Tanto quanto for razoavelmente prático, o método hashCode definido pela classe Object retorna números inteiros distintos para objetos distintos. (Isso geralmente é implementado convertendo o endereço interno do objeto em um número inteiro, mas essa técnica de implementação não é requerida pela linguagem de programação JavaTM.)

Então eles recusaram. É o que o javadoc diz para o jdk 12 .

vladimir_dolzhenko sugeriu que o comportamento antigo possa ser restaurado usando -XX: hashCode = 4. E a mudança de comportamento em si era quase da versão java 1.2.

Integer.valueOf (15) == Integer.valueOf (15); Integer.valueOf (128)! = Integer.valueOf (128)


Isenção de responsabilidade: isso faz parte do jls .

É claro que, ao comparar dois wrappers com o operador == (! =), Não ocorre autoboxing. De um modo geral, é a primeira igualdade que confunde. O fato é que, para valores inteiros i: -129 <i <128, os objetos do wrapper inteiro são armazenados em cache. Portanto, para i desse intervalo, Integer.valueOf (i) não cria um novo objeto a cada vez, mas retorna um já criado. Para i que não se enquadram nesse intervalo, Integer.valueOf (i) sempre cria um novo objeto. Portanto, se você não monitorar de perto o que exatamente e como exatamente ele é comparado, você pode escrever um código que pareça funcionar e que esteja coberto de testes, mas ao mesmo tempo contendo esse "rake".

Na jvm do Oracle (HotSpot), o limite superior de armazenamento em cache pode ser alterado através da propriedade "java.lang.Integer.IntegerCache.high" .

em alguns casos, os valores dos campos estáticos finais primitivos ou de seqüência de caracteres de outra classe são resolvidos no tempo de compilação


Parece confuso, e a afirmação é um pouco longa. O significado é esse. Se tivermos uma classe que define constantes de tipos ou cadeias primitivas como campos estáticos finais com inicialização imediata,
class AnotherClass { public final static String CASE_1 = "case_1"; public final static String CASE_2 = "case_2"; } 

quando usado em outras classes,
 class TheClass { // ... public static int getCaseNumber(String caseName) { switch (caseName) { case AnotherClass.CASE_1: return 1; case AnotherClass.CASE_2: return 2; default: throw new IllegalArgumentException("value of the argument caseName does not belong to the allowed value set"); } } } 

os valores dessas constantes ("case_1", "case_2") são resolvidos em tempo de compilação. E eles são inseridos no código como valores, e não como links. Ou seja, se usamos essas constantes da biblioteca e obtemos uma nova versão da biblioteca na qual os valores das constantes foram alteradas, devemos recompilar o projeto. Caso contrário, os valores constantes antigos podem continuar a ser usados ​​no código.

Esse comportamento é observado em todos os locais em que expressões constantes (por exemplo, switch / case) devem ser usadas ou o compilador pode converter expressões em constantes e ele pode fazê-lo.

Esses campos não podem ser usados ​​em expressões constantes assim que removermos a inicialização imediata, transferindo a inicialização para o bloco estático.

para iniciantes

Sob certas condições, o coletor de lixo pode nunca ser executado.


Como resultado, nenhum finalize () será iniciado. Portanto, você não deve escrever um código que dependa do fato de que finalize () sempre funcione. Sim, e se o objeto entrar no lixo antes do final do programa, provavelmente não será coletado pelo coletor.

O método finalize () para um objeto específico pode ser chamado uma vez e apenas uma vez.


Em finalize (), podemos tornar o objeto visível novamente e o coletor de lixo não o "removerá" dessa vez. Quando esse objeto cair no lixo novamente, ele será "compilado" sem chamar finalize (). Se uma exceção for lançada em finalize () e o objeto ainda não estiver visível para ninguém, ele será "montado". Finalize () não será chamado novamente.

o fluxo no qual finalize () será chamado não é conhecido antecipadamente


É garantido apenas que esse encadeamento esteja livre de bloqueios visíveis pelo programa principal.

a presença de um método finalize () substituído em objetos retarda o processo de coleta de lixo


O que há na superfície é a necessidade de verificar novamente a disponibilidade do objeto - uma vez antes de chamar finalize (), uma vez em uma das seguintes execuções de coleta de lixo.

é realmente difícil combater impasses na finalização ()


Em finalize não trivial (), podem ser necessários bloqueios, o que, dadas as especificidades descritas acima, é muito difícil de depurar.

Object.finalize () desde que a versão 9 do java está marcada como obsoleta!


O que não é surpreendente, dadas as especificidades descritas acima.

inicialização clássica singleton preguiçosa: bloqueio duplo necessário


Há um equívoco sobre este tópico de que a seguinte abordagem (verifique novamente o idioma), que parece muito lógica, sempre funciona:
 public class UnsafeDCLFactory { private Singleton instance; public Singleton get() { if (instance == null) { // read 1, check 1 synchronized (this) { if (instance == null) { // read 2, check 2 instance = new Singleton(); } } } return instance; // read 3 } } 

Examinamos se o objeto foi criado (leia 1, marque 1). Se sim, retorne-o. Caso contrário, defina o bloqueio, verifique se o objeto não foi criado, crie o objeto (o bloqueio foi removido) e retorne o objeto.

A abordagem não funciona pelos seguintes motivos. (leia 1, marque 1) e (leia 3) não estão sincronizados. De acordo com o conceito do modelo de memória java, as alterações feitas em outro encadeamento podem não ser visíveis em nosso encadeamento até que sincronizemos. Obrigado mk2 pelo comentário, aqui está a descrição correta do problema:
Sim, read1 e read3 não estão sincronizados, mas o problema não está em outro thread. E o fato de que leituras não sincronizadas podem ser reordenadas, ou seja, read1! = null, mas read3 == null. E, ao mesmo tempo, devido a "instance = new Singleton ();", podemos obter uma referência ao objeto antes que ele tenha sido completamente construído, e esse é realmente um problema de sincronização com outro encadeamento, mas não read1 e read3, mas read3 e acesso para membros da instância.
É tratado adicionando sincronização durante a leitura ou marcando a variável na qual o link para o singleton vive, volátil. (A solução com volátil funciona apenas com java 5+. Antes disso, java tinha um modelo de memória com incerteza nessa situação.) Aqui está uma versão de trabalho (com otimização adicional - a variável local `res 'foi adicionada para reduzir o número de leituras do campo volátil).
 public class SafeLocalDCLFactory { private volatile Singleton instance; public Singleton getInstance() { Singleton res = instance; // read 1 if (res == null) { // check 1 synchronized (this) { res = instance; // read 2 if (res == null) { // check 2 res = new Singleton(); instance = res; } } } return res; } } 

O código é retirado daqui , no site de Alexei Shipilev. Mais detalhes sobre esse problema podem ser encontrados nele.

"Idioma do titular da inicialização sob demanda" - uma inicialização "preguiçosa" muito bonita do singleton


java inicializa classes (objetos de classe) apenas conforme necessário e, é claro, apenas uma vez. E você pode tirar proveito disso! O mecanismo de idioma do titular da inicialização sob demanda faz exatamente isso. (O código é daqui .)
 public class Something { private Something() {} private static class LazyHolder { static final Something INSTANCE = new Something(); } public static Something getInstance() { return LazyHolder.INSTANCE; } } 

A classe LazyHolder será inicializada apenas na primeira vez que Something.getInstance () for chamado. A Jvm garantirá que isso aconteça apenas uma vez e, além disso, com muita eficiência - se a classe já estiver inicializada, não haverá sobrecarga. Assim, LazyHolder.INSTANCE também será inicializado uma vez, "preguiçoso" e seguro para threads.
pedaço de especificação sobre sobrecarga
Se esse procedimento de inicialização for concluído normalmente e o objeto Class estiver totalmente inicializado e pronto para uso, a invocação do procedimento de inicialização não será mais necessária e poderá ser eliminada do código - por exemplo, remendando-o ou regenerando o código .
Fonte

De um modo geral, singletones não são considerados a melhor prática.

O material ainda não acabou. Portanto, se as mãos "chegarem" e o que já foi escrito estiver em demanda, escreverei mais sobre este tópico.

Obrigado pelos comentários construtivos. Vários lugares do artigo foram expandidos graças a sergey-gornostaev , vladimir_dolzhenko , OlehKurpiak , mk2 .

Source: https://habr.com/ru/post/pt482950/


All Articles