JavaRush /Java Blog /Random-IT /Pausa caffè #56. Una guida rapida alle migliori pratiche ...

Pausa caffè #56. Una guida rapida alle migliori pratiche in Java

Pubblicato nel gruppo Random-IT
Fonte: DZone Questa guida include le migliori pratiche e riferimenti Java per migliorare la leggibilità e l'affidabilità del codice. Gli sviluppatori hanno la grande responsabilità di prendere le decisioni giuste ogni giorno e la cosa migliore che può aiutarli a prendere le decisioni giuste è l'esperienza. E sebbene non tutti abbiano una vasta esperienza nello sviluppo di software, tutti possono sfruttare l'esperienza degli altri. Ho preparato per te alcuni consigli che ho acquisito dalla mia esperienza con Java. Spero che ti aiutino a migliorare la leggibilità e l'affidabilità del tuo codice Java.Pausa caffè #56.  Una guida rapida alle migliori pratiche in Java - 1

Principi di programmazione

Non scrivere codice che funzioni e basta . Sforzati di scrivere codice che sia gestibile , non solo da te, ma da chiunque altro potrebbe finire per lavorare sul software in futuro. Uno sviluppatore trascorre l'80% del suo tempo a leggere il codice e il 20% a scriverlo e testarlo. Quindi, concentrati sulla scrittura di codice leggibile. Il tuo codice non dovrebbe aver bisogno di commenti affinché qualcuno possa capire cosa fa. Per scrivere un buon codice, esistono molti principi di programmazione che possiamo utilizzare come linee guida. Di seguito elencherò quelli più importanti.
  • • KISS – Sta per “Keep It Simple, Stupid”. Potresti notare che gli sviluppatori all'inizio del loro viaggio cercano di implementare progetti complessi e ambigui.
  • • ASCIUTTO - “Non ripeterti”. Cerca di evitare eventuali duplicati, inserendoli invece in un'unica parte del sistema o del metodo.
  • YAGNI - "Non ne avrai bisogno." Se all'improvviso inizi a chiederti: "Che ne dici di aggiungere altro (funzionalità, codice, ecc.)?", Probabilmente dovrai pensare se vale davvero la pena aggiungerli.
  • Codice pulito anziché codice intelligente : in poche parole, lascia il tuo ego fuori dalla porta e dimentica di scrivere codice intelligente. Vuoi un codice pulito, non un codice intelligente.
  • Evitare l'ottimizzazione prematura : il problema con l'ottimizzazione prematura è che non si sa mai dove si troveranno i colli di bottiglia nel programma finché non compaiono.
  • Responsabilità unica – Ogni classe o modulo in un programma dovrebbe preoccuparsi solo di fornire un bit di una funzionalità specifica.
  • • Ereditarietà della composizione anziché dell'implementazione: gli oggetti con comportamento complesso dovrebbero contenere istanze di oggetti con comportamento individuale, anziché ereditare una classe e aggiungere nuovi comportamenti.
  • La ginnastica con oggetti è una programmazione di esercizi concepiti come un insieme di 9 regole .
  • Fail fast, stop fast : questo principio implica l'interruzione dell'operazione corrente quando si verifica un errore imprevisto. Il rispetto di questo principio porta ad un funzionamento più stabile.

Pacchetti

  1. Dai la priorità alla strutturazione dei pacchetti per area tematica piuttosto che per livello tecnico.
  2. Preferisci layout che promuovono l'incapsulamento e l'occultamento delle informazioni per proteggerle da abusi piuttosto che organizzare lezioni per motivi tecnici.
  3. Tratta i pacchetti come se avessero un'API immutabile: non esporre meccanismi interni (classi) destinati solo all'elaborazione interna.
  4. Non esporre classi destinate a essere utilizzate solo all'interno del pacchetto.

Classi

Statico

  1. Non consentire la creazione di una classe statica. Crea sempre un costruttore privato.
  2. Le classi statiche dovrebbero rimanere immutabili, non consentire sottoclassi o classi multi-thread.
  3. Le classi statiche dovrebbero essere protette dai cambiamenti di orientamento e dovrebbero essere fornite come utilità come il filtraggio degli elenchi.

Eredità

  1. Scegli la composizione piuttosto che l'ereditarietà.
  2. Non impostare campi protetti . Specificare invece un metodo di accesso sicuro.
  3. Se una variabile di classe può essere contrassegnata come final , fallo.
  4. Se l'ereditarietà non è prevista, rendi la classe final .
  5. Contrassegnare un metodo come finale se non si prevede che le sottoclassi possano sovrascriverlo.
  6. Se un costruttore non è richiesto, non creare un costruttore predefinito senza logica di implementazione. Java fornirà automaticamente un costruttore predefinito se non ne viene specificato uno.

Interfacce

  1. Non utilizzare il modello un'interfaccia di costanti perché consente alle classi di implementare e inquinare l'API. Utilizzare invece una classe statica. Ciò ha l'ulteriore vantaggio di consentire l'inizializzazione di oggetti più complessi in un blocco statico (come il popolamento di una raccolta).
  2. Evitare un uso eccessivo dell'interfaccia .
  3. Avere una ed una sola classe che implementa un'interfaccia porterà probabilmente a un uso eccessivo delle interfacce e causerà più danni che benefici.
  4. "Programma per l'interfaccia, non per l'implementazione" non significa che dovresti raggruppare ciascuna delle tue classi di dominio con un'interfaccia più o meno identica, così facendo stai rompendo YAGNI .
  5. Mantieni sempre le interfacce piccole e specifiche in modo che i clienti conoscano solo i metodi che li interessano. Controlla l'ISP da SOLID.

Finalizzatori

  1. L' oggetto #finalize() dovrebbe essere utilizzato con giudizio e solo come mezzo per proteggere dagli errori durante la pulizia delle risorse (come la chiusura di un file). Fornire sempre un metodo di pulizia esplicito (come close() ).
  2. In una gerarchia di ereditarietà, chiama sempre finalize() del genitore in un blocco try . La pulizia della classe dovrebbe essere in un blocco finale .
  3. Se non è stato chiamato un metodo di pulizia esplicito e il finalizzatore ha chiuso le risorse, registra questo errore.
  4. Se un logger non è disponibile, utilizzare il gestore delle eccezioni del thread (che finisce per trasmettere un errore standard catturato nei log).

Regole generali

Dichiarazioni

Un'asserzione, solitamente sotto forma di controllo delle precondizioni, impone un contratto "fail fast, stop fast". Dovrebbero essere ampiamente utilizzati per identificare gli errori di programmazione il più vicino possibile alla causa. Stato dell'oggetto:
  • • Un oggetto non dovrebbe mai essere creato o messo in uno stato non valido.
  • • Nei costruttori e nei metodi, descrivere e applicare sempre il contratto utilizzando i test.
  • • La parola chiave Java assert dovrebbe essere evitata poiché può essere disabilitata e solitamente è un costrutto fragile.
  • • Utilizzare la classe di utilità Asserzioni per evitare condizioni if-else dettagliate per i controlli delle precondizioni.

Generici

Una spiegazione completa ed estremamente dettagliata è disponibile nelle Domande frequenti su Java Generics . Di seguito sono riportati gli scenari comuni di cui gli sviluppatori dovrebbero essere a conoscenza.
  1. Quando possibile, è meglio utilizzare l'inferenza del tipo anziché restituire la classe/interfaccia base:

    // MySpecialObject o = MyObjectFactory.getMyObject();
    public  T getMyObject(int type) {
    return (T) factory.create(type);
    }

  2. Se il tipo non può essere determinato automaticamente, incorporarlo.

    public class MySpecialObject extends MyObject {
     public MySpecialObject() {
      super(Collections.emptyList());   // This is ugly, as we loose type
      super(Collections.EMPTY_LIST();    // This is just dumb
      // But this is beauty
      super(new ArrayList());
      super(Collections.emptyList());
     }
    }

  3. Caratteri jolly:

    Utilizza un carattere jolly esteso quando ottieni solo valori da una struttura, usa un carattere jolly super quando inserisci solo valori in una struttura e non utilizzare un carattere jolly quando esegui entrambe le operazioni.

    1. Tutti adorano i PECS ! ( Produttore-estende, Consumatore-super )
    2. Usa Foo per il produttore T.
    3. Utilizzare Foo per il consumatore T.

Single

Un singleton non dovrebbe mai essere scritto nel classico stile del modello di progettazione , che va bene in C++ ma non è appropriato in Java. Anche se è propriamente thread-safe, non implementare mai quanto segue (sarebbe un collo di bottiglia nelle prestazioni!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Se si desidera veramente l'inizializzazione lenta, allora funzionerà una combinazione di questi due approcci.
public final class MySingleton {
  private MySingleton() {
   // singleton
  }
  private static final class MySingletonHolder {
    static final MySingleton instance = new MySingleton();
  }
  public static MySingleton getInstance() {
    return MySingletonHolder.instance;
  }
}
Spring: per impostazione predefinita, un bean è registrato con ambito singleton, il che significa che solo un'istanza verrà creata dal contenitore e connessa a tutti i consumatori. Ciò fornisce la stessa semantica di un normale singleton, senza alcuna limitazione di prestazioni o associazione.

Eccezioni

  1. Utilizza eccezioni verificate per condizioni correggibili ed eccezioni di runtime per errori di programmazione. Esempio: ottenere un numero intero da una stringa.

    Cattivo: NumberFormatException estende RuntimeException, quindi ha lo scopo di indicare errori di programmazione.

  2. Non fare quanto segue:

    // String str = input string
    Integer value = null;
    try {
       value = Integer.valueOf(str);
    } catch (NumberFormatException e) {
    // non-numeric string
    }
    if (value == null) {
    // handle bad string
    } else {
    // business logic
    }

    Uso corretto:

    // String str = input string
    // Numeric string with at least one digit and optional leading negative sign
    if ( (str != null) && str.matches("-?\\d++") ) {
       Integer value = Integer.valueOf(str);
      // business logic
    } else {
      // handle bad string
    }
  3. Devi gestire le eccezioni nel posto giusto, nel posto giusto a livello di dominio.

    MODO SBAGLIATO: il livello dell'oggetto dati non sa cosa fare quando si verifica un'eccezione del database.

    class UserDAO{
        public List getUsers(){
            try{
                ps = conn.prepareStatement("SELECT * from users");
                rs = ps.executeQuery();
                //return result
            }catch(Exception e){
                log.error("exception")
                return null
            }finally{
                //release resources
            }
        }}
    

    MODO CONSIGLIATO : il livello dati dovrebbe semplicemente rilanciare l'eccezione e trasferire la responsabilità di gestire o meno l'eccezione al livello corretto.

    === RECOMMENDED WAY ===
    Data layer should just retrow the exception and transfer the responsability to handle the exception or not to the right layer.
    class UserDAO{
       public List getUsers(){
          try{
             ps = conn.prepareStatement("SELECT * from users");
             rs = ps.executeQuery();
             //return result
          }catch(Exception e){
           throw new DataLayerException(e);
          }finally{
             //release resources
          }
      }
    }

  4. Generalmente le eccezioni NON dovrebbero essere registrate nel momento in cui vengono emesse, ma piuttosto nel momento in cui vengono effettivamente elaborate. Le eccezioni di registrazione, quando vengono lanciate o lanciate nuovamente, tendono a riempire i file di log di rumore. Si noti inoltre che l'analisi dello stack delle eccezioni registra comunque il punto in cui è stata generata l'eccezione.

  5. Supportare l'uso delle eccezioni standard.

  6. Utilizza le eccezioni anziché i codici restituiti.

Uguale e HashCode

Ci sono una serie di problemi da considerare quando si scrivono metodi corretti di equivalenza di oggetti e codici hash. Per semplificarne l'utilizzo, utilizzare java.util.Objects' equals e hash .
public final class User {
 private final String firstName;
 private final String lastName;
 private final int age;
 ...
 public boolean equals(Object o) {
   if (this == o) {
     return true;
   } else if (!(o instanceof User)) {
     return false;
   }
   User user = (User) o;
   return Objects.equals(getFirstName(), user.getFirstName()) &&
    Objects.equals(getLastName(),user.getLastName()) &&
    Objects.equals(getAge(), user.getAge());
 }
 public int hashCode() {
   return Objects.hash(getFirstName(),getLastName(),getAge());
 }
}

Gestione delle risorse

Modi per rilasciare le risorse in modo sicuro: l' istruzione try-with-resources garantisce che ogni risorsa venga chiusa alla fine dell'istruzione. Qualsiasi oggetto che implementa java.lang.AutoCloseable, che include tutti gli oggetti che implementano java.io.Closeable , può essere utilizzato come risorsa.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Utilizzare i ganci di arresto

Utilizzare un hook di spegnimento che viene chiamato quando la JVM si spegne normalmente. (Ma non sarà in grado di gestire interruzioni improvvise, ad esempio dovute a un'interruzione di corrente) Questa è un'alternativa consigliata invece di dichiarare un metodo finalize() che verrà eseguito solo se System.runFinalizersOnExit() è true (il valore predefinito è false) .
public final class SomeObject {
 var distributedLock = new ExpiringGeneralLock ("SomeObject", "shared");
 public SomeObject() {
   Runtime
     .getRuntime()
     .addShutdownHook(new Thread(new LockShutdown(distributedLock)));
 }
 /** Code may have acquired lock across servers */
 ...
 /** Safely releases the distributed lock. */
 private static final class LockShutdown implements Runnable {
   private final ExpiringGeneralLock distributedLock;
   public LockShutdown(ExpiringGeneralLock distributedLock) {
     if (distributedLock == null) {
       throw new IllegalArgumentException("ExpiringGeneralLock is null");
     }
     this.distributedLock = distributedLock;
   }
   public void run() {
     if (isLockAlive()) {
       distributedLock.release();
     }
   }
   /** @return True if the lock is acquired and has not expired yet. */
   private boolean isLockAlive() {
     return distributedLock.getExpirationTimeMillis() > System.currentTimeMillis();
   }
 }
}
Consentire alle risorse di diventare complete (oltre che rinnovabili) distribuendole tra i server. (Ciò consentirà il recupero da un'interruzione improvvisa come un'interruzione di corrente.) Vedi il codice di esempio sopra che utilizza ExpiringGeneralLock (un blocco comune a tutti i sistemi).

Appuntamento

Java 8 introduce la nuova API Date-Time nel pacchetto java.time. Java 8 introduce una nuova API Date-Time per risolvere le seguenti carenze della vecchia API Date-Time: mancato threading, progettazione scadente, gestione complessa del fuso orario, ecc.

Parallelismo

Regole generali

  1. Fare attenzione alle seguenti librerie, che non sono thread-safe. Sincronizza sempre con gli oggetti se vengono utilizzati da più thread.
  2. Data ( non immutabile ): utilizza la nuova API Date-Time, che è thread-safe.
  3. SimpleDateFormat: utilizza la nuova API Date-Time, che è thread-safe.
  4. Preferisci utilizzare le classi java.util.concurrent.atomic piuttosto che rendere volatili le variabili .
  5. Il comportamento delle classi atomiche è più ovvio per lo sviluppatore medio, mentre volatile richiede la comprensione del modello di memoria Java.
  6. Le classi atomiche racchiudono le variabili volatili in un'interfaccia più conveniente.
  7. Comprendere i casi d'uso in cui volatile è appropriato . (vedi articolo )
  8. Usa richiamabile quando è richiesta un'eccezione controllata ma non è presente alcun tipo restituito. Poiché Void non può essere istanziato, comunica l'intento e può restituire in sicurezza null .

Flussi

  1. java.lang.Thread dovrebbe essere deprecato. Anche se ufficialmente non è così, in quasi tutti i casi il pacchetto java.util.concurrent fornisce una soluzione più chiara al problema.
  2. L'estensione java.lang.Thread è considerata una cattiva pratica: implementa invece Runnable e crea un nuovo thread con un'istanza nel costruttore (regola di composizione sull'ereditarietà).
  3. Preferire esecutori e thread quando è richiesta l'elaborazione parallela.
  4. Si consiglia sempre di specificare la propria thread factory personalizzata per gestire la configurazione dei thread creati ( maggiori dettagli qui ).
  5. Utilizza DaemonThreadFactory negli esecutori per thread non critici in modo che il pool di thread possa essere arrestato immediatamente quando il server si spegne ( maggiori dettagli qui ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. La sincronizzazione Java non è più così lenta (55–110 ns). Non evitarlo utilizzando trucchi come il bloccaggio a doppio controllo .
  2. Preferisci la sincronizzazione con un oggetto interno piuttosto che con una classe, poiché gli utenti possono sincronizzarsi con la tua classe/istanza.
  3. Sincronizza sempre più oggetti nello stesso ordine per evitare blocchi.
  4. La sincronizzazione con una classe non blocca intrinsecamente l'accesso ai suoi oggetti interni. Utilizza sempre gli stessi lucchetti quando accedi a una risorsa.
  5. Ricordare che la parola chiave sincronizzata non è considerata parte della firma del metodo e pertanto non verrà ereditata.
  6. Evitare una sincronizzazione eccessiva, ciò può portare a scarse prestazioni e blocchi. Utilizzare la parola chiave sincronizzata esclusivamente per la parte del codice che richiede la sincronizzazione.

Collezioni

  1. Utilizzare raccolte parallele Java-5 nel codice multi-thread quando possibile. Sono sicuri e hanno caratteristiche eccellenti.
  2. Se necessario, utilizzare CopyOnWriteArrayList anziché sincronizzatoList.
  3. Utilizza Collections.unmodifying list(...) o copia la raccolta quando la ricevi come parametro in new ArrayList(list) . Evita di modificare le raccolte locali provenienti dall'esterno della classe.
  4. Restituisci sempre una copia della tua raccolta, evitando di modificare la tua lista esternamente con new ArrayList (list) .
  5. Ogni raccolta deve essere racchiusa in una classe separata, quindi ora il comportamento associato alla raccolta ha una casa (ad esempio metodi di filtraggio, applicazione di una regola a ciascun elemento).

Varie

  1. Scegli lambda rispetto a classi anonime.
  2. Scegli i riferimenti al metodo anziché i lambda.
  3. Utilizza le enumerazioni anziché le costanti int.
  4. Evita di usare float e double se sono richieste risposte precise, usa invece BigDecimal come Money.
  5. Scegli tipi primitivi piuttosto che primitivi boxed.
  6. Dovresti evitare di usare numeri magici nel tuo codice. Usa costanti.
  7. Non restituire Null. Comunica con il client del tuo metodo utilizzando "Opzionale". Lo stesso per le raccolte: restituisce matrici o raccolte vuote, non valori null.
  8. Evitare di creare oggetti non necessari, riutilizzare oggetti ed evitare inutili pulizie GC.

Inizializzazione pigra

L'inizializzazione pigra è un'ottimizzazione delle prestazioni. Viene utilizzato quando i dati sono considerati "costosi" per qualche motivo. In Java 8 dobbiamo utilizzare l'interfaccia del provider funzionale per questo.
== Thread safe Lazy initialization ===
public final class Lazy {
   private volatile T value;
   public T getOrCompute(Supplier supplier) {
       final T result = value; // Just one volatile read
       return result == null ? maybeCompute(supplier) : result;
   }
   private synchronized T maybeCompute(Supplier supplier) {
       if (value == null) {
           value = supplier.get();
       }
       return value;
   }
}
Lazy lazyToString= new Lazy<>()
return lazyToString.getOrCompute( () -> "(" + x + ", " + y + ")");
Per ora è tutto, spero di esserti stato utile!
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION