JavaRush /Java Blog /Random-IT /FindBugs ti aiuta a imparare meglio Java
articles
Livello 15

FindBugs ti aiuta a imparare meglio Java

Pubblicato nel gruppo Random-IT
Gli analizzatori di codice statico sono popolari perché aiutano a trovare errori commessi a causa di disattenzione. Ma ciò che è molto più interessante è che aiutano a correggere gli errori commessi per ignoranza. Anche se tutto è scritto nella documentazione ufficiale del linguaggio, non è un dato di fatto che tutti i programmatori l'abbiano letta attentamente. E i programmatori possono capire: sarai stanco di leggere tutta la documentazione. A questo proposito, un analizzatore statico è come un amico esperto che si siede accanto a te e ti osserva mentre scrivi il codice. Non solo ti dice: “Qui hai sbagliato a copiare e incollare”, ma dice anche: “No, non puoi scrivere così, guarda tu stesso la documentazione”. Un amico del genere è più utile della documentazione stessa, perché suggerisce solo quelle cose che effettivamente incontri nel tuo lavoro e tace su quelle che non ti saranno mai utili. In questo post parlerò di alcune delle complessità di Java che ho imparato utilizzando l'analizzatore statico FindBugs. Forse alcune cose saranno inaspettate anche per te. È importante che tutti gli esempi non siano speculativi, ma siano basati su codice reale.

Operatore ternario?:

Sembrerebbe che non ci sia niente di più semplice dell'operatore ternario, ma ha le sue insidie. Credevo che non ci fosse alcuna differenza fondamentale tra i progetti Type var = condition ? valTrue : valFalse; e Type var; if(condition) var = valTrue; else var = valFalse; si è scoperto che qui c'era una sottigliezza. Poiché l'operatore ternario può far parte di un'espressione complessa, il suo risultato deve essere un tipo concreto determinato in fase di compilazione. Pertanto, ad esempio, con una condizione vera nella forma if, il compilatore porta valTrue direttamente al tipo Type e, sotto forma di operatore ternario, porta prima al tipo comune valTrue e valFalse (nonostante valFalse non sia valutato), quindi il risultato porta al tipo Type. Le regole di casting non sono del tutto banali se l'espressione coinvolge tipi primitivi e wrapper su di essi (Integer, Double, ecc.). Tutte le regole sono descritte in dettaglio in JLS 15.25. Diamo un'occhiata ad alcuni esempi. Number n = flag ? new Integer(1) : new Double(2.0); Cosa accadrà a n se il flag è impostato? Un oggetto Double con un valore pari a 1,0. Il compilatore trova divertenti i nostri maldestri tentativi di creare un oggetto. Poiché il secondo e il terzo argomento sono wrapper su diversi tipi primitivi, il compilatore li scarta e restituisce un tipo più preciso (in questo caso, double). E dopo aver eseguito l'operatore ternario per l'assegnazione, viene eseguita nuovamente la boxe. In sostanza il codice equivale a questo: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); dal punto di vista del compilatore il codice non presenta problemi e viene compilato perfettamente. Ma FindBugs dà un avvertimento:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Il valore primitivo viene unboxed e forzato per l'operatore ternario in TestTernary.main(String[]) Un valore primitivo avvolto viene unboxed e convertito in un altro tipo primitivo come parte della valutazione di un operatore ternario condizionale (l'operatore b? e1: e2 ). La semantica di Java impone che se e1 ed e2 sono valori numerici racchiusi, i valori vengono unboxed e convertiti/forzati nel loro tipo comune (ad esempio, se e1 è di tipo Integer ed e2 è di tipo Float, allora e1 è unboxed, convertito in un valore in virgola mobile e boxed. Vedi JLS Sezione 15.25. Naturalmente, FindBugs avverte anche che Integer.valueOf(1) è più efficiente di new Integer(1), ma tutti lo sanno già.
Oppure questo esempio: Integer n = flag ? 1 : null; l'autore vuole inserire null in n se il flag non è impostato. Pensi che funzionerà? SÌ. Ma complichiamo le cose: Integer n = flag1 ? 1 : flag2 ? 2 : null; sembrerebbe che non ci sia molta differenza. Tuttavia, ora se entrambi i flag sono cancellati, questa riga genera un'eccezione NullPointerException. Le opzioni per l'operatore ternario destro sono int e null, quindi il tipo di risultato è Integer. Le opzioni per quella di sinistra sono int e Integer, quindi secondo le regole Java il risultato è int. Per fare ciò, è necessario eseguire l'unboxing chiamando intValue, che genera un'eccezione. Il codice è equivalente a questo: 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()); } Qui FindBugs produce due messaggi, sufficienti per sospettare un errore:
BX_UNBOXING_IMMEDIATELY_REBOXED: Il valore boxed viene unboxed e quindi immediatamente reboxed in TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Possibile dereferenziazione del puntatore null di null in TestTernary.main(String[]) Esiste un ramo dell'istruzione che, se eseguito, garantisce che un il valore null verrà dereferenziato, il che genererebbe una NullPointerException quando il codice viene eseguito.
Bene, un ultimo esempio su questo argomento: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } non sorprende che questo codice non funzioni: come può una funzione che restituisce un tipo primitivo restituire null? Sorprendentemente, si compila senza problemi. Bene, hai già capito perché viene compilato.

Formato data

Per formattare date e ore in Java, si consiglia di utilizzare classi che implementano l'interfaccia DateFormat. Ad esempio, assomiglia a questo: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Spesso una classe utilizzerà lo stesso formato più e più volte. Molte persone avranno l'idea dell'ottimizzazione: perché creare ogni volta un oggetto formato quando puoi utilizzare un'istanza comune? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } È così bello e bello, ma sfortunatamente non funziona. Più precisamente funziona, ma ogni tanto si rompe. Il fatto è che la documentazione per DateFormat dice:
I formati della data non sono sincronizzati. Si consiglia di creare istanze di formato separate per ciascun thread. Se più thread accedono contemporaneamente a un formato, è necessario sincronizzarlo esternamente.
E questo è vero se si guarda all'implementazione interna di SimpleDateFormat. Durante l'esecuzione del metodo format(), l'oggetto scrive nei campi della classe, quindi l'uso simultaneo di SimpleDateFormat da due thread porterà con una certa probabilità a un risultato errato. Ecco cosa scrive FindBugs a riguardo:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: chiamata al metodo di static java.text.DateFormat in TestDate.getDate() Come afferma JavaDoc, DateFormats sono intrinsecamente non sicuri per l'uso multithread. Il rilevatore ha trovato una chiamata a un'istanza di DateFormat ottenuta tramite un campo statico. Sembra sospetto. Per ulteriori informazioni su questo vedere Sun Bug #6231579 e Sun Bug #6178997.

Insidie ​​di BigDecimal

Avendo appreso che la classe BigDecimal consente di memorizzare numeri frazionari di precisione arbitraria e vedendo che ha un costruttore per double, alcuni decideranno che è tutto chiaro e puoi farlo in questo modo: System.out.println(new BigDecimal( 1.1)); Nessuno vieta davvero di farlo, ma il risultato può sembrare inaspettato: 1.100000000000000088817841970012523233890533447265625. Ciò accade perché il doppio primitivo è memorizzato nel formato IEEE754, in cui è impossibile rappresentare 1.1 in modo perfettamente accurato (nel sistema numerico binario si ottiene una frazione periodica infinita). Pertanto, lì viene memorizzato il valore più vicino a 1,1. Al contrario, il costruttore BigDecimal(double) funziona esattamente: converte perfettamente un dato numero in IEEE754 in forma decimale (una frazione binaria finale è sempre rappresentabile come decimale finale). Se vuoi rappresentare esattamente 1.1 come BigDecimal, puoi scrivere new BigDecimal("1.1") o BigDecimal.valueOf(1.1). Se non visualizzi subito il numero, ma esegui alcune operazioni con esso, potresti non capire da dove viene l'errore. FindBugs emette un avviso DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, che fornisce lo stesso consiglio. Ecco un'altra cosa: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); infatti d1 e d2 rappresentano lo stesso numero, ma uguale restituisce falso perché confronta non solo il valore dei numeri, ma anche l'ordine corrente (il numero di cifre decimali). Questo è scritto nella documentazione, ma poche persone leggeranno la documentazione per un metodo così familiare da pari a pari. Un problema del genere potrebbe non presentarsi immediatamente. Lo stesso FindBugs, sfortunatamente, non avvisa di questo, ma esiste un'estensione popolare per questo: fb-contrib, che tiene conto di questo bug:
MDM_BIGDECIMAL_EQUALS equals() viene chiamato per confrontare due numeri java.math.BigDecimal. Questo normalmente è un errore, poiché due oggetti BigDecimal sono uguali solo se sono uguali sia in valore che in scala, quindi 2.0 non è uguale a 2.00. Per confrontare oggetti BigDecimal per l'uguaglianza matematica, utilizzare invece compareTo().

Interruzioni di riga e printf

Spesso i programmatori che passano a Java dopo il C sono felici di scoprire PrintStream.printf (così come PrintWriter.printf , ecc.). Ottimo, lo so, proprio come in C, non hai bisogno di imparare nulla di nuovo. In realtà ci sono delle differenze. Uno di questi risiede nelle traduzioni di linea. Il linguaggio C ha una divisione in testo e flussi binari. L'output del carattere '\n' in un flusso di testo con qualsiasi mezzo verrà automaticamente convertito in un ritorno a capo dipendente dal sistema ("\r\n" su Windows). In Java non esiste tale separazione: la sequenza corretta di caratteri deve essere passata al flusso di output. Ciò avviene automaticamente, ad esempio, mediante metodi della famiglia PrintStream.println. Ma quando si utilizza printf, il passaggio di '\n' nella stringa di formato è solo '\n', non un ritorno a capo dipendente dal sistema. Ad esempio, scriviamo il seguente codice: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Dopo aver reindirizzato il risultato in un file, vedremo: FindBugs ti aiuta a imparare meglio Java - 1 Pertanto, puoi ottenere una strana combinazione di interruzioni di riga in un thread, che sembra sciatta e può far impazzire alcuni parser. L'errore può passare inosservato per molto tempo, soprattutto se si lavora principalmente su sistemi Unix. Per inserire un ritorno a capo valido utilizzando printf, viene utilizzato uno speciale carattere di formattazione "%n". Ecco cosa scrive FindBugs a riguardo:
VA_FORMAT_STRING_USES_NEWLINE: la stringa di formato deve utilizzare %n anziché \n in TestNewline.main(String[]) Questa stringa di formato include un carattere di nuova riga (\n). Nelle stringhe di formato, è generalmente preferibile utilizzare %n, che produrrà il separatore di riga specifico della piattaforma.
Forse alcuni lettori tutto quanto sopra era noto da molto tempo. Ma sono quasi sicuro che per loro arriverà un interessante avvertimento da parte dell'analizzatore statico, che rivelerà loro nuove caratteristiche del linguaggio di programmazione utilizzato.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION