JavaRush /Blogue Java /Random-PT /FindBugs ajuda você a aprender Java melhor
articles
Nível 15

FindBugs ajuda você a aprender Java melhor

Publicado no grupo Random-PT
Os analisadores de código estático são populares porque ajudam a encontrar erros cometidos por descuido. Mas o que é muito mais interessante é que ajudam a corrigir erros cometidos por ignorância. Mesmo que tudo esteja escrito na documentação oficial da linguagem, não é fato que todos os programadores a leram com atenção. E os programadores podem entender: você estará cansado de ler toda a documentação. Nesse sentido, um analisador estático é como um amigo experiente que se senta ao seu lado e observa você escrever o código. Ele não apenas diz: “Foi aqui que você cometeu um erro ao copiar e colar”, mas também diz: “Não, você não pode escrever assim, veja você mesmo a documentação”. Esse amigo é mais útil do que a documentação em si, porque ele sugere apenas as coisas que você realmente encontra em seu trabalho e silencia sobre aquelas que nunca serão úteis para você. Neste post, falarei sobre alguns dos meandros do Java que aprendi usando o analisador estático FindBugs. Talvez algumas coisas sejam inesperadas para você também. É importante que todos os exemplos não sejam especulativos, mas sim baseados em código real.

Operador ternário?:

Parece que não existe nada mais simples que o operador ternário, mas ele tem suas armadilhas. Achei que não havia diferença fundamental entre os designs Type var = condition ? valTrue : valFalse; e Type var; if(condition) var = valTrue; else var = valFalse; descobri que havia uma sutileza aqui. Como o operador ternário pode fazer parte de uma expressão complexa, seu resultado deve ser um tipo concreto determinado em tempo de compilação. Portanto, digamos, com uma condição verdadeira no formato if, o compilador leva valTrue diretamente ao tipo Type, e na forma de um operador ternário, primeiro leva ao tipo comum valTrue e valFalse (apesar do fato de que valFalse não é avaliado) e então o resultado leva ao tipo Type. As regras de conversão não são totalmente triviais se a expressão envolver tipos primitivos e wrappers sobre eles (Integer, Double, etc.) Todas as regras são descritas em detalhes em JLS 15.25. Vejamos alguns exemplos. Number n = flag ? new Integer(1) : new Double(2.0); O que acontecerá com n se o sinalizador estiver definido? Um objeto Double com valor 1,0. O compilador acha engraçadas nossas tentativas desajeitadas de criar um objeto. Como o segundo e o terceiro argumentos são wrappers sobre diferentes tipos primitivos, o compilador os desembrulha e resulta em um tipo mais preciso (neste caso, double). E após executar o operador ternário para a atribuição, o boxe é executado novamente. Essencialmente o código é equivalente a isto: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); Do ​​ponto de vista do compilador, o código não contém problemas e compila perfeitamente. Mas FindBugs dá um aviso:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: O valor primitivo é retirado da caixa e coagido para o operador ternário em TestTernary.main (String []) Um valor primitivo agrupado é retirado da caixa e convertido em outro tipo primitivo como parte da avaliação de um operador ternário condicional (o operador b? e1: e2 ). A semântica do Java determina que se e1 e e2 forem valores numéricos agrupados, os valores serão retirados da caixa e convertidos/coagidos para seu tipo comum (por exemplo, se e1 for do tipo Integer e e2 for do tipo Float, então e1 será desempacotado, convertido em um valor de ponto flutuante e em caixa. Consulte JLS Seção 15.25. Claro, FindBugs também avisa que Integer.valueOf(1) é mais eficiente que new Integer(1), mas todo mundo já sabe disso.
Ou este exemplo: Integer n = flag ? 1 : null; O autor deseja colocar nulo em n se o sinalizador não estiver definido. Você acha que irá funcionar? Sim. Mas vamos complicar: Integer n = flag1 ? 1 : flag2 ? 2 : null; parece que não há muita diferença. No entanto, agora, se ambos os sinalizadores estiverem limpos, esta linha lança um NullPointerException. As opções para o operador ternário correto são int e null, portanto o tipo de resultado é Integer. As opções da esquerda são int e Integer, portanto, de acordo com as regras Java, o resultado é int. Para fazer isso, você precisa realizar o unboxing chamando intValue, que lança uma exceção. O código é equivalente a este: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Aqui FindBugs produz duas mensagens, que são suficientes para suspeitar de um erro:
BX_UNBOXING_IMMEDIATELY_REBOXED: O valor encaixotado é retirado da caixa e imediatamente reboxeado em TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Possível desreferência de ponteiro nulo de nulo em TestTernary.main(String[]) Existe um ramo da instrução que, se executado, garante que um o valor nulo será desreferenciado, o que geraria uma NullPointerException quando o código fosse executado.
Bem, um último exemplo sobre este tópico: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Não é surpreendente que este código não funcione: como uma função que retorna um tipo primitivo pode retornar nulo? Surpreendentemente, ele compila sem problemas. Bem, você já entendeu por que ele compila.

Formato de data

Para formatar datas e horas em Java, é recomendado utilizar classes que implementem a interface DateFormat. Por exemplo, é assim: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Muitas vezes uma classe usa o mesmo formato repetidamente. Muitas pessoas terão a ideia de otimização: por que criar um objeto de formato sempre que você pode usar uma instância comum? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } É tão lindo e legal, mas infelizmente não funciona. Mais precisamente, funciona, mas ocasionalmente quebra. O fato é que a documentação do DateFormat diz:
Os formatos de data não são sincronizados. Recomenda-se criar instâncias de formato separadas para cada thread. Se vários threads acessarem um formato simultaneamente, ele deverá ser sincronizado externamente.
E isso é verdade se você observar a implementação interna do SimpleDateFormat. Durante a execução do método format(), o objeto grava nos campos da classe, portanto, o uso simultâneo de SimpleDateFormat de dois threads levará a um resultado incorreto com alguma probabilidade. Aqui está o que FindBugs escreve sobre isso:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Chamada para o método de java.text.DateFormat estático em TestDate.getDate() Como afirma o JavaDoc, DateFormats são inerentemente inseguros para uso multithread. O detector encontrou uma chamada para uma instância de DateFormat que foi obtida por meio de um campo estático. Isso parece suspeito. Para obter mais informações sobre isso, consulte Sun Bug #6231579 e Sun Bug #6178997.

Armadilhas do BigDecimal

Tendo aprendido que a classe BigDecimal permite armazenar números fracionários de precisão arbitrária, e vendo que ela possui um construtor para double, alguns decidirão que está tudo claro e você pode fazer assim: System.out.println(new BigDecimal( 1.1)); Ninguém realmente proíbe fazer isso, mas o resultado pode parecer inesperado: 1.10000000000000088817841970012523233890533447265625. Isso acontece porque o duplo primitivo é armazenado no formato IEEE754, no qual é impossível representar 1,1 com perfeita precisão (no sistema numérico binário obtém-se uma fração periódica infinita). Portanto, o valor mais próximo de 1,1 é armazenado ali. Pelo contrário, o construtor BigDecimal(double) funciona exatamente: ele converte perfeitamente um determinado número no IEEE754 para a forma decimal (uma fração binária final é sempre representável como um decimal final). Se você deseja representar exatamente 1,1 como um BigDecimal, você pode escrever new BigDecimal("1.1") ou BigDecimal.valueOf(1.1). Se você não exibir o número imediatamente, mas fizer algumas operações com ele, poderá não entender de onde vem o erro. FindBugs emite um aviso DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, que dá o mesmo conselho. Aqui está outra coisa: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); na verdade, d1 e d2 representam o mesmo número, mas igual retorna falso porque compara não apenas o valor dos números, mas também a ordem atual (o número de casas decimais). Isso está escrito na documentação, mas poucas pessoas lerão a documentação de um método tão familiar como igual. Tal problema pode não surgir imediatamente. O próprio FindBugs, infelizmente, não avisa sobre isso, mas existe uma extensão popular para ele - fb-contrib, que leva esse bug em consideração:
MDM_BIGDECIMAL_EQUALS equals() sendo chamado para comparar dois números java.math.BigDecimal. Isso normalmente é um erro, pois dois objetos BigDecimal só são iguais se forem iguais em valor e escala, de modo que 2,0 não é igual a 2,00. Para comparar objetos BigDecimal quanto à igualdade matemática, use compareTo().

Quebras de linha e printf

Freqüentemente, os programadores que mudam para Java depois de C ficam felizes em descobrir PrintStream.printf (bem como PrintWriter.printf , etc.). Tipo, ótimo, eu sei que, assim como em C, você não precisa aprender nada novo. Na verdade existem diferenças. Um deles está nas traduções de linha. A linguagem C possui uma divisão em fluxos de texto e binários. A saída do caracter '\n' para um fluxo de texto por qualquer meio será automaticamente convertida em uma nova linha dependente do sistema ("\r\n" no Windows). Não existe tal separação em Java: a sequência correta de caracteres deve ser passada para o fluxo de saída. Isso é feito automaticamente, por exemplo, por métodos da família PrintStream.println. Mas ao usar printf, passar '\n' na string de formato é apenas '\n', não uma nova linha dependente do sistema. Por exemplo, vamos escrever o seguinte código: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Redirecionando o resultado para um arquivo, veremos: FindBugs ajuda você a aprender Java melhor - 1 Assim, você pode obter uma estranha combinação de quebras de linha em um thread, que parece desleixada e pode surpreender algum analisador. O erro pode passar despercebido por muito tempo, especialmente se você trabalha principalmente em sistemas Unix. Para inserir uma nova linha válida usando printf, um caractere de formatação especial "%n" é usado. Aqui está o que FindBugs escreve sobre isso:
VA_FORMAT_STRING_USES_NEWLINE: A string de formato deve usar %n em vez de \n em TestNewline.main(String[]) Esta string de formato inclui um caractere de nova linha (\n). Em strings de formato, geralmente é preferível usar %n, que produzirá o separador de linha específico da plataforma.
Talvez, para alguns leitores, tudo isso já fosse conhecido há muito tempo. Mas tenho quase certeza de que para eles haverá um aviso interessante do analisador estático, que lhes revelará novos recursos da linguagem de programação utilizada.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION