JavaRush /Java-Blog /Random-DE /Kaffeepause Nr. 56. Eine Kurzanleitung zu Best Practices ...

Kaffeepause Nr. 56. Eine Kurzanleitung zu Best Practices in Java

Veröffentlicht in der Gruppe Random-DE
Quelle: DZone Dieses Handbuch enthält die besten Java-Praktiken und Referenzen zur Verbesserung der Lesbarkeit und Zuverlässigkeit Ihres Codes. Entwickler tragen eine große Verantwortung dafür, jeden Tag die richtigen Entscheidungen zu treffen, und das Beste, was ihnen dabei helfen kann, die richtigen Entscheidungen zu treffen, ist Erfahrung. Und obwohl nicht alle über umfassende Erfahrung in der Softwareentwicklung verfügen, kann jeder auf die Erfahrungen anderer zurückgreifen. Ich habe für Sie einige Empfehlungen vorbereitet, die ich aus meiner Erfahrung mit Java gewonnen habe. Ich hoffe, dass sie Ihnen dabei helfen, die Lesbarkeit und Zuverlässigkeit Ihres Java-Codes zu verbessern.Kaffeepause Nr. 56.  Eine Kurzanleitung zu Best Practices in Java – 1

Programmierprinzipien

Schreiben Sie keinen Code, der einfach funktioniert . Versuchen Sie, Code zu schreiben, der wartbar ist – nicht nur für Sie selbst, sondern auch für alle anderen, die möglicherweise in Zukunft an der Software arbeiten. Ein Entwickler verbringt 80 % seiner Zeit mit dem Lesen von Code und 20 % mit dem Schreiben und Testen von Code. Konzentrieren Sie sich also darauf, lesbaren Code zu schreiben. Ihr Code sollte keine Kommentare benötigen, damit jemand versteht, was er tut. Um guten Code zu schreiben, gibt es viele Programmierprinzipien, die wir als Richtlinien verwenden können. Im Folgenden liste ich die wichtigsten auf.
  • • KISS – Steht für „Keep It Simple, Stupid“. Möglicherweise stellen Sie fest, dass Entwickler zu Beginn ihrer Reise versuchen, komplexe, mehrdeutige Designs zu implementieren.
  • • DRY – „Wiederholen Sie sich nicht.“ Vermeiden Sie Duplikate und fügen Sie sie stattdessen in einen einzigen Teil des Systems oder der Methode ein.
  • YAGNI – „Du wirst es nicht brauchen.“ Wenn Sie sich plötzlich fragen: „Wie wäre es mit dem Hinzufügen weiterer Funktionen (Funktionen, Code usw.)?“, dann müssen Sie wahrscheinlich darüber nachdenken, ob es sich tatsächlich lohnt, diese hinzuzufügen.
  • Sauberer Code statt Smart Code – Einfach ausgedrückt: Lassen Sie Ihr Ego draußen und vergessen Sie das Schreiben von Smart Code. Sie wollen sauberen Code, keinen Smart Code.
  • Vermeiden Sie vorzeitige Optimierung – Das Problem bei vorzeitiger Optimierung besteht darin, dass Sie nie wissen, wo sich die Engpässe im Programm befinden, bis sie auftreten.
  • Einzelverantwortung – Jede Klasse oder jedes Modul in einem Programm sollte sich nur um die Bereitstellung eines Teils einer bestimmten Funktionalität kümmern.
  • Zusammensetzung statt Implementierungsvererbung – Objekte mit komplexem Verhalten sollten Instanzen von Objekten mit individuellem Verhalten enthalten, anstatt eine Klasse zu erben und neue Verhaltensweisen hinzuzufügen.
  • Objektturnen sind Programmierübungen, die als Satz von 9 Regeln konzipiert sind .
  • Schnell scheitern, schnell stoppen – Dieses Prinzip bedeutet, dass der aktuelle Vorgang angehalten wird, wenn ein unerwarteter Fehler auftritt. Die Einhaltung dieses Prinzips führt zu einem stabileren Betrieb.

Pakete

  1. Priorisieren Sie die Strukturierung von Paketen nach Themenbereich und nicht nach technischer Ebene.
  2. Bevorzugen Sie Layouts, die die Kapselung und das Verbergen von Informationen zum Schutz vor Missbrauch fördern, anstatt Kurse aus technischen Gründen zu organisieren.
  3. Behandeln Sie Pakete so, als hätten sie eine unveränderliche API – legen Sie keine internen Mechanismen (Klassen) offen, die nur für die interne Verarbeitung gedacht sind.
  4. Machen Sie keine Klassen verfügbar, die nur zur Verwendung innerhalb des Pakets vorgesehen sind.

Klassen

Statisch

  1. Erlauben Sie nicht die Erstellung einer statischen Klasse. Erstellen Sie immer einen privaten Konstruktor.
  2. Statische Klassen sollten unveränderlich bleiben und keine Unterklassen oder Multithread-Klassen zulassen.
  3. Statische Klassen sollten vor Orientierungsänderungen geschützt und als Dienstprogramme wie Listenfilterung bereitgestellt werden.

Nachlass

  1. Wählen Sie Komposition statt Vererbung.
  2. Legen Sie keine geschützten Felder fest. Geben Sie stattdessen eine sichere Zugriffsmethode an.
  3. Wenn eine Klassenvariable als final markiert werden kann , tun Sie dies.
  4. Wenn keine Vererbung erwartet wird, machen Sie die Klasse final .
  5. Markieren Sie eine Methode als endgültig , wenn nicht erwartet wird, dass Unterklassen sie überschreiben dürfen.
  6. Wenn kein Konstruktor erforderlich ist, erstellen Sie keinen Standardkonstruktor ohne Implementierungslogik. Java stellt automatisch einen Standardkonstruktor bereit, wenn keiner angegeben ist.

Schnittstellen

  1. Verwenden Sie nicht das Muster „Schnittstelle mit Konstanten“, da es Klassen ermöglicht, die API zu implementieren und zu verunreinigen. Verwenden Sie stattdessen eine statische Klasse. Dies hat den zusätzlichen Vorteil, dass Sie eine komplexere Objektinitialisierung in einem statischen Block durchführen können (z. B. das Auffüllen einer Sammlung).
  2. Vermeiden Sie eine übermäßige Nutzung der Schnittstelle .
  3. Wenn es nur eine einzige Klasse gibt, die eine Schnittstelle implementiert, führt dies wahrscheinlich zu einer übermäßigen Nutzung von Schnittstellen und schadet mehr als es nützt.
  4. „Programm für die Schnittstelle, nicht die Implementierung“ bedeutet nicht, dass Sie jede Ihrer Domänenklassen mit einer mehr oder weniger identischen Schnittstelle bündeln sollten, da Sie dadurch YAGNI kaputt machen .
  5. Halten Sie die Schnittstellen immer klein und spezifisch, damit Kunden nur die Methoden kennen, die sie interessieren. Schauen Sie sich den ISP von SOLID an.

Finalisierer

  1. Das #finalize() -Objekt sollte mit Bedacht und nur zum Schutz vor Fehlern beim Bereinigen von Ressourcen (z. B. Schließen einer Datei) verwendet werden. Stellen Sie immer eine explizite Bereinigungsmethode bereit (z. B. close() ).
  2. Rufen Sie in einer Vererbungshierarchie immer finalize() des übergeordneten Elements in einem try- Block auf . Die Klassenbereinigung sollte in einem „finally“ -Block erfolgen .
  3. Wenn keine explizite Bereinigungsmethode aufgerufen wurde und der Finalizer die Ressourcen geschlossen hat, protokollieren Sie diesen Fehler.
  4. Wenn kein Logger verfügbar ist, verwenden Sie den Ausnahmehandler des Threads (der letztendlich einen Standardfehler übergibt, der in den Protokollen erfasst wird).

Allgemeine Regeln

Aussagen

Eine Behauptung, normalerweise in Form einer Vorbedingungsprüfung, erzwingt einen „Fail Fast, Stop Fast“-Vertrag. Sie sollten flächendeckend eingesetzt werden, um Programmierfehler möglichst ursachennah zu erkennen. Objektzustand:
  • • Ein Objekt sollte niemals erstellt oder in einen ungültigen Zustand versetzt werden.
  • • Beschreiben und erzwingen Sie in Konstruktoren und Methoden den Vertrag immer mithilfe von Tests.
  • • Das Java-Schlüsselwort „asset“ sollte vermieden werden , da es deaktiviert werden kann und normalerweise ein sprödes Konstrukt ist.
  • • Verwenden Sie die Dienstprogrammklasse Assertions , um ausführliche if-else- Bedingungen für Vorbedingungsprüfungen zu vermeiden.

Generika

Eine vollständige und äußerst detaillierte Erklärung finden Sie in den Java Generics FAQ . Im Folgenden finden Sie häufige Szenarien, die Entwickler beachten sollten.
  1. Wann immer möglich, ist es besser, Typinferenz zu verwenden, anstatt die Basisklasse/Schnittstelle zurückzugeben:

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

  2. Wenn der Typ nicht automatisch ermittelt werden kann, integrieren Sie ihn.

    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. Platzhalter:

    Verwenden Sie einen erweiterten Platzhalter, wenn Sie nur Werte aus einer Struktur abrufen, verwenden Sie einen Super- Platzhalter , wenn Sie nur Werte in eine Struktur einfügen, und verwenden Sie keinen Platzhalter, wenn Sie beides tun.

    1. Jeder liebt PECS ! ( Producer-extends, Consumer-super )
    2. Verwenden Sie Foo für Produzent T.
    3. Verwenden Sie Foo für Verbraucher T.

Singletons

Ein Singleton sollte niemals im klassischen Designmusterstil geschrieben werden , was in C++ in Ordnung ist, in Java jedoch nicht geeignet ist. Auch wenn es richtig Thread-sicher ist, sollten Sie niemals Folgendes implementieren (es würde einen Leistungsengpass verursachen!):
public final class MySingleton {
  private static MySingleton instance;
  private MySingleton() {
    // singleton
  }
  public static synchronized MySingleton getInstance() {
    if (instance == null) {
      instance = new MySingleton();
    }
    return instance;
  }
}
Wenn eine verzögerte Initialisierung wirklich gewünscht ist, funktioniert eine Kombination dieser beiden Ansätze.
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: Standardmäßig wird eine Bean im Singleton-Bereich registriert, was bedeutet, dass vom Container nur eine Instanz erstellt und mit allen Verbrauchern verbunden wird. Dies bietet die gleiche Semantik wie ein regulärer Singleton, ohne Leistungs- oder Bindungseinschränkungen.

Ausnahmen

  1. Verwenden Sie geprüfte Ausnahmen für korrigierbare Bedingungen und Laufzeitausnahmen für Programmierfehler. Beispiel: Abrufen einer Ganzzahl aus einer Zeichenfolge.

    Schlecht: NumberFormatException erweitert RuntimeException und soll daher auf Programmierfehler hinweisen.

  2. Tun Sie Folgendes nicht:

    // 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
    }

    Richtige Verwendung:

    // 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. Sie müssen Ausnahmen an der richtigen Stelle, am richtigen Ort auf Domänenebene behandeln.

    FALSCH – Die Datenobjektschicht weiß nicht, was zu tun ist, wenn eine Datenbankausnahme auftritt.

    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
            }
        }}
    

    EMPFOHLENER WEG : Die Datenschicht sollte die Ausnahme einfach erneut auslösen und die Verantwortung für die Behandlung der Ausnahme an die richtige Schicht übergeben.

    === 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. Ausnahmen sollten im Allgemeinen NICHT zum Zeitpunkt ihrer Ausgabe protokolliert werden, sondern zum Zeitpunkt ihrer tatsächlichen Verarbeitung. Protokollierungsausnahmen neigen dazu, die Protokolldateien mit Rauschen zu füllen, wenn sie ausgelöst oder erneut ausgelöst werden. Beachten Sie außerdem, dass der Ausnahme-Stack-Trace weiterhin aufzeichnet, wo die Ausnahme ausgelöst wurde.

  5. Unterstützen Sie die Verwendung von Standardausnahmen.

  6. Verwenden Sie Ausnahmen anstelle von Rückkehrcodes.

Gleicht und HashCode

Beim Schreiben geeigneter Objekt- und Hashcode-Äquivalenzmethoden sind eine Reihe von Problemen zu berücksichtigen. Um die Verwendung zu vereinfachen, verwenden Sie „equals “ und „hash“ von java.util.Objects .
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());
 }
}

Resourcenmanagement

Möglichkeiten zur sicheren Freigabe von Ressourcen: Die try-with-resources- Anweisung stellt sicher, dass jede Ressource am Ende der Anweisung geschlossen wird. Jedes Objekt, das java.lang.AutoCloseable implementiert, einschließlich aller Objekte, die java.io.Closeable implementieren , kann als Ressource verwendet werden.
private doSomething() {
try (BufferedReader br = new BufferedReader(new FileReader(path)))
 try {
   // business logic
 }
}

Verwenden Sie Shutdown-Hooks

Verwenden Sie einen Shutdown-Hook , der aufgerufen wird, wenn die JVM ordnungsgemäß heruntergefahren wird. (Plötzliche Unterbrechungen, beispielsweise aufgrund eines Stromausfalls, können jedoch nicht verarbeitet werden.) Dies ist eine empfohlene Alternative anstelle der Deklaration einer finalize()- Methode , die nur ausgeführt wird, wenn System.runFinalizersOnExit() wahr ist (Standard ist falsch). .
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();
   }
 }
}
Ermöglichen Sie, dass Ressourcen vollständig (und erneuerbar) werden, indem Sie sie auf Server verteilen. (Dies ermöglicht die Wiederherstellung nach einer plötzlichen Unterbrechung, z. B. einem Stromausfall.) Sehen Sie sich den Beispielcode oben an, der ExpiringGeneralLock verwendet (eine Sperre, die allen Systemen gemeinsam ist).

Terminzeit

Java 8 führt die neue Date-Time-API im java.time-Paket ein. Java 8 führt eine neue Date-Time-API ein, um die folgenden Mängel der alten Date-Time-API zu beheben: kein Threading, schlechtes Design, komplexe Zeitzonenbehandlung usw.

Parallelität

Allgemeine Regeln

  1. Achten Sie auf die folgenden Bibliotheken, die nicht threadsicher sind. Synchronisieren Sie immer mit Objekten, wenn diese von mehreren Threads verwendet werden.
  2. Datum ( nicht unveränderlich ) – Verwenden Sie die neue Datum-Uhrzeit-API, die Thread-sicher ist.
  3. SimpleDateFormat – Verwenden Sie die neue Date-Time-API, die Thread-sicher ist.
  4. Verwenden Sie lieber java.util.concurrent.atomic -Klassen , anstatt Variablen flüchtig zu machen .
  5. Das Verhalten atomarer Klassen ist für den durchschnittlichen Entwickler offensichtlicher, wohingegen flüchtige Klassen ein Verständnis des Java-Speichermodells erfordern.
  6. Atomare Klassen packen flüchtige Variablen in eine praktischere Schnittstelle.
  7. Verstehen Sie Anwendungsfälle, in denen Volatilität angemessen ist . (siehe Artikel )
  8. Verwenden Sie Callable wenn eine geprüfte Ausnahme erforderlich ist, aber kein Rückgabetyp vorhanden ist. Da Void nicht instanziiert werden kann, kommuniziert es die Absicht und kann sicher null zurückgeben .

Streams

  1. java.lang.Thread sollte veraltet sein. Obwohl dies offiziell nicht der Fall ist, bietet das Paket java.util.concurrent in fast allen Fällen eine klarere Lösung des Problems.
  2. Das Erweitern von java.lang.Thread gilt als schlechte Praxis – implementieren Sie stattdessen Runnable und erstellen Sie einen neuen Thread mit einer Instanz im Konstruktor (Zusammensetzungsregel über Vererbung).
  3. Bevorzugen Sie Executoren und Threads, wenn eine parallele Verarbeitung erforderlich ist.
  4. Es wird immer empfohlen, eine eigene benutzerdefinierte Thread-Factory anzugeben, um die Konfiguration der erstellten Threads zu verwalten ( weitere Details hier ).
  5. Verwenden Sie DaemonThreadFactory in Executors für unkritische Threads, damit der Thread-Pool beim Herunterfahren des Servers sofort heruntergefahren werden kann ( weitere Details hier ).
this.executor = Executors.newCachedThreadPool((Runnable runnable) -> {
   Thread thread = Executors.defaultThreadFactory().newThread(runnable);
   thread.setDaemon(true);
   return thread;
});
  1. Die Java-Synchronisation ist nicht mehr so ​​langsam (55–110 ns). Vermeiden Sie es nicht, indem Sie Tricks wie das doppelt überprüfte Sperren anwenden .
  2. Bevorzugen Sie die Synchronisierung mit einem internen Objekt anstelle einer Klasse, da Benutzer mit Ihrer Klasse/Instanz synchronisieren können.
  3. Synchronisieren Sie mehrere Objekte immer in derselben Reihenfolge, um Deadlocks zu vermeiden.
  4. Durch die Synchronisierung mit einer Klasse wird der Zugriff auf ihre internen Objekte nicht automatisch blockiert. Verwenden Sie beim Zugriff auf eine Ressource immer die gleichen Sperren.
  5. Denken Sie daran, dass das synchronisierte Schlüsselwort nicht als Teil der Methodensignatur betrachtet wird und daher nicht vererbt wird.
  6. Vermeiden Sie übermäßige Synchronisierung, da dies zu schlechter Leistung und Deadlocks führen kann. Verwenden Sie das synchronisierte Schlüsselwort ausschließlich für den Teil des Codes, der eine Synchronisierung erfordert.

Sammlungen

  1. Verwenden Sie nach Möglichkeit parallele Java-5-Sammlungen in Multithread-Code. Sie sind sicher und haben hervorragende Eigenschaften.
  2. Verwenden Sie bei Bedarf CopyOnWriteArrayList anstelle von synchronisiertList.
  3. Verwenden Sie Collections.unmodifiable list(...) oder kopieren Sie die Sammlung, wenn Sie sie als Parameter in new ArrayList(list) empfangen . Vermeiden Sie es, lokale Sammlungen von außerhalb Ihrer Klasse zu ändern.
  4. Geben Sie immer eine Kopie Ihrer Sammlung zurück und vermeiden Sie eine externe Änderung Ihrer Liste mit new ArrayList (list) .
  5. Jede Sammlung muss in eine separate Klasse eingeschlossen werden, sodass das mit der Sammlung verbundene Verhalten nun ein Zuhause hat (z. B. Filtermethoden, Anwenden einer Regel auf jedes Element).

Verschiedenes

  1. Wählen Sie Lambdas gegenüber anonymen Klassen.
  2. Wählen Sie Methodenverweise anstelle von Lambdas.
  3. Verwenden Sie Enums anstelle von Int-Konstanten.
  4. Vermeiden Sie die Verwendung von Float und Double, wenn präzise Antworten erforderlich sind, sondern verwenden Sie stattdessen BigDecimal wie Money.
  5. Wählen Sie Primitivtypen anstelle von Box-Primitiven.
  6. Sie sollten die Verwendung magischer Zahlen in Ihrem Code vermeiden. Verwenden Sie Konstanten.
  7. Geben Sie nicht Null zurück. Kommunizieren Sie mit Ihrem Methodenclient über „Optional“. Das Gleiche gilt für Sammlungen – es werden leere Arrays oder Sammlungen zurückgegeben, keine Nullen.
  8. Vermeiden Sie die Erstellung unnötiger Objekte, verwenden Sie Objekte wieder und vermeiden Sie unnötige GC-Bereinigungen.

Verzögerte Initialisierung

Die verzögerte Initialisierung ist eine Leistungsoptimierung. Es wird verwendet, wenn Daten aus irgendeinem Grund als „teuer“ gelten. In Java 8 müssen wir hierfür das funktionale Provider-Interface nutzen.
== 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 + ")");
Das ist alles für den Moment, ich hoffe, das war hilfreich!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION