Ich habe versucht, Effective Java 3rd Edition "fast alle Kapitel" in "leicht lesbarem Japanisch" zu erklären.

Ich denke, Effective Java ist ein unvermeidliches Buch, um ein "vollwertiger" Java-Ingenieur zu werden. Insbesondere Ingenieure, die in der Lage sind, öffentliche APIs zu erstellen, können nur dann anständige APIs erstellen, wenn sie verstehen, was in diesem Buch geschrieben steht.

Es versteht sich von selbst, dass es ein wunderbares Buch ist, aber andererseits denke ich auch, dass es ein Buch ist, das schwer zu verstehen ist: "Kurz gesagt, das ist es."

Die Ursache ist wahrscheinlich wie folgt.

Das Wesentliche des Inhalts selbst ist nicht sehr schwierig, aber ich finde es schade, dass die Schwelle dieses Buches aus diesem Grund angehoben wird.

Daher möchte ich in diesem Artikel (fast) alle Artikel in "leicht lesbarem Japanisch" so weit wie möglich erklären.

Ich habe jedoch Erklärungen für Elemente weggelassen, die zu offensichtlich oder leicht zu lesen sind. Auch meine persönliche Meinung ist gemischt. Ich hoffe, Sie können es unter dieser Voraussetzung sehen.

Effektive Java 3rd Edition wurde für Java 9 geschrieben. Daher werde ich die offizielle Java 9-Dokumentation für alle Fälle veröffentlichen (da sie unerwartet schwer zu erreichen ist).

[Java 9] Offizielles Dokument oben https://docs.oracle.com/javase/jp/9/

[Java 9] JDK Javadoc (gefolgt vom oberen Bildschirm) https://docs.oracle.com/javase/jp/9/docs/toc.htm

[Java 9] Java-Sprachspezifikationen (gefolgt vom oberen Bildschirm) https://docs.oracle.com/javase/specs/jls/se9/html/index.html

Kapitel 1 Einleitung

Es enthält nur Definitionen von Begriffen, die in Büchern verwendet werden. Du musst es nicht lesen. (Ende)

Kapitel 2 Erstellen und Verschwinden von Objekten

Punkt 1 Betrachten Sie eine statische Factory-Methode anstelle eines Konstruktors.

Zum Beispiel sieht es wie folgt aus.

Betrachten Sie zuerst die statische Factory-Methode, und wählen Sie einen Konstruktor aus, wenn er subtil ist.

◆ Vorteile

** ① Sie können einen beschreibenden Namen angeben. ** ** **

Der Konstruktor hat die folgenden Unannehmlichkeiten.

Die statische Factory-Methode hat diese Unannehmlichkeit nicht.

** ② Es ist nicht erforderlich, ein neues Objekt zu erstellen. Verwenden Sie dasselbe Objekt erneut. ** ** **

** ③ Sie können ein Objekt dieses Subtyps anstelle des Rückgabetyps selbst zurückgeben. ** ** **

Beispielsweise verfügt java.util.Collections über eine statische Factory-Methode namens "emptyList ()". Es hat die folgenden Funktionen.

Dank dessen ist die Sammlungs-API sehr einfach. Speziell···

** ④ Sie können den zurückzugebenden Subtyp je nach Situation ändern. ** ** **

In ③ basiert die Erklärung auf der Annahme, dass immer derselbe Subtyp zurückgegeben wird. Was ich in ④ sagen möchte, ist, dass Sie aus mehreren Untertypen diejenige auswählen und zurückgeben können, die zu Ihrer Situation passt.

** ⑤ Der zurückzugebende Subtyp kann zur Laufzeit festgelegt werden. (Es ist in Ordnung, auch wenn zum Zeitpunkt der Implementierung der statischen Factory-Methode noch keine Entscheidung getroffen wurde.) **

Zum Beispiel entspricht JDBC "DriverManager.getConnection ()" diesem.

Dadurch wird es als API flexibler.

◆ Nachteile

** ① Der Benutzer kann keine Unterklasse des Rückgabetyps erstellen. ** ** **

Beispielsweise gibt emptyList () in java.util.Collections EmptyList zurück. Da EmptyList jedoch eine private Klasse ist, können Benutzer keine Unterklassen von EmptyList erstellen.

Ohne auf dieses Beispiel beschränkt zu sein, konnte ich mir keinen Fall vorstellen, der mich dazu bringen würde, eine Unterklasse zu erstellen. In der Praxis glaube ich nicht, dass es Schwächen oder Einschränkungen gibt.

** ② Für Benutzer ist es schwierig, statische Factory-Methoden zu finden. ** ** **

Dies ist sicherlich der Fall. In Javadoc ist der Konstruktor ein separater Abschnitt, der ihn hervorhebt, aber statische Factory-Methoden sind in der Liste der Methoden enthalten.

Versuchen Sie, die API für Benutzer verständlich zu machen, indem Sie den folgenden allgemeinen Namensmustern folgen.

Namensmuster Beispiel Bedeutung
from Date d = Date.from(instant); Typkonvertierung.
of Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); Zusammenfassen.
valueOf BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); Es wird austauschbar mit von und von verwendet.
instance / getInstance StackWalker luke = StackWalker.getInstance(options); Gibt die Instanz gemäß dem Parameter zurück.
create / newInstance Object newArray = Array.newInstance(classObject, arrayLen); Gibt für jeden Aufruf eine neue Instanz zurück.
Typname abrufen FileStore fs = Files.getFileStore(path); Geben Sie eine andere Klasse als Ihre eigene zurück.
neuer Typname BufferedReader br = Files.newBufferedReader(path); Geben Sie eine andere Klasse als Ihre eigene zurück. Gibt für jeden Aufruf eine neue Instanz zurück.
Modellname List<Complaint> litany = Collections.list(legacyLitany); getModellname、newModellnameの短縮版。

Referenz

Übersicht über das Collections Framework https://docs.oracle.com/javase/jp/9/docs/api/java/util/doc-files/coll-overview.html

Punkt 2 Betrachten Sie einen Builder, wenn Sie mit vielen Konstruktorparametern konfrontiert sind

[NG] Teleskopmuster

public class NutritionFacts {
    //Felddefinition weggelassen

    public NutritionFacts(int servingSize, int servings) {
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories) {
        this(servingSize, servings, calories, 0);
    }

    //Der obige Fluss geht endlos weiter ...
}

NG Punkt

[NG] Java Beans-Muster

//Es sind einfach nur Java Beans.
public class NutritionFacts {
    //Felddefinition weggelassen

    public NutritionFats() {}
    
    public void setServingSize(int val) { //Abkürzung}
    public void setServings(int val) { //Abkürzung}
    public void setCalories(int val) { //Abkürzung}
    //Der Setter fährt fort ...
}

NG Punkt

[Builder-Muster]

//Benutzercode
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
    calories(100).sodium(35).carbohydrate(27).build();

Vorteile

Nachteile (trivial)

Punkt 3 Erzwingen Sie Singleton-Eigenschaften mit einem privaten Konstruktor oder einem Aufzählungstyp

Es gibt drei Möglichkeiten, einen Singleton zu erreichen. Wählen Sie die beste für Ihre Situation.

Wenn Sie eine einzelne Tonne mit Aufzählungstyp implementieren, sieht dies folgendermaßen aus.

Aufzählungstyp einzelne Tonne


public enum Elvis {
    INSTANCE;
    public void someMethod();
}

Der beste Weg ist, enum zu verwenden, da andere Methoden das Risiko eingehen, keine einzige Tonne zu sein, und Sie müssen sie umgehen. Insbesondere ist es wie folgt.

Ich denke, dass diese Risiken in der Praxis ignoriert werden können, aber Sie sollten richtig darüber nachdenken.

Die Methode zur Verwendung des Aufzählungstyps wurde in der Praxis nicht gesehen, ist jedoch in den obigen Punkten rational und sollte aktiv angewendet werden.

Die beiden Methoden zur Verwendung des privaten Konstruktors bieten die folgenden Vorteile. Ich bin jedoch der Meinung, dass es nur eine begrenzte Anzahl von Fällen gibt, in denen Sie diese Vorteile erzielen möchten. Letztendlich ist enum in vielen Fällen die beste Wahl.

Methode Vorteile
Auf öffentliches Feld setzen und veröffentlichen ・ Aus der API geht klar hervor, dass es sich um eine einzelne Tonne handelt.
・ Einfach
Rückkehr mit statischer Factory-Methode ・ Sie können später ändern, ob es sich um Singleton handeln soll
・ Kann eine generische Singleton-Factory sein
・ Sie können Methodenreferenzen verwenden

Punkt 4 Unmöglichkeit der Instanziierung mit privatem Konstruktor erzwingen

Eine Klasse, die nur aus statischen Methoden und statischen Feldern besteht, wird üblicherweise als Dienstprogrammklasse bezeichnet.

Solche Klassen sollten nicht instanziiert und verwendet werden, aber wenn der Konstruktor verwendet wird, können sie versehentlich instanziiert werden. Ich glaube nicht, dass es tatsächlich viel Schaden gibt, aber es macht mich sehr enttäuscht, wenn es auf diese Weise verwendet wird.

Implementieren wir also einen privaten Konstruktor wie folgt:

public class UtilityClass {
    //Unterdrücken Sie den Standardkonstruktor, damit er nicht instanziiert werden kann.
    //Indem wir einen Kommentar wie diesen hinterlassen, vermitteln wir der Nachwelt die Absicht, diesen Konstruktor zu implementieren.
    private UtilityClass() {
        throw new AssertionError();
    }
}

Dies verhindert, dass Sie versehentlich instanziiert oder versehentlich geerbt werden, um Unterklassen zu erstellen.

Punkt 5 Wählen Sie die Abhängigkeitsinjektion aus, anstatt Ressourcen direkt zu verknüpfen

Nehmen wir als Beispiel eine Rechtschreibprüfung.

[NG] Als Utility-Klasse implementiert

public class SpellChecker{
    //Wörterbuch für Speccheck
    private static final Lexicon dictionary = ...;

    //Unterdrücken Sie die Instanziierung gemäß der Methode von Punkt 4
    private SpellChecker() {} 

    public static boolean isValid(String word) { ... }
}

[NG] Implementiert als eine Tonne

// SpellChecker.INSTANCE.isValid("some-word");Verwenden Sie wie.
public class SpellChecker{
    //Wörterbuch für Speccheck
    private final Lexicon dictionary = ...;

    //Unterdrücken Sie die Instanziierung gemäß der Methode von Punkt 4
    private SpellChecker() {} 
    public static SpellChecker INSTANCE = new SpellChecker(...); 

    public static boolean isValid(String word) { ... }
}

Da in diesen NG-Beispielen nur ein Wörterbuch verwendet werden kann, ist es nicht möglich, die Wörterbücher je nach Situation zu wechseln. Diese Schwierigkeit betrifft nicht nur den Produktionscode, sondern auch das Testen.

Es gibt eine Möglichkeit, eine Methode wie "setDictionary (Lexikon)" bereitzustellen, damit Sie sie später ändern können, aber für den Benutzer ist sie schwer zu verstehen. Es ist auch nicht threadsicher.

Erstens bedeutet die Tatsache, dass sich das Wörterbuch je nach Situation ändert, dass das Wörterbuch ein "Zustand" ist. Daher sollten Sie die Rechtschreibprüfung als eine Klasse implementieren, die instanziiert und verwendet werden kann.

Insbesondere ist es wie folgt.

[OK] Im Stil der Abhängigkeitsinjektion implementiert

public class SpellChecker{
    //Wörterbuch für Speccheck
    private final Lexicon dictionary;

    //Da es einen "Status" hat, der als Wörterbuch bezeichnet wird, wird es instanziiert und verwendet.
    //Zu diesem Zeitpunkt wird eine Abhängigkeit namens Wörterbuch eingefügt.
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    } 

    public static boolean isValid(String word) { ... }
}

Auf diese Weise können Sie je nach Situation die Wörterbücher wechseln und es ist threadsicher.

Punkt 6 Vermeiden Sie unnötige Objekte

Durch die Wiederverwendung von Objekten können die Kosten für die Erstellung von Objekten minimiert und ihre Geschwindigkeit erhöht werden.

Wenn Sie dagegen viele unnötige Objekte erstellen, ist dies extrem langsam.

Die unveränderliche Natur ist sehr wichtig, da unveränderliche Objekte (unveränderliche Objekte) immer sicher wiederverwendet werden können.

[NG Teil 1]

// new String()Wir erstellen also ein unnötiges Objekt.
// "bikini"Wir erstellen also ein unnötiges Objekt.
String s = new String("bikini");

【OK】

//Das generierte Objekt ist"bikini"Nur String-Instanz.
//String-Literale innerhalb derselben JVM"bikini"Instanzen von werden immer wiederverwendet.
String s = "bikini";

[NG Teil 2]

//Da es sich um einen Konstruktor handelt, werden immer neue Objekte erstellt.
new Boolean(String);

【OK】

//Die statische Factory-Methode muss kein neues Objekt erstellen.
//Ein wahres oder falsches boolesches Objekt wird wiederverwendet.
Boolean.valueOf(String);

[NG Teil 3]

//Innerhalb von Übereinstimmungen wird ein Musterobjekt erstellt.
// isRomanNumeral()Bei jedem Aufruf wird ein Musterobjekt erstellt.
static boolean isRomanNumeral(String s){
    return s.matches("Reguläre Ausdrücke. Der Inhalt wird weggelassen.");
}

【OK】

public class RomanNumerals {
    //Sie verwenden ein Musterobjekt wieder.
    private static final Pattern ROMAN = Pattern.compile("Reguläre Ausdrücke. Der Inhalt wird weggelassen.");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }
}

[NG Teil 4]

//NG Beispiel im Auto Boxen
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        //Da Long unveränderlich ist, wird bei jedem Hinzufügen eine neue Instanz erstellt.
        sum += i;

    return sum;
}

【OK】

private static long sum() {
    //Wechseln Sie zum primitiven Typ (Long)-> long)
    long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;

    return sum;
}

Übrigens gibt Map.keySet () eine Set-Ansicht (Adapter) zurück, aber unabhängig davon, wie oft Sie keySet () aufrufen, wird dieselbe Set-Instanz zurückgegeben. Es ist leicht zu glauben, dass bei jedem Aufruf eine neue Instanz erstellt wird. Tatsächlich wird die Instanz jedoch auch an diesen Stellen wiederverwendet und die Effizienz verbessert. Selbst wenn wir die API implementieren, sollten wir sie unter dem Gesichtspunkt "Muss eine neue Instanz erstellt werden?" Optimal implementieren.

Punkt 7 Veraltete Objektreferenzen entfernen

Wenn Ihre Klasse einen eigenen Speicher verwaltet, der außerhalb der Kontrolle des Garbage Collectors liegt, müssen Sie die Verweise auf nicht mehr benötigte Objekte löschen (setzen Sie die Variable auf "null").

Nicht verwaltet bedeutet, dass der Garbage Collector nicht einmal ein praktisch nicht verwendetes Objekt erkennen kann.

Normalerweise unterliegen Variablen, die außerhalb des Gültigkeitsbereichs liegen, der Speicherbereinigung. Wenn Sie dagegen Objekte in einer Klasse verwalten, sind diese nicht außerhalb des Gültigkeitsbereichs und unterliegen keiner Speicherbereinigung. Aus Sicht des Garbage Collectors gilt das Objekt als in Gebrauch.

In diesem Buch wird das Obige anhand einer einfachen Stapelimplementierung als Beispiel erläutert. Weitere Informationen finden Sie im Buch.

Fall der Implementierung eines eigenen Caches

Selbst wenn Sie Ihren eigenen Cache implementieren, entspricht dies dem oben genannten "Fall der Verwaltung Ihres eigenen Speichers in der Klasse".

Wenn Sie beispielsweise Bilddaten mit HashMap zwischenspeichern, sollte HashMap als Feld eines Objekts A verwaltet werden, damit die Referenzen wie folgt verbunden sind.

Quelle-> Objekt A-> HashMap-> Schlüssel und Wert

Selbst wenn die von HashMap verwalteten Daten (Schlüssel und Wert) nicht mehr benötigt werden, werden diese Daten nicht als Müll gesammelt, solange weiterhin auf Objekt A und HashMap verwiesen wird. Infolgedessen wächst der Speicher.

Das Mittel in diesem Fall ist wie folgt. (Ich glaube nicht, dass es viele Möglichkeiten gibt, den Cache selbst zu implementieren ...)

** Methode ① Implementieren Sie den Cache mit WeakHashMap. ** ** **

Wenn ein Schlüssel nicht mehr von etwas anderem als seinem WeakHashMap-Objekt referenziert wird, unterliegt die WeakHashMap dem nächsten GC für diesen Eintrag (Schlüssel-Wert-Paar). Wir verwenden einen Mechanismus, der als schwache Referenz bezeichnet wird.

Wenn Sie den Schlüssel mit dem Status "Der Schlüssel wird nicht von einem anderen als diesem WeakHashMap-Objekt referenziert" aus dem Cache löschen möchten, sollten Sie ihn übernehmen.

Wie in diesem Buch erläutert, kann es nicht mehr als Cache bezeichnet werden, wenn es so einfach aus dem Cache gelöscht wird. Einzelheiten finden Sie unter [siehe unten. ](# Was ist eine Referenz schwache Referenz)

** Methode (2) Löschen Sie regelmäßig alte Daten im Cache mit ScheduledThreadPoolExecutor. ** ** **

Dieser Artikel wird empfohlen, um ein schnelles Verständnis der Verwendung von ScheduledThreadPoolExecutor zu erhalten. https://codechacha.com/ja/java-scheduled-thread-pool-executor/

Verwenden Sie es, wenn Sie etwas löschen möchten, das seit einiger Zeit im Cache registriert ist.

** Methode ③ Wenn Sie dem Cache einen neuen Eintrag hinzufügen, löschen Sie den alten. ** ** **

Es ist einfach. Wenn Sie ähnlich wie in (2) etwas löschen möchten, das seit einiger Zeit im Cache registriert ist, sollten Sie es übernehmen.

Fall der Registrierung von Listenern und Rückrufen im Speicher

Wenn Sie eine API erstellen, mit der Listener und Rückrufe vom Client registriert werden können, unterliegen die registrierten Listener und Rückrufe keiner GC, es sei denn, sie werden mit angemessener Berücksichtigung erstellt.

Es ist eine gute Idee, einen schwachen Referenzmechanismus zu verwenden, z. B. ihn als Schlüssel für WeakHashMap zu speichern. Wenn es von niemand anderem als WeakHashMap mehr verwendet wird, unterliegt es der GC.

Referenz: Was ist eine schwache Referenz?

Objekte, die normalerweise den Referrer nicht erreichen können (Objekte, die von niemandem verwendet werden), unterliegen der GC und werden aus dem Speicher gelöscht.

Es kann jedoch ein Problem sein, wenn es sofort gelöscht wird. Es gibt einen Mechanismus, der verhindert, dass Objekte, die von der Referenzquelle aus nicht erreicht werden können, sofort der GC unterliegen. Dies ist das java.lang.ref-Paket.

In der Welt dieses Pakets werden gewöhnliche Referenzen als "starke Referenzen" bezeichnet und ihre eigenen "Referenzen" wie folgt definiert.

java.lang.Arten von Referenzen in ref Schwierigkeit, ein GC-Ziel zu werden (relativer Wert) Erläuterung Verwenden
Schwache Referenz
Leicht zu löschen
Wenn nur Sie (WeakReference-Objekt) auf ein Objekt A verweisen, werden Sie dem nächsten GC unterzogen. Verwenden Sie diese Option, wenn Sie Objekt A sofort aus dem Speicher löschen möchten, wenn keine starken Verweise auf Objekt A vorhanden sind. WeakHashMap verwendet WeakReference für diesen Zweck. In diesem Buch steht geschrieben, dass es als Cache verwendet werden kann, aber wenn die starke Referenz verschwindet, verschwindet sie. Ich denke, dass es kein Cache mehr ist, und ich denke, dass es fast keine Möglichkeit gibt, sie zu verwenden.
Weiche Referenz ★★
Ziemlich stur
Wenn nur Sie (SoftReference-Objekt) auf ein Objekt A verweisen, wurde das referenzierte Objekt kürzlich erstellt./Wenn darauf verwiesen wird, unterliegt es nicht dem nächsten GC. Wenn nicht, unterliegt es dem nächsten GC. Ich denke, dies ist der Fall, wenn Sie es für Caching-Zwecke verwenden, aber Java SE verfügt nicht über eine Map, die dies unterstützt. Es scheint fast keine Chance zu geben, es tatsächlich zu benutzen.
Phantomreferenz ★★★
Grundsätzlich verschwindet es nicht
Selbst wenn nur Sie (PhantomReference-Objekt) auf ein bestimmtes Objekt A verweisen, ist es nicht das Ziel von GC. (Unbekannt)

Punkt 8 Vermeiden Sie Finalisierer und Reiniger

Der Finalizer hier ist java.lang.Object # finalize () oder eine Methode, die ihn in einer Unterklasse überschreibt.

Ein Reiniger ist ein java.lang.ref.Cleaner.

Es gibt verschiedene Gründe, warum dies nicht gut ist, aber es besteht fast keine Notwendigkeit, den Inhalt zu verstehen.

Verwenden Sie es niemals, da es sowieso gefährlich ist. Das ist gut.

Punkt 9 Wählen Sie "Try-with-Resources" und nicht "Try-finally"

try-finally hat folgende Probleme:

Try-with-Resources löst diese Probleme.

Wenn während des Versuchs eine Ausnahme auftritt und dann auch beim Schließen eine Ausnahme auftritt, wird der ersteren Priorität eingeräumt und ausgelöst. Sie können dies in der catfh-Klausel abfangen.

Verwenden Sie für den Zugriff auf Letzteres die Methode getSuppressed im ersteren Ausnahmeobjekt. In vielen Fällen möchten Sie jedoch das erstere kennenlernen, sodass Sie anscheinend nicht viele Möglichkeiten haben, es zu verwenden.

Kapitel 3 Allen Objekten gemeinsame Methoden

Punkt 10 Befolgen Sie bei allgemeinen Überschreibungen den allgemeinen Vertrag

Ich denke, die Möglichkeit, die Equals-Methode zu überschreiben, ist in erster Linie begrenzt. Wenn Sie überschreiben möchten, müssen die Anforderungen erfüllt werden.

Wenn Sie die Methode equals überschreiben müssen, halten Sie sich an diese Anforderung. Insbesondere sind die Anforderungen wie folgt.

Das Buch sagt, dass es bei jeder Anforderung einige Dinge zu beachten gibt, aber es gibt nicht viele Möglichkeiten, Gleichheit zu überschreiben. Daher ist es wahrscheinlich weniger kostspielig, mehr zu lernen, wenn Sie es nicht benötigen.

Aus diesem Grund beziehe ich mich nur dann auf Bücher, wenn ich sie brauche, und ich werde auch in diesem Artikel nicht ins Detail gehen.

Punkt 11 Wenn Sie überschreiben gleich überschreiben, überschreiben Sie immer hashCode

Wie Sie in Punkt 10 sehen können, gibt es nicht viele Chancen, Gleichheit zu überschreiben, daher scheint dieser Punkt auch nicht sehr wichtig zu sein. Ich gebe Ihnen nur einen Überblick.

Die Anforderungen zum Überschreiben der hashCode-Methode lauten wie folgt:

Punkt 12 Überschreiben Sie immer toString

Das Überschreiben der toString-Methode hat den Vorteil, dass Benutzer dieser Klasse das Debuggen erleichtern können.

Das in der Praxis geschaffene System ist jedoch kein Kunstwerk, und die personellen und zeitlichen Ressourcen sind begrenzt. Daher denke ich, dass Sie entscheiden sollten, ob Sie bei Bedarf überschreiben möchten oder nicht.

Beachten Sie Folgendes, wenn Sie die toString-Methode überschreiben:

Punkt 13 Klon vorsichtig überschreiben

Das Überschreiben von Object.clone () ist im Grunde NG. Der Grund ist wie folgt.

Da es sich um NG handelt, sollten Sie Object.clone () nicht überschreiben, es sei denn, Sie haben bereits eine Klasse, die Object.clone () überschreibt, und müssen diese bei der Wartung beheben.

Verwenden Sie stattdessen die folgenden Methoden. Diese haben die oben genannten Nachteile nicht.

//Konstruktor kopieren
public Yum(Yum yum) { ... }

//Fabrik kopieren
public static Yum newInstance(Yum yum) { ... }

Unabhängig davon, ob Sie einen Kopierkonstruktor oder eine Kopierfactory-Methode oder Object.clone () verwenden, sollten Sie die folgenden Punkte gemeinsam beachten.

Mit anderen Worten, beim Kopieren eines Felds reicht es nicht aus, die Referenz des Kopierquellenobjekts auf das Kopierzielfeld festzulegen. Dies liegt daran, dass dieselbe Objektreferenz zwischen der Kopierquelle und dem Kopierziel geteilt wird. Dies ist die sogenannte flache Kopie. Die Idee ist offensichtlich, aber die Implementierung ist ziemlich umständlich. Es gibt eine Ausnahme von dieser Regel. Wenn es sich um ein unveränderliches Objekt handelt, können Sie die Referenz kopieren.

Um diesen Artikel zu verstehen, empfehlen wir Ihnen, Folgendes im Voraus zu kennen.

Punkt 14 Erwägen Sie die Implementierung von Comparable

Durch Implementieren von Comparable.compareTo () in der von Ihnen entwickelten Klasse können Sie die Objekte dieser Klasse in einer Sammlung speichern und die praktische API der Sammlung verwenden. Zum Beispiel können Sie gut sortieren.

Wenn Sie diesen Vorteil nutzen möchten, implementieren Sie eine vergleichbare Schnittstelle.

In diesem Fall müssen Anforderungen erfüllt sein, die dem Überschreiben der Equals-Methode ähneln.

Es gibt Hinweise zu den ersten drei Anforderungen. Bei einer vorhandenen Klasse, die Comparable implementiert, ist es praktisch unmöglich, diese Anforderungen zu erfüllen, wenn Sie sie erweitern und neue Felder hinzufügen. Wenn Sie es erzwingen, ist es kein objektorientierter Code mehr. Lassen Sie es uns in einem solchen Fall mit Komposition statt mit Erweiterung realisieren (Punkt 18).

Was ist, wenn wir die vierte Anforderung verletzen? Ein Beispiel für eine Verletzung ist Big Decimal. new BigDecimal ("1.0") und new BigDecimal ("1.00") sind in der equals-Methode nicht gleich und in der compareTo-Methode gleich.

Wenn Sie sie in ein neues HashSet einfügen, werden sie mit der Methode equiqls verglichen, sodass die Anzahl der Elemente 2 beträgt. Wenn Sie es dagegen in ein neues TreeSet einfügen, beträgt die Anzahl der Elemente 1, da es mit der compareTo-Methode verglichen wird. Wenn Sie dieses Verhalten nicht berücksichtigen, wissen Sie nicht, was die Ursache für den unwahrscheinlichen Fall eines Problems ist.

Beachten Sie zusätzlich zu den vier Anforderungen Folgendes:

private static final Comparator<PhoneNumber> COMPARATOR = 
            comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.prefix)
                .thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}
//NG Beispiel
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
}

Kapitel 4 Klassen und Schnittstellen

Punkt 15 Minimieren Sie die Zugänglichkeit für Klassen und Mitglieder

Was ist das Verstecken und Einkapseln von Informationen?

Minimieren Sie den Teil der Komponente, auf den von außen zugegriffen werden kann (öffentliche API). Machen Sie andere Teile (interne Daten, Implementierungsdetails) von außen unzugänglich.

Um es klar auszudrücken, halten Sie das, was Sie öffentlich oder geschützt erklären, auf ein Minimum. Gestalten Sie Ihre Klasse sorgfältig dafür.

Gründe für das Ausblenden und Einkapseln von Informationen (warum)

Durch das Ausblenden und Einkapseln von Informationen können Komponenten individuell entwickelt und individuell optimiert werden. Mit anderen Worten, Sie können die Sorge, dass andere Komponenten nicht beschädigt werden, erheblich reduzieren.

Der Zweck des Versteckens und Einkapselens von Informationen besteht darin, Folgendes zu erreichen.

Wenn Sie diese Ziele nicht erreichen können, macht es keinen Sinn, Informationen auszublenden oder zu kapseln. Praktische Programme sind keine Kunstwerke, sondern ein Mittel für ein erfolgreiches Geschäft. Sie sollten also nicht zufrieden sein, dass Sie etwas Schönes gemacht haben (ich bin sehr vorsichtig, weil es einfach ist, auf diese Weise zufrieden zu sein).

Wie es geht (wie)

Beachten Sie insbesondere die folgenden Punkte.

Punkt 16 Verwenden Sie in der öffentlichen Klasse die Zugriffsmethode anstelle des öffentlichen Felds.

Es ist selbstverständlich, Setter / Getter hinzuzufügen.

In der Praxis halte ich es für vernünftig, dies zu tun, und ich denke, es ist einfach, Setter / Getter hinzuzufügen, ohne an irgendetwas zu denken. Wenn Sie den Grund jedoch nicht verstehen, ist dies kein gutes Klassendesign. Lassen Sie uns den Grund verstehen, warum wir Setter / Getter erneut hinzufügen sollten.

Der Grund für das Hinzufügen von Setter / Getter besteht darin, dass Informationen ausgeblendet und gekapselt werden. Insbesondere ist es wie folgt.

Da der wesentliche Punkt darin besteht, die Anzahl der öffentlichen APIs so weit wie möglich zu reduzieren, besteht wenig Bedarf, Setter / Getter in den Feldern für paketprivate Klassen und private innere Klassen vorzubereiten. Wenn Sie versuchen, Setter / Getter unnötig bereitzustellen, dauert die Implementierung länger und der Code ist schwer zu lesen. Fühlen Sie sich nicht wie "nur Setter / Getter hinzufügen, ohne nachzudenken".

Punkt 17 Variabilität minimieren

Unveränderliche Objekte (unveränderliche Objekte) haben mehrere Vorteile, einschließlich der Tatsache, dass sie threadsicher verwendet werden können. Typische Beispiele in JDK sind grundlegende Datenklassen wie String, Integer, BigDecimal, BigInteger.

Wenn Sie eine eigene Klasse mit Werten erstellen, machen Sie diese unveränderlich, es sei denn, Sie haben einen guten Grund, sie variabel zu machen. Auch wenn es nicht praktisch ist, es perfekt unveränderlich zu machen, ist es eine gute Idee, es so unveränderlich wie möglich zu machen, beispielsweise das Feld so endgültig wie möglich zu machen. Dies liegt daran, dass es Vorteile wie weniger Probleme gibt, wenn die Anzahl möglicher Zustände verringert wird.

Vorteile von unveränderlichen Objekten

Nachteile unveränderlicher Objekte

Wie man ein unveränderliches Objekt herstellt (Anforderungen müssen erfüllt sein)

Punkt 18 Wählen Sie Komposition statt Vererbung

Durch Vererbung wird die Kapselung unterbrochen. Mit anderen Worten, Unterklassen hängen von den Implementierungsdetails von Oberklassen ab. Infolgedessen können Änderungen in den Implementierungsdetails von Oberklassen dazu führen, dass Unterklassen nicht wie beabsichtigt funktionieren, oder zu Sicherheitslücken.

Sie könnten denken, dass es in Ordnung ist, wenn die Unterklasse die Methoden der Oberklasse nicht überschreibt, aber das ist nicht der Fall. Die Signatur einer Methode, die die später hinzugefügte Oberklasse enthält, kann mit der in der Unterklasse implementierten Methode in Konflikt stehen.

Springen Sie wegen dieser Unannehmlichkeiten nicht plötzlich in die Vererbung. Kompositionen haben nicht die gleichen Unannehmlichkeiten.

Die Vererbung ist jedoch zu 100% schlecht und die Zusammensetzung nicht zu 100% positiv. Lassen Sie uns je nach Situation zwischen Komposition und Vererbung wählen.

Was ist Zusammensetzung?

Anstatt eine vorhandene Klasse zu erben, behalten Sie die vorhandene Klasse in einem privaten Feld, wie unten gezeigt. Erweitern Sie eine vorhandene Klasse, indem Sie die Methoden dieser vorhandenen Klasse aufrufen.


//Klasse übertragen. Die Komposition wurde in dieser Klasse angewendet.
//Eine Klasse wird separat vom InstrumentedSet bereitgestellt, damit sie wiederverwendet werden kann.
public class ForwardingSet<E> implements Set<E> {
    //Halten Sie das Objekt der vorhandenen Klasse, die Sie erweitern möchten, in das Feld.
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    //Werfen Sie den Prozess auf das Objekt der vorhandenen Klasse, die Sie erweitern möchten.
    public void clear() {
        this.s.clear();
    }

    //Danach weggelassen.
}

/*
Erstellen Sie Ihre eigene Klasse, indem Sie die Übertragungsklasse erben.
Sie könnten denken: "Was? Vererbung ist nicht gut, oder?", Aber Vererbung hier ist
Dies ist eine vernünftige Entscheidung, um das Weiterleitungsset für andere Zwecke wiederverwendbar zu machen.
*/
public class InstrumentedSet<E> extends FowardingSet<E> {
    //Diese Klasse ist dafür verantwortlich, zu verwalten, wie oft ein Satz insgesamt hinzugefügt wurde.
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    //Danach weggelassen.
}

Übrigens ist die Technik im obigen Codebeispiel nicht genau "Delegierung". Bitte seien Sie vorsichtig.

Wenn es in Ordnung ist zu erben

Sie können in den folgenden Fällen erben.

Als wichtige Voraussetzung für die Übernahme der Vererbung ist es erforderlich, dass eine "is-a" -Beziehung zwischen den Klassen hergestellt wird. Das heißt, wenn Klasse B von Klasse A erbt, muss die Antwort "Sind alle Bs A?" Ja sein. Vererbung ist eine Technik zum Realisieren solcher Beziehungen im Code.

Punkt 19 Entwurf und Dokument für die Vererbung, andernfalls die Vererbung verbieten

Das Erstellen einer Klasse, die vererbt werden soll, ist tatsächlich äußerst schwierig und schwierig. Insbesondere sollten die folgenden Punkte berücksichtigt werden.

Wie Sie sehen können, ist es extrem schwierig. Wenn Sie eine Klasse erstellen, die auf jeden Fall vererbt werden soll, sollten Sie bereit sein, die oben genannten Punkte zu übernehmen. Das ist ein Profi.

Ich denke, es gibt mehr Fälle, in denen dies nicht der Fall ist, als beim Erstellen einer Klasse, die vererbt werden soll. In diesem Fall machen Sie entweder die Klasse final oder machen Sie den Konstruktor privat und bereiten Sie eine statische Factory-Methode vor, damit die erstellte Klasse nicht versehentlich vererbt wird.

Punkt 20 Wählen Sie eine Schnittstelle über einer abstrakten Klasse aus

Dieser Artikel ist extrem schwer zu lesen. Ich werde vorrangig die Verständlichkeit erläutern.

Es gibt mehrere Implementierungen eines "Typs". Beispielsweise gibt es mehrere Implementierungen eines "Typs" namens "Vergleichbar". Java bietet die folgenden zwei Mechanismen, um einen solchen Typ "Mehrfachimplementierungen zulassen" zu realisieren.

Wenn Sie einen neuen Typ erstellen möchten, der "mehrere Implementierungen zulässt", tun Sie dies grundsätzlich mit einer Schnittstelle. Die Schnittstelle ist der abstrakten Klasse auf folgende Weise überlegen: Es ist einfach für Benutzer zu verwenden.

Techniken zum Erstellen komplizierter Schnittstellen "Montagehilfe" "Skelettmontage"

Wenn Sie selbst eine einfache Benutzeroberfläche erstellen möchten, müssen Sie sich über diese Technik keine Gedanken machen. Bitte sehen Sie nur diejenigen, die es brauchen.

Angenommen, Sie erstellen beim Erstellen Ihrer eigenen Schnittstelle mehrere Methoden für diese Schnittstelle. Angenommen, Sie definieren Methode A und Methode B. Wenn Sie wissen, dass Methode A Methode B aufruft, ist es normalerweise einfacher, die typische Logik von Methode A in Ihrer Schnittstelle zu implementieren. Für Benutzer ist es einfacher, wenn sie nur wenige Teile implementieren.

Es gibt die folgenden zwei Muster, um dies zu erreichen.

Punkt 21 Entwerfen Sie die Schnittstelle für die Zukunft

Sobald die Schnittstelle veröffentlicht ist, ist es die letzte. Es ist nicht so einfach zu ändern. Lassen Sie es uns vor der Veröffentlichung gründlich überprüfen.

Wenn Sie einer Schnittstelle später eine Methode hinzufügen, wird bei der Klasse, die diese Schnittstelle implementiert, ein Kompilierungsfehler angezeigt. Sie können die Standardeinstellung als "Trick" verwenden, um dieses Problem zu vermeiden. In den folgenden Punkten ist dies jedoch NG. Es ist wichtig, sich nicht auf die Standardeinstellungen zu verlassen, sondern fest zu entwerfen.

Punkt 22 Verwenden Sie die Schnittstelle nur, um den Typ zu definieren

Es gibt eine Methode zum Definieren einer Konstante in einer Schnittstelle und zum Implementieren dieser Schnittstelle, damit die Klasse die Konstante verwenden kann. Das ist NG. Das JDK hat tatsächlich eine solche Schnittstelle, aber Sie sollten sie nicht kopieren.

Dies liegt daran, dass die Klasse die Konstanten anderer Komponenten verwendet, was ein Implementierungsdetail darstellt. Das Implementieren einer Schnittstelle bedeutet, diesen Teil zu einer öffentlichen API zu machen. Implementierungsdetails sollten keine öffentlichen APIs sein. Erstens kommt das Offenlegen von Konstanten nicht in Frage, da es weit vom Wesen der Schnittstelle entfernt ist.

Wenn Sie die Konstante nach außen bereitstellen möchten, führen Sie einen der folgenden Schritte aus.

//Nicht unveränderliche Gebrauchsklasse
public class PhysicalConstatns(){
    private PhysicalConstants() {} //Instanziierung verhindern

    public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
}

Durch das statische Importieren der Konstantenklasse muss der Benutzer den Klassennamen nicht jedes Mal schreiben, wenn die Konstante verwendet wird. Es wird im Buch erklärt, aber wenn Sie später darauf zurückblicken, werden Sie gefragt: "Wo ist diese Konstante definiert?", Was schwierig zu lesen sein kann. Berücksichtigen Sie das Gleichgewicht, wenn Sie entscheiden, ob ein statischer Import angemessen ist.

Punkt 23 Wählen Sie eine Klassenhierarchie anstelle einer markierten Klasse aus

Angenommen, es gibt eine Klasse in einer Klasse, die ausdrückt, ob es sich um einen Kreis oder ein Rechteck handelt. Einige von ihnen sind Konstruktoren, Felder und Methoden, die für Kreise verwendet werden, andere auch für Rechtecke.

Klassen, die auf diese Weise versuchen, mehrere Konzepte in einer Klasse auszudrücken, werden in Büchern als "markierte Klassen" bezeichnet.

Wenn Sie mehrere Konzepte ausdrücken möchten, teilen Sie die Klassen gehorsam auf. Verwenden wir insbesondere Vererbung / Subtypisierung, um sie in einer hierarchischen Struktur zu organisieren. Es ist natürlich.

Punkt 24 Wählen Sie eine statische Elementklasse gegenüber einer nicht statischen Elementklasse aus

Manchmal möchten Sie eine andere Klasse innerhalb einer Klasse deklarieren. In diesem Zusammenhang wird die erstere Klasse als "einschließende Klasse" und die letztere Klasse als "verschachtelte Klasse" bezeichnet.

Es gibt verschiedene Möglichkeiten, verschachtelte Klassen zu implementieren. Insbesondere gibt es die folgenden vier.

Lassen Sie uns diese richtig verwenden. In Anbetracht des folgenden Ablaufs sollte kein Fehler vorliegen.

Untersuchungsschritt 0

Ist es möglich, dass die von Ihnen erstellte "verschachtelte Klasse" unabhängig von der einschließenden Klasse von anderen Klassen verwendet wird?

Wenn JA, sollte es überhaupt nicht als verschachtelte Klasse erstellt werden. Es sollte als gewöhnliche Klasse erstellt werden, unabhängig von der einschließenden Klasse.

Untersuchungsschritt 1

Wenn die "verschachtelte Klasse", die Sie erstellen möchten, alle folgenden ist, verwenden Sie ** anonyme Klasse **.

Wenn eine anonyme Klasse in einer nicht statischen Methode der einschließenden Klasse deklariert wird, verweist die Instanz der anonymen Klasse automatisch auf die Instanz der einschließenden Klasse. In einigen Fällen führt dies zu einem Speicherverlust. Seien Sie sich der Gefahr bewusst, bevor Sie es verwenden.

Dies ist nicht der Fall, wenn es in einer nicht statischen Methode deklariert wird.

Untersuchungsschritt 2

Wenn die "verschachtelte Klasse", die Sie erstellen möchten, alle folgenden ist, verwenden Sie ** lokale Klasse **.

Untersuchungsschritt 3

Wenn die "verschachtelte Klasse", die Sie erstellen möchten, alle folgenden ist, verwenden Sie eine ** nicht statische Mitgliedsklasse **.

Untersuchungsschritt 4

Wenn die "verschachtelte Klasse", die Sie erstellen möchten, alle folgenden ist, verwenden Sie ** statische Elementklassen **.

Punkt 25 Beschränken Sie Quelldateien auf eine einzelne Klasse der obersten Ebene

Normalerweise würden Sie nicht mehrere Klassen der obersten Ebene in einer Quelldatei implementieren. In diesem Artikel wird erklärt, warum es sich um NG handelt, aber Sie müssen den Grund nicht kennen, da Sie dies in der Praxis überhaupt nicht tun. (Ende)

Kapitel 5 Generika

Punkt 26: Verwenden Sie den Prototyp nicht

Ein Prototyp ist ein Ausdruck, der keinen Typparameter enthält, z. B. "Liste" anstelle von "Liste ".

Es ist nicht mehr normal, dass Sie den Prototyp nicht verwenden sollten. Der Grund ist, dass Sie zur Laufzeit möglicherweise eine ClassCastException erhalten. Wenn Sie es mit Typparametern implementieren, können Sie solche Risiken in Form von Kompilierungsfehlern und Warnungen zur Kompilierungszeit feststellen. Verwenden wir nicht den Prototyp.

Es gibt jedoch Situationen, in denen Sie den Prototyp möglicherweise versehentlich verwenden. Insbesondere ist es wie folgt.

【NG】

//Tun Sie dies nicht nur, weil Sie die Typparameter von Set nicht kennen.
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if(s2.contains(o1))
            result++;
    return result;
}

Lassen Sie es uns wie folgt implementieren.

【OK】

//Dies führt zu einem Kompilierungsfehler, wenn versucht wird, Elemente zu s1 oder s2 hinzuzufügen.
//Es ist definitiv besser, als es zur Laufzeit zu bemerken. (Es kann jedoch null hinzugefügt werden.)
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
    //Abkürzung
}

Punkt 27 Entfernen Sie die nicht inspizierte Warnung

Bei der Implementierung mit Generika können die Risiken, die zur Laufzeit zu einer ClassCastException führen, durch Kompilierungsfehler und Warnungen zur Kompilierungszeit erkannt werden.

Sie können die Warnungen kompilieren, wenn Sie sie in Ruhe lassen. Reagieren Sie jedoch in der Regel auf alle Warnungen und korrigieren Sie sie, damit sie nicht angezeigt werden. Fügen Sie "@SuppressWarning (" nicht markiert ")" hinzu, um die Warnung nur zu unterdrücken, wenn es wirklich kein Problem gibt. Übrigens ist "nicht markiert" eine Warnung, die bedeutet "Ich habe den Typ nicht überprüft, aber ist es in Ordnung?"

Wenn Sie die Warnung nicht unterdrücken, wenn es sich nicht wirklich um ein Problem handelt, können Sie die Warnung, die wirklich ein Problem darstellt, nicht bemerken. Lassen Sie uns dort auch keine Ecken abschneiden.

Punkt 28 Wählen Sie eine Liste aus einem Array aus

Sequenzen und Generika haben aufgrund ihres historischen Hintergrunds unterschiedliche Eigenschaften. Aus diesem Grund verursacht die kombinierte Verwendung Probleme. Implementieren Sie diese einheitlich in Generics, es sei denn, es gibt wesentliche Probleme mit der Einfachheit oder Leistung des Codes.

Welche Probleme treten bei kombinierter Anwendung auf?

Wenn Sie beide in Kombination implementieren, verstehen Sie die Bedeutung der vom Compiler ausgegebenen Fehler und Warnungen nicht und sind unnötig verwirrt. In einigen Fällen wird die Warnung ohne sorgfältige Überlegung unterdrückt, was zu einer ClassCastException führt oder die Wartungsmitglieder fragt: "Warum machen Sie hier" @ SuppressWarning "...?" Wird umarmt und verwirrt sein.

Zum Beispiel ...

Warum passiert das?

Dies liegt daran, dass es die folgenden Unterschiede zwischen den beiden gibt. (Obwohl es im Buch erklärt wird, erklärt es nicht, warum diese Unterschiede zu dem oben genannten Problem führen ... Ich werde später einen Kommentar hinzufügen, wenn ich Zeit habe.)

Unterschied ① Unterschied ②
Array Zum BeispielObject[] objectArray = new Long[1];DannLong[]IstObject[]Als Subtyp vonWird behandelt.. Diese Zuordnungen sind also zulässig. Diese Eigenschaften werden als Kovarianten bezeichnet. Es ist gut, sich mit Nuancen wie "flexibel nach der anderen Partei ändern" zu erinnern. ArrayIst、Zur Laufzeit自身がどんな型を格納できるのか、ということを知っています。なので、不適切な型のオブジェクトを代入しようとすると、Zur LaufzeitIch bekomme eine Ausnahme. Diese Eigenschaften werden als "Beton" bezeichnet.
Generika Zum BeispielList<Object> objectList = new ArrayList<Long>();DannArrayList<Long>IstList<Object>Als Subtyp vonNicht behandelt.. Daher tritt ein Kompilierungsfehler auf. Diese Eigenschaften werden als invariant bezeichnet. Es ist nicht so flexibel wie kovariant. GenerikaIstZur Kompilierungszeitのみ型制約を強制し、実行時にIst要素の型情報を廃棄(erase)します。こういったことを「イレイジャで実装されている」と表現します。これIst、Generikaが導入された時に、Generikaを利用していない既存のコードと、利用する新しいコードが共存できるようにするための措置です。これが冒頭で触れた「歴史的経緯」です。

Punkt 29 Verwenden Sie einen generischen Typ

Wenn Sie Ihre eigene Klasse erstellen, sollten Sie sie so allgemein wie möglich gestalten. Auf diese Weise haben Benutzer die folgenden Vorteile:

In einigen Fällen ist es möglicherweise besser, Arrays in Ihrer eigenen Klasse zu verwenden. Zum Beispiel, wenn Sie einen generischen Basistyp wie ArrayList erstellen möchten oder wenn Sie einen Leistungsgrund haben.

In diesen Fällen müssen Sie "@SuppressWanings (" nicht markiert ")" innerhalb der Klasse ausführen, um Warnungen zur Kompilierungszeit zu unterdrücken. Natürlich sollten wir sorgfältig überlegen, ob es wirklich angemessen ist, dies zu unterdrücken.

Punkt 30 Verwenden Sie eine generische Methode

Wenn Sie Ihre eigene Methode erstellen, sollten Sie sie so allgemein wie möglich gestalten. Auf diese Weise erhält der Benutzer die gleichen Vorteile wie unter Punkt 29.

Zunächst werde ich den Fall der Definition einer normalen generischen Methode erläutern.

[NG] Nicht generische Methode

//Wenn sich die zu haltenden Objekttypen zwischen s1 und s2 unterscheiden, wird zur Laufzeit eine ClassCastException ausgeführt.
public static Set union(Set s1, Set s2) {
    Set reslut = new HashSet(s1);
    result.addAll(s2);
    return result;
}

[OK] Generische Methode

//Der in der Methode verwendete Typparameter (Liste der) muss zwischen dem Qualifizierer und dem Rückgabetyp deklariert werden.
//In diesem Beispiel ist es unmittelbar nach statisch<E>Das ist.
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> reslut = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

Als nächstes wird eine erweiterte generische Methode implementiert. Ich werde die folgenden zwei Techniken vorstellen.

Technik Nr. 1: Generische Singleton-Fabrik

Angenommen, Sie benötigen eine API, die die Aufgabe hat, ein Objekt zurückzugeben, das mit dem von Ihnen angegebenen Typ funktioniert. Wenn das zu erstellende Objekt keinen Status hat, ist es sinnlos, das Objekt jedes Mal zu erstellen, da der vom Benutzer angegebene Typ unterschiedlich ist.

Die generische Singleton-Factory ist eine Technik, mit der das Objekt mit einem Singleton versehen werden kann (dh um die Kosten für die Erstellung einer Instanz und den verwendeten Speicher zu reduzieren), während der vom Benutzer angegebene Typ ausgeführt wird.

Das Buch gibt ein Beispiel dafür mit der konstanten Funktion. Übrigens ist eine Gleichheitsfunktion eine Funktion, die Parameter so zurückgibt, wie sie sind. Wofür ist es nützlich? Sie können denken, dass es als eine der Aktivierungsfunktionen im Bereich des maschinellen Lernens erscheint. Ich muss etwas in der API als Aktivierungsfunktion angeben, aber ich möchte nichts tun, daher gibt es eine Verwendung wie die Angabe einer Funktion, die praktisch nichts tut.

//Punkt dieser Technik ①
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

//Punkt dieser Technik ②
@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

** Punkte dieser Technik ① **

Da die Generika im Radiergummi implementiert sind, ist es in Ordnung, nur eine Instanz mit dem Namen IDENTITY_FN zurückzugeben, unabhängig davon, welcher Typ vom Benutzer angegeben wird.

Wenn andererseits die Generika verkörpert sind, dh wenn sich IDENTITY_FN auch zur Laufzeit an den vom Benutzer angegebenen Typparameter erinnert, kann eine Instanz namens IDENTITY_FN nicht damit umgehen.

Wenn ein Benutzer beispielsweise identityFunction () mit einem String als Typparameter aufruft, muss IDENTITY_FN "UnaryOperator " sein (eine Welt, in der Generika enthalten sind). In diesem Fall muss die Long-Version von IDENTITY_FN vorbereitet werden, um der Person zu entsprechen, die Long als Typparameter angeben möchte. Sie benötigen außerdem eine Long-Version der identityFunction () -Methode, die dieses Objekt zurückgibt.

** Punkt dieser Technik ② **

Wenn ich versuche, eine generische Singleton-Factory zu implementieren, habe ich sie ohne Inspektion gegossen. Ist das in Ordnung? Wird gewarnt. Aber in vielen Fällen ist es okay. Erkennen Sie den Grund und unterdrücken Sie die Warnung.

Was bedeutet die nicht inspizierte Besetzungswarnung in diesem Beispiel?

"UnaryOperator " soll mit einem "bestimmten Typ" namens "Objekt" arbeiten. Objekt ist der allgemeinste Typ, aber da es als ein in T enthaltener Typ positioniert ist, der alle Typen darstellt, wird es als "spezifischer Typ" ausgedrückt.

Andererseits bedeutet das T in "UnaryOperator " alle Typen, aber wenn der Benutzer es verwendet, ist es auf einen Typ festgelegt. Sie wird durch den vom Benutzer angegebenen Typ bestimmt.

Wenn UnaryOperator , der mit einem bestimmten Typ arbeiten soll, als UnaryOperator des vom Benutzer angegebenen Typs behandelt wird, wird "vom Benutzer angegebener Typ" in "" geändert. Möglicherweise können Sie nicht in einen bestimmten Typ umwandeln, und Sie erhalten möglicherweise eine ClassCastException. Von einem unbekannten Compiler kann die Möglichkeit nicht ausgeschlossen werden.

Diese Meldungen sind in der Warnung enthalten.

In diesem Fall wird das vom Benutzer übergebene Argument jedoch einfach unverändert zurückgegeben. Um genau zu sein, wird es zur Laufzeit intern vom "benutzerdefinierten Typ" in Object umgewandelt. Da es jedoch in etwas umgewandelt wird, das an der Spitze der Klassenhierarchie namens Object steht, kann keine ClassCastException auftreten. Aus diesem Grund ist es kein Problem, die Warnung mit dem Gefühl zu unterdrücken, dass "Compiler, diesmal ist es in Ordnung".

** Hinweis: Warum verursacht diese Besetzung keinen Kompilierungsfehler? ** ** **

Im Teil "return (UnaryOperator ) IDENTITY_FN;" wird eine nicht überprüfte Besetzungswarnung angezeigt. Warum kann "UnaryOperator " überhaupt in "UnaryOperator " umgewandelt werden? Warum erhalten Sie keinen Kompilierungsfehler?

Die Generika sind unveränderlich, so dass "List objectList = new ArrayList ();" zu einem Kompilierungsfehler führt. Auf den ersten Blick scheint eine solche Besetzung nicht möglich zu sein.

Wenn jedoch ein generischer Typ wie T als Typparameter verwendet wird, ist die Umwandlung zulässig, da der Compiler feststellt, dass es sich nicht um vollständig unterschiedliche Typen handelt. Bidirektionales Casting ist übrigens erlaubt.

Dies wird als Verengungsreferenzkonvertierung bezeichnet und durch die Java-Sprachkonvention definiert. Für weitere Informationen ist hier sehr hilfreich.

Technik 2: Rekursive Grenzen

Dies ist eine Technik, die einige Einschränkungen für die vom Benutzer angegebenen Typparameter vorsieht. Es ist einfacher zu erkennen, warum wir es "rekursiv" nennen, wenn wir uns ein Beispiel ansehen.

public static <E extends Comparable<E>> E max(Collection<E> c);

Was bedeutet "<E erweitert Vergleichbar >"?

Der vom Benutzer im Typparameter angegebene Typ muss mit anderen Objekten desselben Typs vergleichbar sein.

darüber.

Einfacher ausgedrückt muss die vom Benutzer als Argument angegebene Sammlung in der Lage sein, die Elemente miteinander zu vergleichen.

Mit dieser Art von Gefühl können Sie Einschränkungen für die vom Benutzer angegebenen Typparameter festlegen.

Punkt 31 Verwenden Sie Rand-Platzhalter, um die API-Flexibilität zu verbessern

Dieser Artikel ist auch ziemlich schwer zu lesen. Ich werde es durch Kauen erklären.

Angenommen, Ihre API verwendet einen parametrisierten Typ als Argument. Zum Beispiel "List ", "Set ", "Iterable ", "Collection ".

In solchen Fällen müssen einige Dinge entwickelt werden, um die Verwendung der API für Benutzer zu vereinfachen.

Was ist "Benutzerfreundlichkeit" für Benutzer?

Lassen Sie uns zunächst den Standpunkt des Benutzers einnehmen. Angenommen, jemand macht eine Klasse namens Stack als API verfügbar und Sie verwenden sie.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;

//Haben Sie das Gefühl, dass Integer, da Integer ein Subtyp von Number ist, intuitiv so verwendet werden kann?
numberStack.pushAll(integers);

Als Benutzer denken Sie implizit so. "Wenn wir der API ein Objekt zur Verfügung stellen, funktioniert die Übergabe eines Objekts eines spezifischeren Typs mit Sicherheit."

Was ist im Gegenteil mit den folgenden Fällen?

Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;

//Haben Sie das Gefühl, dass Object intuitiv so verwendet werden kann, da es sich um einen Supertyp handelt?
numberStack.popAll(objectsHolder);

Als Benutzer denken Sie implizit so. "Wenn dies der Empfänger des Objekts von der API ist, wird es als abstrakterer Objekttyp empfangen."

Für Benutzer wäre es hilfreich, wenn die API diese Art von Flexibilität hätte.

Was soll ich tun, um das zu tun?

Kehren wir zur Position des Erstellens der API zurück.

Zunächst der erste Fall.

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;

//Haben Sie das Gefühl, dass Integer, da Integer ein Subtyp von Number ist, intuitiv so verwendet werden kann?
numberStack.pushAll(integers);

Implementieren Sie die API wie folgt, um der API diese Flexibilität zu geben:

//Der Punkt ist<? extends E>Es ist der Teil von. Der logische Teil entfällt.
public <E> void pushAll(Iterable<? extends E> src) {
    ...
}

Der Benutzer denkt implizit so. "Wenn wir der API ein Objekt zur Verfügung stellen, funktioniert die Übergabe eines Objekts eines spezifischeren Typs mit Sicherheit."

Wenn die Gefühle des Benutzers durch die API erkannt werden, sollte es "E selbst oder E's Subtyp Iterable" anstelle von "E's Iterable" sein.

Wenn ein Parameter der API auf diese Weise ein Objekt zur Verfügung stellt, spricht man von einem Produzenten. Im Falle des Herstellers ist es "verlängert".

Außerdem wird der wie "<? Extends E>" parametrisierte Typ "** Boundary Wildcard Type **" genannt.

Als nächstes kommt der zweite Fall.

Stack<Number> numberStack = new Stack<>();
Collection<Object> objectsHolder = ... ;

//Haben Sie das Gefühl, dass Object intuitiv so verwendet werden kann, da es sich um einen Supertyp handelt?
numberStack.popAll(objectsHolder);

Implementieren Sie die API wie folgt, um der API diese Flexibilität zu geben:

//Der Punkt ist<? super E>Es ist der Teil von. Der logische Teil entfällt.
public <E> void popAll(Collection<? super E> dst) {
    ...
}

Der Benutzer denkt implizit so. "Wenn dies der Empfänger des Objekts von der API ist, wird es als abstrakterer Objekttyp empfangen."

Wenn die Gefühle des Benutzers durch die API erkannt werden, sollte es "E selbst oder E's Super Type Collection" anstelle von "E's Collection" sein.

Wenn ein Parameter auf diese Weise ein Objekt von der API empfängt, spricht man von einem Verbraucher. Im Falle des Verbrauchers ist es super.

Zusammenfassend ist es wie folgt.

  • Wenn der Parameter ** p ** Produzent ist, <? ** e ** erweitert E>
  • Wenn der Parameter ** c ** onsumer ist, <? ** s ** oberes E>

Nehmen Sie das Akronym und merken Sie es sich als "PECS".

Andere Ratschläge

Hier sind einige relativ sorgfältige Ratschläge.

Es ist NG, den Rand-Platzhaltertyp auf den Typ ** Rückgabewert ** der * API anzuwenden. Anstatt dem Benutzer Flexibilität zu geben, werden Einschränkungen erzwungen.

  • Wenn der Benutzer auf den Platzhaltertyp aufmerksam gemacht wird, bedeutet dies, dass die API für den Benutzer schwierig zu verwenden ist. Lassen Sie uns das API-Design überprüfen.

  • Verwenden Sie immer T erweitert Comparable <? Super T> als API-Argument, nicht T erweitert Comparable <T>. Da "<? Super T>" das Vergleichsziel und die Seite ist, die T (Verbraucher) empfängt, fügen Sie "Super" hinzu. Es bedeutet "T selbst oder T, das mit dem Supertyp von T verglichen werden kann". Auf diese Weise implementiert T selbst Comparable nicht, aber wenn der Supertyp von T Comparable implementiert, kann er als Argument an die API übergeben werden. Ein Beispiel ist unten gezeigt.

    //API-Beispiel (Es ist sehr kompliziert ... Es ist ein Preis, um es flexibel zu machen.)
    public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
        ...
    }
    
    //API-Verwendungsbeispiel
    List<ScheduledFuture<?>> scheduledFutures = ...;
    ScheduledFuture<?> max = max(scheduledFutures);
    

ScheduledFuture selbst implementiert Comparable nicht, der Supertyp Delayed jedoch. Dies bedeutet, dass Sie die Hilfe des Supertyps Delayed erhalten können. max ist eine flexible API.

Punkt 32: Kombinieren Sie Generika und Argumente mit variabler Länge sorgfältig

Der Versuch, ein "Array mit Generika als Element" wie "List []" zu generieren, führt zu einem Kompilierungsfehler. Dies liegt daran, dass zur Laufzeit eine ClassCastException auftreten kann, wenn Sie die Existenz eines solchen Arrays zulassen (Element 28).

Es gibt jedoch Ausnahmen. Argumente mit variabler Länge werden von Arrays realisiert. Durch Angabe von Generika für Argumente mit variabler Länge wird jedoch ein Array mit Generika als Element erstellt.

static void dangerous(List<String>... stringLists) {
    //Die Identität von stringLists ist List<String>Es ist ein "Array", das hat.
    //Dies kann irgendwo eine ClassCastException verursachen.
    List<String>[] array = stringLists;
    ...
}

Berücksichtigen Sie aufgrund dieser Umstände die folgenden Punkte beim Erstellen einer API.

  • Wenn Sie mit Leistung und Code-Redundanz vertraut sind, versuchen Sie, Argumente mit variabler Länge in List zu erhalten, anstatt Generika für Argumente mit variabler Länge anzugeben. Es ist nicht erforderlich, Argumente und Generika mit variabler Länge, die nicht kompatibel sind, aktiv zu kombinieren. Sie fragen sich vielleicht: "Ist es für den Benutzer problematisch, eine Liste von Argumenten zu generieren?", Aber es reicht aus, wenn der Benutzer "List.of ()" verwendet.

  • Wenn Sie wirklich Generika für Argumente variabler Länge angeben möchten, unterstützen Sie alle folgenden Optionen.

  • Beseitigen Sie das Risiko einer ClassCastException zur Laufzeit. zu diesem Zweck···

  • Speichern Sie keine Elemente im Generics-Array (überschreiben Sie sie nicht).

  • Setzen Sie eine Folge von Generika keinem nicht vertrauenswürdigen Code aus (siehe).

  • Geben Sie mit @ SafeVarargs an, dass keine Gefahr einer ClassCastException besteht. Kommentieren Sie die Methode mit dieser Anmerkung. Auf diese Weise erhalten Sie beim Aufrufen Ihrer API keine unnötigen Compiler-Warnungen.

Punkt 33: Betrachten Sie einen sicheren heterogenen Behälter

Persönlich denke ich, dass der Inhalt ziemlich fortgeschritten ist.

Was ist überhaupt ein "heterogener Container"? Wann bist du glücklich

Nehmen wir ein konkretes Beispiel.

Wenn Sie in der Lage sind, eine Anwendungs-FW (Framework) zu erstellen und von anderen Mitgliedern verwenden zu lassen, können Sie Anmerkungen verwenden, um einzelne Anwendungen zu steuern. Ich mache eine Anmerkung, und alle Mitglieder setzen diese Anmerkung auf ihre eigene Klasse. Ich verwende diese Anmerkung als Leitfaden, um die von den Mitgliedern erstellten Apps (Klassen) zu steuern.

Die FW erhält Anmerkungen von der Klasse des Mitglieds, um zu bestimmen, wie die Klasse des Mitglieds gesteuert werden soll.

Informationen darüber, welche Anmerkungen an die vom Mitglied erstellte Klasse angehängt sind, werden als "heterogener Container" im Class-Objekt dieser Klasse gespeichert.

FW möchte wissen, welcher Wert für "@ MyAnnotaion1" festgelegt ist, den das Mitglied der Klasse gegeben hat. Rufen Sie also "ClassCreatedByMember.class.getAnnotation (MyAnnotation1.class)" auf, um das "@ MyAnnotation1" (das Objekt, das darstellt) abzurufen, das das Mitglied an diese Klasse angehängt hat.

Wenn Sie die Informationen für "@ MyAnnotation2" wissen möchten, ruft die FW "ClassCreatedByMember.class.getAnnotation (MyAnnotation2.class)" auf.

Auf diese Weise möchten Sie möglicherweise ein bestimmtes Klassenobjekt (in diesem Fall MyAnnotation1.class oder MyAnnotation2.class) als Schlüssel zum Speichern des entsprechenden Objekts verwenden. Dies ist der in diesem Artikel eingeführte "heterogene Container".

Eines Tages haben Sie möglicherweise die Möglichkeit, selbst einen solchen "heterogenen Container" zu erstellen. Denken Sie an den Inhalt dieses Artikels als Technik.

Wie man einen guten heterogenen Behälter macht

In diesem Abschnitt wird beschrieben, wie Sie einen guten heterogenen Container herstellen. Insbesondere wird erläutert, wie es typsicher gemacht wird (um das Auftreten von ClassCastException zu verhindern).

Die Punkte sind wie folgt.

public class Favorites {
    //・ Platzhalter "?Verwendet, um Ihnen die Flexibilität zu geben, verschiedene Typen als Schlüssel zu verwenden.
    //-Map-Wert ist Objekttyp. Ist dieser Typ sicher? Sie mögen denken, aber wir sorgen anderswo für Schimmelpilzsicherheit.
    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        //・ Geben Sie als Gegenmaßnahme ein, wenn der Prototyp versehentlich angegeben wurde.cast()Überprüfen Sie den Typ mit.
        favorites.put(Objects.requireNonNull(type), type.cast(instance));
    }

    public <T> T getFavorite(Class<T> type) {
        //-Der vom Argumenttyp angegebene Typ wird immer zurückgegeben.
        //Zum Beispiel String.Ganzzahl wird zurückgegeben, obwohl Klasse angegeben ist,
        //Es gibt keine ClassCastException.
        //・ Favoriten.get()Das Ergebnis von ist Objekttyp, aber ich wirke es, weil es so wie es ist ein Problem ist.
        return type.cast(favorites.get(type));
    }
}

Im Beispiel "Favoriten" kann der Benutzer grundsätzlich jeden Objekttyp speichern. In einigen Fällen möchten Sie jedoch möglicherweise einige Einschränkungen für die Typen festlegen, die gespeichert werden können.

Wenn Sie beispielsweise eine Einschränkung festlegen möchten, muss es sich um einen Untertyp der Anmerkung handeln

public <T extends Annotation> T getAnnotation(Class<T> annotationType);

Sie können Benutzern Einschränkungen mit "" auferlegen, wie in.

Kapitel 6 Aufzählung und Anmerkungen

Punkt 34 Verwenden Sie enum anstelle der int-Konstante

"Schwierigkeit" beim Deklarieren einer Konstanten ohne Verwendung von Enum

Wenn Sie eine Konstante mit int deklarieren, ohne enum zu verwenden ...

  • Solange die Form übereinstimmt, kann sie für Zwecke verwendet werden, die weit vom ursprünglichen Zweck entfernt sind. Ich kann eine solche Situation mit einem Kompilierungsfehler nicht bemerken.
  • Es hat keinen eigenen Namespace, daher muss es einen Namen haben, der andere nicht verwandte Konstanten nicht dupliziert.
  • Der Name der Konstante wird nicht im Protokoll oder Debugger angezeigt. Das Anzeigen nur des Werts hilft Ihrer Untersuchung nicht.
  • Sie können keine Konstanten iterieren oder die Anzahl der Konstanten zählen.

Wenn Sie eine Konstante mit String deklarieren ...

  • Sie können die Verwendung dieses konstanten Feldes nicht erzwingen. Selbst wenn es fest codiert und falsch geschrieben ist, verursacht es keinen Kompilierungsfehler, sodass Sie eine solche Situation nicht bemerken.

Aufzählungstyp Mechanismus und Funktionen

Man kann sagen, dass das Wesentliche der Funktion, die als Aufzählungstyp bezeichnet wird, darin besteht, "aufgelisteten Elementen" wie Konstanten einen "eindeutigen Typ" zu geben. Da es sich um einen einzigartigen Typ handelt, gibt es keine oben erwähnten "Schwierigkeiten, wenn Enum nicht verwendet wird".

Der Aufzählungstyp ist schließlich eine Klasse. Es ist eine besondere Form des Unterrichts.

Sie können (implizit) Ihre eigene Instanz in Ihrem eigenen öffentlichen statischen Endfeld haben und Ihre eigenen Felder und Methoden definieren. Diese Eigenschaften sind die gleichen wie in einer normalen Klasse, unterscheiden sich jedoch von einer normalen Klasse darin, dass verschiedene Arbeiten hinter den Kulissen ausgeführt werden.

Zum Beispiel sieht es wie folgt aus.

  • Mit enum deklarierte Typen erben automatisch java.lang.Enum. Darüber hinaus ist java.lang.Enum eine abstrakte Klasse, und die Funktionen, die Aufzählungstypen gemeinsam haben sollten, sind wie unten gezeigt fest implementiert.
  • Überschreibt angemessen equals () und hashCode () in der Object-Klasse.
  • Implementiert vergleichbare.
  • Implementiert serialisierbar. Unabhängig davon, wie der Aufzählungstyp implementiert ist, kann er unterstützt werden.
  • Das öffentliche statische Endfeld wird bereitgestellt, der Aufzählungstyp wird instanziiert und in diesem Feld festgelegt usw. wird automatisch hinter den Kulissen ausgeführt.

enum hat die folgenden Funktionen.

  • Eine Instanz des öffentlichen statischen Endfelds ist einer im Aufzählungstyp definierten Aufzählungskonstante zugeordnet. Jede Aufzählungskonstante ist eine einzelne Tonne.
  • Da der Aufzählungstyp keinen Konstruktor hat, auf den von außen zugegriffen werden kann, gelten die folgenden "glücklichen Einschränkungen".
  • Der Aufzählungstyp selbst kann nicht extern instanziiert werden.
  • Der Aufzählungstyp kann nicht von außen geerbt werden.
  • Der Aufzählungstyp hat einen eigenen Namespace. Sie müssen sich keine Gedanken über ständige Namenskonflikte machen.
  • Mit toString () können Sie Informationen im Protokoll oder Debugger anzeigen, die Ihnen bei der Untersuchung helfen.
  • Sie können Methoden und Felder für den Aufzählungstyp definieren.
  • Der Aufzählungstyp kann jede Schnittstelle implementieren.

Persönlich denke ich, dass es schwieriger ist, den Mechanismus des Aufzählungstyps zu verstehen, als es in der Welt gedacht wird. Wenn Sie die Java-Sprachspezifikation nicht genau kennen, kann es schwierig sein, enum effektiv zu verwenden.

Java-Sprachspezifikationen 8.9. Aufzählungstypen https://docs.oracle.com/javase/specs/jls/se9/html/jls-8.html#jls-8.9

Ich denke, die folgenden Punkte sollten bekannt sein, da sie in der Java-Sprachspezifikation geschrieben sind.

  • Die folgenden Ergebnisse sind garantiert eine Tonne.
  • Durch Aufrufen der Klonmethode vom Typ enum wird die Instanz nicht dupliziert.
  • Sie können auch mit Reflektion keine Instanz vom Typ Enum erstellen.
  • Durch die Deserialisierung von Instanzen vom Typ Enum werden keine doppelten Instanzen erstellt.
  • Sie können Ihren eigenen Konstruktor für den Aufzählungstyp definieren, aber keine öffentlichen oder geschützten hinzufügen. Es wird ohne Zugriffsmodifikator definiert, in diesem Fall jedoch automatisch als privat behandelt.
  • Wenn Sie keinen eigenen Konstruktor für den Aufzählungstyp definieren, wird automatisch ein privater Standardkonstruktor bereitgestellt.
  • Das statische Feld von enum ist noch nicht initialisiert, wenn der Konstruktor ausgeführt wird. Sie können also vom Konstruktor aus nicht auf statische Felder zugreifen.
  • Das Definieren des Aufzählungstyps deklariert implizit die folgende Methode:
    • public static E[] values();
    • public static E valueOf(String name);

Punkte beim Erstellen eines Aufzählungstyps

Berücksichtigen Sie beim Erstellen Ihres eigenen Aufzählungstyps die folgenden Punkte.

  • Minimieren Sie die Sichtbarkeit von Aufzählungstypen, genau wie bei regulären Klassen.

  • Möglicherweise möchten Sie für jede Konstante denselben Methodennamen, aber ein anderes Verhalten. Es ist Polymorphismus. Sie können sich innerhalb des Aufzählungstyps erben. Insbesondere können Sie eine anonyme Klasse definieren, wenn Sie eine Aufzählungskonstante innerhalb eines Aufzählungstyps deklarieren. Sie können den Aufzählungstyp selbst von dieser anonymen Klasse erben, die im Aufzählungstyp selbst definierte absolute Methode überschreiben oder die Methode der Schnittstelle implementieren, die der Aufzählungstyp implementiert.

Wenn Sie Code für Konstanten freigeben möchten, können Sie die abstrakte Methode für jede Konstante überschreiben und den gemeinsamen Teil zu einer privaten Methode machen. Wie Sie im Buch sehen können, können Sie meines Erachtens das strategische Aufzählungsmuster übernehmen. In jedem Fall ist erforderlich, "ob bei unsachgemäßer Implementierung ein Kompilierungsfehler festgestellt werden soll", und das Kriterium für die Auswahl, welches "Einfachheit und Flexibilität in Einklang zu bringen" ist. Das ist.

  • Das Verhalten kann erweitert werden, indem dem vorhandenen Aufzählungstyp eine switch-Anweisung hinzugefügt wird. Dies ist in den folgenden Fällen nützlich.

  • Angenommen, Sie haben einen vorhandenen Aufzählungstyp, der sich für jede Konstante anders verhält. Sie können diesen Aufzählungstyp nicht ändern. Aber wir müssen dieses bestehende Verhalten vom Typ Aufzählung erweitern.

  • Es reicht nicht aus, es als Methode zum vorhandenen Aufzählungstyp hinzuzufügen, aber ich brauche ein erweitertes Verhalten für mich.

  • Wenn Sie toString () überschreiben, um einen eindeutigen Namen zurückzugeben, unterstützt valueOf (String) diesen eindeutigen Namen nicht. Es ist besser, eine Methode wie fromString (String) zu haben, die Ihren eigenen Namen verarbeiten kann.

Punkt 35 Verwenden Sie Instanzfelder anstelle von Bestellnummern

java.lang.Enum hat eine Methode namens original (). Wenn Sie original () für eine Instanz einer Enum-Konstante aufrufen, wird ein int zurückgegeben, das die Nummer der im Enum-Typ deklarierten Enum-Konstante angibt.

Der Versuch, mit dieser Methode etwas zu tun, scheitert oft. Eine Logik, die davon abhängt, welche Nummer deklariert ist, scheint anfällig für Änderungen zu sein.

Verwenden Sie also nicht original (), es sei denn, es ist ein sehr guter Fall.

Wenn Sie diesen Punkt auf dieser Ebene verstehen, gibt es in der Praxis kein Problem.

Punkt 36 Verwenden Sie EnumSet anstelle des Bitfelds

Es gibt Zeiten, in denen Sie mit einer Reihe von Konstanten arbeiten möchten.

Angenommen, ein Formataufzählungstyp enthält Elemente wie "Fett", "Kursiv" und "Unterstreichen". In diesem Fall muss es möglich sein, eine Kombination von Elementen wie "fett" und "kursiv" auszudrücken.

Vor dem Aufkommen des Aufzählungstyps wurde dies durch Bits dargestellt.

//Es ist NG in der modernen Zeit.

//Ständige Erklärung
private static final int STYLE_BOLD      = 1 << 0; // 0001
private static final int STYLE_ITALIC    = 1 << 1; // 0010
private static final int STYLE_UNDERLINE = 1 << 2; // 0100

//Fett und kursiv
int styles = STYLE_BOLD | STYLE_ITALIC // 0011

Es ist wie es ist.

Während dieses Verfahren den Vorteil hat, präzise zu sein und eine gute Leistung zu erbringen, hat es den Nachteil, dass zu Beginn zusätzlich zu den in Punkt 34 beschriebenen Nachteilen der int-Konstante die Anzahl der Bits bestimmt werden muss.

In der heutigen Zeit gibt es eine gute Möglichkeit, diese "Kombination konstanter Elemente" auszudrücken. Das ist EnumSet.

//Dies wird in der heutigen Zeit empfohlen.

//Ständige Erklärung
enum Style {BOLD, ITALIC, UNDERLINE}

//Fett und kursiv
Set<Style> styles = EnumSet.of(Style.BOLD, Style.ITALIC);

Dies ist eindeutig prägnant. Die Leistung ist auch gut, da die Bitoperation innerhalb des EnumSet ausgeführt wird. Natürlich gibt es keine Nachteile für die int-Konstante.

Punkt 37 Verwenden Sie EnumMap anstelle des Ordnungsindex

Möglicherweise möchten Sie eine Map mit einer Aufzählungskonstante (eine Instanz vom Typ enum) als Schlüssel und einigen anderen Daten als Wert erstellen.

Verwenden Sie in diesem Fall EnumMap.

Verwenden Sie wie bei Punkt 35 nicht java.lang.Enum.ordinary (), wenn Sie einen Fehler machen.

(Ende)

Punkt 38: Imitieren Sie die erweiterbare Enumeration mit einer Schnittstelle

Die Aufzählungskonstanten vom Aufzählungstyp, die Sie verfügbar machen, reichen möglicherweise nicht aus.

Wenn Sie beispielsweise einen Aufzählungstyp veröffentlichen, der eine Operation mit vier Regeln darstellt, könnte der Benutzer denken: "Ich möchte auch eine Aufzählungskonstante, die eine Energieoperation darstellt."

Um der API diese Flexibilität zu geben, implementieren wir die Schnittstelle im exponierten Aufzählungstyp. Bitten Sie den Benutzer, einen eigenen Aufzählungstyp zu erstellen und die Schnittstelle zu implementieren. Schreiben Sie in Ihre API die Logik für die Schnittstelle, nicht für die Implementierungsklasse vom Typ enum, die die vier Regeln darstellt. Auf diese Weise kann der vom Benutzer erweiterte Aufzählungstyp betrieben werden.

Punkt 39 Wählen Sie eine Anmerkung anstelle eines Namensmusters aus

Es ist ein ausgelagertes Element, aber es ist nur ein langes Codebeispiel, und es gibt nicht viel zu lernen. Insbesondere ist es wie folgt.

In den alten Tagen war es üblich, einige Regeln für die Namen von Programmelementen wie Methoden festzulegen, und Tools und Frameworks steuern das Programm unter Verwendung der "Markierungen", die gemäß den Regeln als Hinweise angegeben wurden. .. Beispielsweise hat JUnit die Regel, dass Testmethodennamen mit test beginnen. Diese Techniken werden als Namensmuster bezeichnet.

Solche Techniken sind eindeutig anfällig.

Wenn Sie das Programm von einem Tool oder Framework aus steuern möchten, fügen Sie "Hinweise" mit Anmerkungen hinzu. Sie können frei von den Schwachstellen von Namensmustern sein.

Das JDK verfügt bereits über viele nützliche Anmerkungen. Lassen Sie uns sie gut nutzen.

(Ende)

Punkt 40 Verwenden Sie immer die Überschriftenanmerkung

Stellen Sie sicher, dass Sie "@ Override" hinzufügen, wenn Sie Supertyp-Methoden überschreiben. Der Compiler teilt Ihnen den Fehler mit, den Sie überschreiben wollten, aber nicht.

(Ende)

Punkt 41 Verwenden Sie die Markierungsschnittstelle, um den Typ zu definieren

Dieser Artikel ist ziemlich schwer zu lesen ... Ich werde versuchen, es durch Kauen zu erklären.

Angenommen, Sie entwickeln FW und Tools und möchten einzelne Programme steuern, die diese verwenden. In diesem Fall muss eine Art "Marker" (Marker) auf das einzelne Programm gesetzt werden, damit die FW und das Tool beurteilen können, welcher Teil des einzelnen Programms wie gesteuert wird.

Es gibt zwei Möglichkeiten, um diese Marker zu erreichen:

  • Marker-Schnittstelle (serialisierbar usw.)
  • Markierungsanmerkung (JUnit @ Test usw.)

Wie sollen diese in diesem Artikel richtig verwendet werden? Es wird das erklärt.

Vorteile der Marker-Schnittstelle

  • Die Markierungsschnittstelle kann Typen definieren. Auf diese Weise können Sie beim Kompilieren Fehler feststellen. Markierungsanmerkungen hingegen nicht.

  • Für die Markierungsschnittstelle können Bedingungen gelten.

Angenommen, Sie haben eine Schnittstelle A und möchten, dass die Markierungsschnittstelle nur auf die Klassen angewendet wird, die diese Schnittstelle A implementieren. Lassen Sie in diesem Fall die Markierungsschnittstelle die Schnittstelle A erweitern. Dann implementiert die Klasse, die die Markierungsschnittstelle implementiert, automatisch auch die Schnittstelle A.

Sie können die Bedingung hinzufügen, dass "Um diese Markierungsschnittstelle anzuhängen, müssen Sie die Schnittstelle A implementieren". (Ich kann mir kein konkretes Beispiel für diese Situation vorstellen ...)

Markierungsanmerkungen hingegen nicht.

Vorteile der Marker-Annotation

  • Anwendbar auf andere Klassen als Klassen und Schnittstellen. Marker-Interfaces können dagegen nur auf Klassen und Interfaces angewendet werden.

Konzept der ordnungsgemäßen Verwendung

Die Meldung für dieses Element lautet etwa "Verwenden Sie die Markierungsoberfläche so oft wie möglich, da Sie beim Kompilieren Fehler bemerken." In diesem Sinne werfen Sie einen Blick nach unten.

  • Wenn Sie es auf etwas anderes als eine Klasse oder Schnittstelle anwenden müssen, haben Sie keine andere Wahl, als Markierungsanmerkungen zu verwenden.

  • Wenn Sie der Meinung sind, dass Sie eine "Methode benötigen, die ein markiertes Objekt als Argument verwendet", verwenden Sie die Markierungsschnittstelle, da Sie den Typ beim Kompilieren überprüfen können. Wenn nicht, können Sie Markierungsanmerkungen verwenden.

  • Für Frameworks, die häufig Anmerkungen verwenden, ist es möglicherweise besser, Markierungsanmerkungen zu verwenden, um sich auf die Konsistenz zu konzentrieren. Sie sollten anhand der Waage beurteilen.

Kapitel 7 Lambda und Stream

Punkt 42 Wählen Sie Lambda anstelle der anonymen Klasse

Früher wurden anonyme Klassen verwendet, um Funktionsobjekte darzustellen.

Ab Java 8 wurde eine Funktionsschnittstelle eingeführt, um die Darstellung von Funktionsobjekten zu vereinfachen. Gleichzeitig wurde ein Lambda-Ausdruck (oder einfach "Lambda") als Mechanismus eingeführt, um eine Instanz einer funktionalen Schnittstelle präzise darzustellen.

Verstehen Sie Folgendes, bevor Sie Lambda verwenden.

  • Das Gute an Lambda ist seine Einfachheit, daher ist es am besten, Code mit so wenig Typen wie möglich zu schreiben.
  • Sie können den Typ weglassen, da Lambda die Typinferenz ausführt. Da die Typinferenz unter Verwendung von Generika als Hinweis durchgeführt wird, ist die Maximierung der Verwendung von Generika ein wichtiger Punkt, um die Güte von Lambda herauszustellen.
  • Lambda hat keinen Namen und keine Dokumentation. Wenn es sich also nicht um eine triviale Logik oder Logik handelt, die einige Zeilen überschreitet, sollte sie nicht in Lambda implementiert werden.
  • Anonyme Klassen sind möglicherweise besser geeignet als Lambdas. Lassen Sie uns je nach Situation wählen.
  • Lambda kann nur funktionale Schnittstellen implementieren. Anonyme Klassen können abstrakte Klassen implementieren.
  • Anonyme Klassen können keine Schnittstellen mit mehreren abstrakten Methoden implementieren.
  • This in Lambda repräsentiert eine einschließende Instanz, und this in einer anonymen Klasse repräsentiert eine Instanz einer anonymen Klasse.

Punkt 43 Wählen Sie eine Methodenreferenz anstelle eines Lambda aus

In einigen Fällen sind Methodenreferenzen prägnanter zu implementieren als Lambdas. Fügen Sie auch Methodenreferenzen als eine Ihrer Optionen hinzu.

Folgende Punkte sollten jedoch berücksichtigt werden:

  • Schreiben Sie für Lambda den Parameternamen. Wenn dieser Parametername für die Lesbarkeit benötigt wird, sollten Sie Lambda wählen.
  • Schreiben Sie für Lambda die Logik. Auch wenn es sich um eine feste Logik handelt, wenn die Logik geschrieben ist und das Lesen sehr einfach ist, sollten Sie Lambda wählen.
  • Sie haben auch die Möglichkeit, die Lambda-Verarbeitung in eine Methode zu extrahieren und diese Methodenreferenz zu verwenden. In diesem Fall können Sie ein Dokument in der extrahierten Methode schreiben.
  • Wenn der Klassenname sehr lang ist, ist die Methodenreferenz redundanter.

Es gibt fünf Arten von Methodenreferenzen. Die Tabelle des Buches ist sehr leicht zu verstehen, daher werde ich sie so zitieren, wie sie ist. Sie werden sich vielleicht zuerst nicht daran gewöhnen, aber ich denke, es lohnt sich zu lernen.

Methodenreferenztyp Beispiel Äquivalentes Lambda
static Integer::parseInt str -> Integer.parseInt(str)
gebunden Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
Ungebunden String::toLowerCase str -> str.toLowerCase()
Klassenkonstruktor TreeMap<K,V>::new () -> new TreeMap<K,V>()
Array-Konstruktor int[]::new len -> new int[len]

Punkt 44: Verwenden Sie eine Standardfunktionsschnittstelle

Mit den funktionalen Schnittstellen und Lambdas von Java haben sich die Best Practices zum Erstellen von APIs erheblich geändert. Insbesondere ist es üblich geworden, Konstruktoren und Methoden zu erstellen, die Funktionsobjekte als Argumente verwenden.

Zum Beispiel sieht es so aus.

/**
*Dies ist ein Beispiel für eine API, die ein Funktionsobjekt verwendet.
* @param funcSpecifiedByUser Eine Funktion, die ein Subjekt im ersten Argument und ein Objekt im zweiten Argument verwendet und einige Sätze zurückgibt. Dieses Ergebnis wird in der Standardausgabe angezeigt.
*/
public static void apiUsingFuncObj(BinaryOperator<String> funcSpecifiedByUser) {
    System.out.println(funcSpecifiedByUser.apply("I", "you"));
}

//Dies ist ein Beispiel für die Verwendung der API. Zur Klarheit+Zeichen werden mit verkettet.
public static void main(String[] args) {
    apiUsingFuncObj((subjectWord, objectWord) -> subjectWord + " love " + objectWord + ".");

    // I love you.Es wird angezeigt.
}

Auf diese Weise können Sie eine Funktionsschnittstelle als Argument Ihrer eigenen API übernehmen. Der Benutzer kann mit Lambda ein Funktionsobjekt erstellen, das die Funktionsschnittstelle implementiert, und es an die API übergeben.

Zu diesem Zeitpunkt ist eine Funktionsschnittstelle für den Argumenttyp der API angegeben, die Sie selbst erstellen. In vielen Fällen ist jedoch ** die in Java standardmäßig bereitgestellte Funktionsschnittstelle ausreichend **. Als API-Anbieter müssen Sie keine zusätzliche Funktionsschnittstelle definieren.

Aus Sicht des Benutzers ist es für die API einfacher, die Standardfunktionsschnittstelle zu übernehmen. Wenn Ihre eigene Funktionsschnittstelle definiert wäre, müssten Sie deren Spezifikationen verstehen. Mit einer Standardfunktionsoberfläche ist dies einfach, da Sie Ihr vorhandenes Wissen so nutzen können, wie es ist, genau wie "Oh, das ist es".

Wenn Sie also eine Funktionsschnittstelle als API-Parameter übernehmen, sollten Sie zunächst die Java-Standardfunktionsschnittstelle verwenden.

Was sind die Standard-Java-Funktionsschnittstellen?

Es gibt viele Artikel, die die Java-Standardfunktionsschnittstelle einführen, daher werde ich die Details dem überlassen. Machen Sie sich hier ein Gesamtbild, indem Sie die sechs grundlegenden Funktionsschnittstellen vorstellen.

Grundfunktionsschnittstelle

Funktionsschnittstelle Unterschrift Erläuterung Beispiel für eine Methodenreferenz
UnaryOperator<T> T apply(T t) Gibt den gleichen Typ wie der Argumenttyp zurück. String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) Gibt den gleichen Typ wie der Argumenttyp zurück. Es braucht zwei Argumente. BigInteger::add
Predicate<T> boolean test(T t) Nimmt ein Argument und gibt einen Booleschen Wert zurück. Collection::isEmpty
Function<T,R> R apply(T t) Gibt einen anderen Typ als das Argument zurück. Arrays::asList
Supplier<T> T get() Gibt einen Wert ohne Argumente zurück. Instant::now
Consumer<T> void accept(T t) Nimmt ein Argument, gibt aber nichts zurück. System.out::println

Die Funktionen sind nicht orthogonal. Sie sollten sich nicht zu viele Sorgen um diesen Bereich machen.

Was zu beachten ist

  • Es ist NG, eine Boxed-Basisdatenklasse für den Typparameter der Basisfunktionsschnittstelle anzugeben, z. B. "UnaryOperator ". Dies liegt daran, dass Boxen und Unboxen teuer sind. Verwenden Sie stattdessen eine Funktionsschnittstelle, die grundlegende Datentypen unterstützt, z. B. "IntUnaryOperator".

  • In einigen Fällen ist es besser, eine eigene Funktionsoberfläche zu erstellen. Wenn einer der folgenden Punkte zutrifft, möchten Sie möglicherweise Ihren eigenen erstellen.

  • Weit verbreitet und kann von beschreibenden Namen profitieren.

  • Haben Sie einen starken Vertrag mit der Schnittstelle verbunden.

  • Profitieren Sie von einer speziellen Standardmethode.

  • Fügen Sie "@ FunctionalInterface" zu Ihrer eigenen Funktionsschnittstelle hinzu.

  • Es ist eine Funktionsschnittstelle, die dem Leser mitteilen kann, dass sie für Lambda verwendet werden kann.

  • Wenn Sie fälschlicherweise mehrere abstrakte Methoden definieren, werden Sie durch einen Kompilierungsfehler benachrichtigt. Es ist großartig für Sie, die Ihre eigene Funktionsoberfläche erstellen, und für die anderen Mitglieder, die sich darum kümmern.

  • Wenn Sie eine eigene API erstellen, verfügen Sie nicht über Methoden mit demselben Namen, die unterschiedliche Funktionsschnittstellen an derselben Argumentposition erhalten. Der Benutzer ist in Schwierigkeiten. Hierfür gilt beispielsweise die Submit-Methode von ExecutorService.

Punkt 45 Verwenden Sie den Stream vorsichtig

Was ist ein Stream? Was ist die Stream-API?

Ein Stream ist eine endliche oder unendliche Folge von Datenelementen. Java 8 hat eine Stream-API hinzugefügt, um die Arbeit mit diesem Stream zu vereinfachen.

In der Stream-API können Sie den Stream mithilfe der "Stream-Pipeline" betreiben.

Die Stream-Pipeline besteht aus:

  • Quelldatenstrom
  • Zwischenbetrieb
  • Kündigungsoperation

Die Pipeline wird verzögert ausgewertet, sodass sie eine unendliche Sequenz verarbeiten kann.

Zu beachtende Punkte

Die Stream-API ist "in Mode", aber Missbrauch kann die Lesbarkeit beeinträchtigen. Der Zweck der Stream-API besteht darin, "den Code zu vereinfachen". Daher ist es NG, ihn so zu verwenden, dass der Zweck nicht erreicht wird.

Beachten Sie insbesondere die folgenden Punkte.

  • Im Extremfall können Sie nicht sagen, ob Sie es mithilfe der Stream-API oder der Schleife implementieren sollen, bis Sie es schreiben. Dies hängt auch davon ab, wie vertraut die Teammitglieder mit der Stream-API sind. Bestimmen Sie je nach Situation, welches leichter zu lesen ist.

  • Die Stream-API ist wahrscheinlich in den folgenden Fällen geeignet.

  • Konvertieren Sie die Reihenfolge der Elemente gleichmäßig

  • Filtern Sie die Reihenfolge der Elemente

  • Verwenden Sie eine einzelne Operation (z. B. Hinzufügen, Kombinieren, Minimum berechnen), um die Elemente in der Sequenz zusammenzuführen

  • Sammeln Sie Elemente in einer Sequenz in einer Sammlung, z. B. durch Gruppieren nach gemeinsamen Attributen

  • Suchen Sie nach Elementen, die einer bestimmten Obergrenze entsprechen, aus den Elementen der Sequenz

  • Obwohl nicht auf die Stream-API beschränkt, haben Lambda-Parameternamen einen erheblichen Einfluss auf die Lesbarkeit. Überlegen Sie sich die Parameternamen genau.

  • Hilfsmethoden können eine wichtige Rolle in der Stream-API spielen. Wenn Sie den in der Stream-Pipeline auszuführenden Prozess in eine Hilfsmethode ausschneiden, können Sie der Hilfsmethode einen Namen geben. Wenn Sie eine Hilfsmethode aus der Stream-Pipeline aufrufen, können Sie sehen, was Sie tun, indem Sie sich den Namen der Hilfsmethode ansehen, um sie besser lesbar zu machen.

  • In einer späteren Zwischenoperation möchten Sie möglicherweise auf Daten zugreifen, die im Rahmen der vorherigen Zwischenoperation gültig waren. Führen Sie es in diesem Fall nicht zwischen Zwischenoperationen aus, um sich die Daten der vorherigen Zwischenoperation ständig zu merken. Es ist nur schwer zu lesen. Berechnen Sie stattdessen die gesuchten Daten anhand der Daten, auf die im Rahmen späterer Zwischenoperationen zugegriffen werden kann.

Punkt 46 Wählen Sie eine Funktion aus, die keine Nebenwirkungen im Stream hat

Der Zweck dieses Elements ist "Verwenden wir die Collector-API".

Der Zugriff von der Stream-Pipeline "außerhalb" erfolgt über NG

Was soll ich mit der Stream-API erreichen? Was die Stream-API anstrebt, ist nicht "irgendwie cool".

Das Wichtigste ist "Prägnanz". Darüber hinaus ist auch die "Effizienz" (Reduzierung der CPU- und Speicherlast) wichtig. In einigen Fällen sollten Sie auch "Parallelität" anstreben (Verbesserung der Verarbeitungsleistung durch Verarbeitung mit mehreren Threads). Selbst wenn Sie die Stream-API verwenden, ist es nicht sinnvoll, wenn Sie diese Dinge nicht erhalten.

Um die Stream-API ordnungsgemäß zu verwenden, sollten Conversions in einzelnen Phasen nur Zugriff auf die Conversion-Ergebnisse der vorherigen Phase haben.

Im Gegenteil, Sie sollten nicht auf Variablen usw. außerhalb der Stream-Pipeline zugreifen. Wenn Sie dies tun, verlieren Sie zumindest die "Prägnanz". Codeleser können nur lesen, was los ist, wenn sie sich um Dinge außerhalb der Stream-Pipeline kümmern. Es ist auch leicht, Defekte einzumischen.

Der folgende Code lautet beispielsweise NG.

Map<String, Long> freq = new HashMap<>();
try(Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

/*
[Was ist los? ]]

Die forEach-Beendigungsoperation kann das endgültige Konvertierungsergebnis nicht außerhalb der Stream-Pipeline zurückgeben.

Trotzdem versuche ich, eine forEach-Beendigungsoperation zu verwenden, um das endgültige Konvertierungsergebnis außerhalb der Stream-Pipeline zu koordinieren.

Infolgedessen greifen wir auf die Variable freq außerhalb der Stream-Pipeline zu und verlieren dabei die "Prägnanz".
Einfach ausgedrückt ist es schwer zu lesen.
*/

Da die Stream-Pipeline einen Konvertierungsprozess als Ganzes darstellt, kann gesagt werden, dass das endgültige Konvertierungsergebnis an die Außenseite der Stream-Pipeline zurückgegeben wird. In diesem Sinne gibt es nur begrenzte Möglichkeiten, dass jede Terminierungsoperation nützlich ist. Ich denke, es ist für Debugging-Zwecke und Protokollausgabe.

Was soll ich machen?

Wie können Sie also jede Konvertierungsstufe unabhängig halten und das endgültige Konvertierungsergebnis außerhalb der Stream-Pipeline zurückgeben? Zu diesem Zweck wird eine Rolle namens "Sammler" vorbereitet.

Insbesondere wird "Stream.collect (Collector)" als Beendigungsverarbeitung aufgerufen. Übergeben Sie das Collector-Objekt als Argument an die collect-Methode. Dieser Kollektor sammelt die Elemente des Streams. Oft sammeln Sie Elemente in einer Sammlung. Die Methode collect gibt die vom Kollektor außerhalb der Stream-Pipeline gesammelten Ergebnisse zurück.

Standardmäßig sind verschiedene Sammler erhältlich. Rufen Sie die Factory-Methode in java.util.stream.Collectors auf, um diese Collector-Objekte abzurufen.

Im Folgenden werden einige der typischen Sammler vorgestellt, die standardmäßig erhältlich sind.

Verwenden Verwenden So erhalten Sie ein Collector-Objekt Bemerkungen
Sammle Stream-Elemente in der Liste - Collectors.toList()
Sammle Stream-Elemente in Set - Collectors.toSet()
Sammeln Sie Stream-Elemente in einer beliebigen Sammlung - Collectors.toCollection(collectionFactory)
Sammle Stream-Elemente in Map Einfach sammeln Collectors.toMap(keyMapper, valueMapper)
Sammeln Sie beim ordnungsgemäßen Zusammenführen, wenn Sie Schlüssel duplizieren Collectors.toMap(keyMapper, valueMapper, mergeFunction)
Geben Sie zusätzlich zu ↑ an, dass eine bestimmte Map-Implementierung verwendet werden soll Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapFactory)
Teilen Sie Stream-Elemente in Gruppen ein und speichern Sie die Elementliste für jede Gruppe als Map-Werte. Collectors.groupingBy(classifier)
Fast das gleiche wie ↑, jedoch als Kartenwerte aufgeführtAußerGeben Sie eine Sammlung von an Collectors.groupingBy(classifier, downstream) Ein Downstream (Downstream-Kollektor) ist ein Kollektor (Funktionsobjekt), der einen Teilstrom (eine Gruppe von Elementen, die zu einer Gruppe gehören) als Eingabe verwendet und eine Sammlung erstellt. Zum Beispiel Sammler.counting()Sie können den nachgeschalteten Kollektor verwenden, um die Anzahl der Fälle für jede Gruppe zu zählen.
Geben Sie zusätzlich zu ↑ an, dass eine bestimmte Map-Implementierung verwendet werden soll Collectors.groupingBy(classifier, mapFactory, downstream) Die Reihenfolge der Downstreams unterscheidet sich von ↑. Lass uns aufpassen.
Ruft das Maximalwertelement in einem Stream-Element ab - Collectors.maxBy(comparator) Nimmt einen Komparator, der die Vergleichsregel als Argument angibt
Holen Sie sich das minimale Element in einem Stream-Element - Collectors.minBy(comparator)
Verketten Sie einfach die Zeichenfolgen von Stream-Elementen - Collectors.joining()
Verketten Sie Stream-Element-Strings mit einem Trennzeichen - Collectors.joining(delimiter)

Natürlich gibt es andere als die oben genannten.

In der Tabelle habe ich Collectors.toList () usw. zur Erklärung geschrieben, aber wenn wir es tatsächlich verwenden, importieren wir statisch alle in Collectors definierten Mitglieder, damit "Collectors" weggelassen werden kann. .. Es wird viel einfacher zu lesen sein.

Punkt 47 Wählen Sie als Rückgabetyp Sammlung über Stream aus

Ich denke, es ist üblich, dass Ihre eigene API eine Folge von Elementen zurückgibt. In diesem Fall möchten Sie je nach Benutzer den zurückgegebenen Wert möglicherweise als Stream oder als Iterable behandeln.

Daher ist der Rückgabetyp, der beide verarbeiten kann, der beste. Insbesondere sind Collection oder seine Subtypen gut. Dies liegt daran, dass die Collection-Schnittstelle ein Subtyp von Iterable ist und über eine Stream-Methode verfügt.

  • Wenn der Rückgabetyp ** eine Sammlung oder deren Untertypen ** sein kann, beachten Sie Folgendes:

  • Wenn die Anzahl der Elemente klein genug ist, um im Speicher gespeichert zu werden, ist eine Standardimplementierung einer Sammlung wie ArrayList in Ordnung.

  • Andernfalls müssen Sie eine spezielle Sammlung implementieren, die einen kleinen Speicherbereich benötigt.

  • Wenn der Rückgabetyp ** nicht ** eine Sammlung oder deren Untertypen sein kann, beachten Sie Folgendes:

  • Es ist vorzuziehen, entweder Iterable oder Stream zu wählen, je nachdem, was natürlicher ist.

  • Manchmal bestimmt die einfache Implementierung, welche verwendet werden soll.

  • In beiden Fällen benötigen Sie einen Adapter, um von einem zum anderen zu konvertieren. Die Verwendung eines Adapters stört die Implementierung und ist langsam.

Punkt 48 Achten Sie beim Parallelisieren von Streams darauf

Das Aufrufen von "Stream.parallel ()" in einer Stream-Pipeline führt zu einer Multithread-Verarbeitung der Pipeline, was häufig zu schrecklichen Ergebnissen führt. Mit anderen Worten, was nicht schneller wird, ist katastrophal langsamer als das Ausführen in einem einzelnen Thread. Es ist ziemlich schwer zu verstehen, warum und es ist auch sehr schwer zu implementieren, um schnell zu sein.

Parallelisieren Sie Streams also nur, wenn Sie einen guten Grund oder eine Bestätigung haben.

Kapitel 8 Methode

Punkt 49 Überprüfen Sie die Gültigkeit der Parameter

Lassen Sie uns zu Beginn die Gültigkeit der von der Methode oder dem Konstruktor akzeptierten Parameter überprüfen. Wenn Sie dies nicht tun, kann dies zu unverständlichen Ausnahmen aufgrund falscher Parameter oder unerwarteter Anomalien außerhalb Ihres Codes führen.

Beachten Sie die folgenden Punkte:

  • Wenn die Parameter Einschränkungen unterliegen, schreiben Sie sie in das Javadoc @ throw.
  • Die in Java 7 hinzugefügte Objects.requireNonNull-Methode ist nützlich für die Nullprüfung. Lass es uns benutzen.
  • Ab Java 9 wurden den Objekten checkFromIndexSize-, checkFromToIndex- und checkIndex-Methoden zum Überprüfen von Listen- und Array-Indizes hinzugefügt. Es ist praktisch, wenn es Ihrem Zweck entspricht.
  • Wenn die Verarbeitungskosten der Gültigkeitsprüfung hoch sind und ** und ** die Gültigkeitsprüfung implizit in der Mitte des Prozesses durchgeführt wird, sollte die Gültigkeitsprüfung nicht explizit bereitgestellt werden.

Punkt 50 Defensive Kopie, falls erforderlich

Klassen, die als APIs verfügbar gemacht werden, sollten von Benutzern als schlecht behandelt angesehen werden, auch wenn sie nicht böswillig sind. Stellen Sie sich das als etwas vor, das die Invarianten dieser Klasse brechen würde.

Unabhängig davon, wie unangemessen der Benutzer es verwendet, sollte die Invariante der Klasse daher nicht gebrochen werden. Das ist eine defensive Kopie. Führen Sie insbesondere die folgenden Aktionen aus.

  • Wenn Sie ein variables Objekt von einem Benutzer erhalten und es als Status speichern möchten, erstellen Sie eine Kopie dieses Objekts und speichern Sie die Referenz. In diesem Fall ist es gefährlich, die Klonmethode des empfangenen Objekts zu verwenden. Sie können dieser Unterklasse nicht vertrauen.
  • Wenn Sie dem Benutzer ein variables Objekt oder Array zurückgeben möchten, das die Klasse als Status hat, erstellen Sie eine Kopie dieses Objekts und geben Sie es zurück.
  • Versuchen Sie zunächst, so viele unveränderliche Klassen wie möglich zu verwenden. Sie müssen sich darüber keine Sorgen machen. Verwenden Sie ab Java 8 unveränderliche Klassen wie java.time.Instant (eine andere Klasse im selben Paket) anstelle von java.util.Date.

Es gibt jedoch Zeiten, in denen Sie sich entscheiden, keine defensive Kopie zu erstellen. Dies ist in den folgenden Fällen der Fall. In einem solchen Fall müssen Maßnahmen ergriffen werden, beispielsweise in Javadoc.

  • Wenn die Verarbeitungskosten der defensiven Kopie nicht akzeptabel sind.
  • Wenn Sie dem Benutzer aus irgendeinem Grund vertrauen können.
  • Selbst wenn die Invariante gebrochen ist, ist nur der Benutzer in Schwierigkeiten.

Punkt 51 Entwerfen Sie die Signatur der Methode sorgfältig

Dieser Artikel ist eine Sammlung von Tipps. Wenn Sie diese Regeln befolgen, ist Ihre eigene API einfacher zu erlernen und zu verwenden. Und es macht es auch schwieriger, zu Fehlern zu führen.

  • Wählen Sie den Methodennamen sorgfältig aus.
  • Sei verständlich.
  • Muss mit anderen Namen im selben Paket übereinstimmen.
  • In Übereinstimmung mit dem weit verbreiteten Konsens.
  • Kurz sein.
  • Geben Sie nicht zu viele nützliche Methoden an.
  • Wenn es zu viele Methoden gibt, ist dies sowohl für den Betreuer als auch für den Benutzer schwierig.
  • Stellen Sie einen Prozess bereit, der ausgeführt werden kann, indem mehrere Methoden nur dann als eine bequemere Methode kombiniert werden, wenn klar ist, dass er häufig verwendet wird.
  • Halten Sie die Anzahl der Parameter auf 4 oder weniger.
  • Benutzer können sich nicht an viele Parameter erinnern.
  • Es ist NG, mehrere Parameter des gleichen Typs zu haben. Wenn Sie versehentlich einen Fehler machen, wird der Compiler Ihnen dies nicht mitteilen.
  • So reduzieren Sie die Parameter.
  • In mehrere Methoden unterteilen. List's subList, indexOf und lastIndexOf sind Beispiele.
  • Erstellen Sie eine Hilfsklasse, die eine Sammlung von Parametern enthält. Es wird eine statische Mitgliedsklasse sein.
  • Wenden Sie das Builder-Muster auf Methodenaufrufe an. Am Ende ausführen, aber es ist eine gute Idee, die Gültigkeit der Parameter zu diesem Zeitpunkt zu überprüfen.
  • Verwenden Sie für Parametertypen so oft wie möglich Schnittstellen.
  • Bietet Ihnen die Flexibilität, zu einer anderen Implementierung zu wechseln.
  • Verwenden Sie einen Aufzählungstyp mit zwei Elementen anstelle eines booleschen Parameters.
  • Leicht zu lesen und erweiterbar.

Punkt 52 Vorsichtig mit Überlastung verwenden

Wenn Sie Ihre eigene API erstellen, ist es NG, mehrere überladene Methoden und Konstruktoren mit derselben Anzahl von Parametern bereitzustellen. Es funktioniert möglicherweise nicht wie vom Benutzer beabsichtigt und kann den Benutzer verwirren.

Gehen wir deshalb wie folgt vor.

  • Wenn es sich um eine Methode handelt, ändern Sie den Methodennamen.
  • Wenn es sich um einen Konstruktor handelt, können Sie den Namen nicht ändern. Implementieren Sie ihn daher mit einer statischen Factory-Methode.
  • Wenn Sie überladen müssen und möchten, dass sie sich gleich verhalten, wechseln Sie von eingeschränkt zu allgemein, um sicherzustellen, dass beide das gleiche Verhalten haben.

Punkt 53: Verwenden Sie Argumente mit variabler Länge mit Vorsicht

Argumentmethoden mit variabler Länge sind nützlich. Beachten Sie jedoch die folgenden Punkte.

  • Wenn das Argument mit variabler Länge die erforderlichen Parameter enthält, tritt kein Kompilierungsfehler auf, wenn der Benutzer das Argument mit variabler Länge nicht versehentlich angibt. Nehmen Sie die erforderlichen Parameter nicht in die Argumente mit variabler Länge auf und definieren Sie sie getrennt von den Argumenten mit variabler Länge.
  • Argumente mit variabler Länge werden erreicht, indem bei jedem Aufruf einer Methode ein Array generiert wird. Erkennen Sie die Kosten für die Erstellung eines solchen Arrays und definieren Sie Ihre API. Wenn diese Kosten nicht akzeptabel sind, ermitteln Sie, wie viele häufig verwendete Argumente Sie haben, und definieren Sie die Methoden für diese Argumente einzeln.

Element 54 Gibt eine leere Sammlung oder ein leeres Array anstelle von null zurück

Einige Methoden, die eine Datenfolge zurückgeben, geben in einigen Fällen null zurück, dies ist jedoch NG.

Der Grund ist wie folgt.

  • Benutzer müssen Code schreiben, der Nullen behandelt. Erstens kann die Entsprechung zu Null durchgesickert sein.
  • Für den API-Implementierer wird der Code durch die Rückgabe von null unnötig überladen.

【NG】

public List<Cheese> getCheeses() {
    return cheesesInStock.isEmpty() ? null
        : new ArrayList<>(cheesesInStock);
}

【OK】

public List<Cheese> getCheeses() {
    //Es besteht keine Notwendigkeit, sich mit der bedingten Verzweigung zu beschäftigen.
    //Dies gibt eine leere Liste zurück.
    return new ArrayList<>(cheesesInStock);
}

Die Kosten für die Erstellung einer leeren Liste sind oft vernachlässigbar. Versuchen Sie in diesem Fall, eine unveränderliche leere Sammlung zurückzugeben, z. B. mit Collections.emptyList (). Dies ist jedoch selten der Fall, tun Sie es also nicht blind.

Punkt 55: Optional vorsichtig zurücksenden

Vor Java 8 wurden die folgenden Schritte ausgeführt, um Methoden zu schreiben, die keinen Wert zurückgaben.

  • Gibt null zurück.
  • Eine Ausnahme auslösen.

Offensichtlich gab es ein Problem mit diesen Methoden, und Java 8 fügte gute hinzu. Das ist optional.

Durch die Rückgabe von Optional von der API können Sie den Benutzer auf die Möglichkeit aufmerksam machen, dass der Rückgabewert leer ist, und den Benutzer zwingen, ihn zu verarbeiten, wenn er leer ist.

Die Methode zum Erstellen eines optionalen Objekts lautet wie folgt.

So generieren Sie optional Erläuterung
Optional.empty() Gibt eine leere Option zurück.
Optional.of(value) Gibt ein optionales Element zurück, das einen Wert ungleich Null enthält. Wenn null übergeben wird, wird eine NullPointerException ausgelöst.
Optional.ofNullable(value) Akzeptiert einen potenziell null Wert und gibt eine leere Option zurück, wenn null übergeben wird.

API-Benutzer behandeln Optional wie folgt.

//Wenn es leer ist, wird der von orElse angegebene Standardwert verwendet.
String lastWordInLexicon max(words).orElse("No words...");

//Wenn es leer ist, löst die von orElseThrow angegebene Ausnahmefactory die Ausnahme aus.
//Die Ausnahmefactory ist jetzt so angegeben, dass die Kosten für das Generieren einer Ausnahme nur dann anfallen, wenn die Ausnahme tatsächlich ausgelöst wird.
Toy myToy = max(toys).orElseThrow(TemperTantrumException::new);

//Wenn Sie wissen, dass die Option nicht leer ist, können Sie den Wert auf einmal abrufen.
//In dem unwahrscheinlichen Fall, dass die Option leer ist, wird eine NoSuchElementException ausgelöst.
Element lastNobleGas = max(Elements.NOBLE_GASES).get();

Verschiedene nützliche Methoden sind in Optional definiert. Beispielsweise nimmt die orElseGet-Methode ein "Supplier " und kann es aufrufen, wenn "Optional" leer ist. Diese House isPresent-Methode existiert, weil Sie mit anderen Methoden nicht das tun konnten, was Sie wollten, und Sie sollten sie nicht aggressiv verwenden.

Wenn Sie mit Streams arbeiten, deren Element Optional ist, möchten Sie häufig nach nicht leeren filtern. In diesem Fall ist es besser, Folgendes zu implementieren.

//Java 8 oder früher
// Optional.isPresent()Ist eine der wenigen Situationen, in denen
streamOfOptional
    .filter(Optional::isPresent)
    .map(Optional::get)

//Java 9 oder höher
//Stream optional hinzugefügt()In der Methode
//Gibt einen Stream zurück, der das Element enthält, wenn Optional einen Wert hat, und nichts, wenn es keinen Wert hat.
//Unter Ausnutzung dieser Eigenschaft kann sie wie folgt implementiert werden.
streamOfOptional
    .flatMap(Optional::stream)

Bitte beachten Sie bezüglich Optional Folgendes.

  • Natürlich sollten Sie die in Optional verpackte Sammlung usw. nicht zurückgeben. Wenn es sich um eine Sammlung handelt, geben Sie einfach eine leere Sammlung zurück.
  • Nicht zurückgeben Optional für Boxed Basic-Datentypen. Dies liegt daran, dass Verarbeitungskosten verschwendet werden, die oft nicht vernachlässigbar sind.
  • Optional sollte nur für die Rückgabe des Rückgabewerts der Methode verwendet werden. Zum Beispiel ist es oft unangemessen, Optional in einem Instanzfeld zu halten. Es muss andere Mittel geben.

Punkt 56 Schreiben Sie Dokumentkommentare für alle öffentlichen API-Elemente

Warum benötigen Sie Dokumentkommentare (Javadoc)? [Warum]

Sie sollten ein Javadoc für öffentliche APIs schreiben, um zu verhindern, dass Benutzer sie missbrauchen.

Darüber hinaus ist es nicht öffentlich zugänglich, sodass Wartungsmitglieder die API warten können, damit sie sie nicht in die falsche Richtung ändern, und dass Wartungsmitglieder die Absicht und den Zweck der ursprünglichen Implementierung verstehen können. Sie sollten auch ein Javadoc für schreiben.

In der Praxis sollten Sie den Mindestinhalt schreiben, der diesen Zwecken dienen kann. Code ist kein Kunstwerk, sondern ein Mittel, um Geld zu verdienen. Es ist auch wichtig, dass die Kosten von Javadoc den Gewinn wert sind.

Was soll ich machen? [Wie]

Es ist wie folgt.

  • Geben Sie die API und den "Vertrag" an, den der Benutzer behalten soll.
  • Voraussetzung: Es muss festgelegt werden, damit der Benutzer die API aufrufen kann.
  • Nachbedingung: Eine Angelegenheit, die erfüllt sein muss, nachdem der Anruf erfolgreich abgeschlossen wurde.
  • Nebenwirkungen: Beobachtbare Änderungen des Systemstatus. Starten Sie einen Thread im Hintergrund usw.
  • Parameter: Grundsätzlich in Nomenklatur schreiben.
  • Rückgabewert: Grundsätzlich eine Nomenklatur. Es kann weggelassen werden, wenn es mit der Gliederungsbeschreibung der API übereinstimmt.
  • Ausnahmen ausgelöst: Schreiben Sie, ob aktiviert oder deaktiviert. Schreiben Sie den Namen der Ausnahmeklasse und die Bedingungen, die ausgelöst werden sollen.
  • Drücken Sie den Code mit "{@code}" aus.
  • Beispielsweise steht "this" in "this list" für das Objekt, für das die Methode aufgerufen wurde.
  • Schreiben Sie für Klassen, die geerbt und verwendet werden, in "@ implSpec", dass Sie Ihre anderen Methoden aufrufen. Andernfalls wird der Benutzer es falsch verwenden. Beachten Sie, dass dieses Tag zumindest in Java 9 nicht standardmäßig aktiviert ist.
  • Wenn Sie Zeichen wie "<> &" darstellen möchten, verwenden Sie "{@literal}".
  • Schreiben Sie eine kurze Beschreibung am Anfang des Dokumentkommentars.
  • Verschiedene APIs dürfen nicht dieselbe Übersicht haben.
  • Bitte beachten Sie, dass die Gliederungserklärung mit einem Punkt (.) Endet. Sie sollten {@literal} usw. entsprechend verwenden.
  • In der Übersichtsbeschreibung wird das Thema der Kürze halber häufig weggelassen. Für Englisch sollten Sie das Formular für die Anwesenheit der dritten Person verwenden.
  • Die Nomenklatur sollte für Klassen, Schnittstellen und Felder verwendet werden.
  • In Java 9 können Sie im Suchfeld oben rechts im Javadoc-Bildschirm nach Schlüsselwörtern suchen. Zu diesem Zeitpunkt sind nicht nur der Klassenname und der Methodenname, sondern auch die Schlüsselwörter, die explizit von "{@index}" festgelegt wurden, für die Suche verantwortlich.
  • Dokumentieren Sie alle Typparameter für generische Typen und Methoden.
  • Fügen Sie für den Aufzählungstyp für jede Konstante einen Dokumentkommentar hinzu.
  • Dokumentieren Sie alle Mitglieder für Anmerkungen.
  • Dokumentationskommentare auf Paketebene sollten sich in package-info.java befinden.
  • Beschreiben wir, ob es in Ordnung ist, mit Multithreading zu arbeiten.
  • Beschreiben Sie, ob und in welchem Format es serialisiert werden kann.
  • {@ InheritDoc} ist schwer zu benutzen und soll Einschränkungen haben.
  • Möglicherweise benötigen Sie eine Dokumentation, die ihre Beziehungen beschreibt, nicht nur einzelne Klassen.
  • Die oben genannten Regeln sollten automatisch überprüft werden. In Java 8 und höher ist dies standardmäßig aktiviert. Prüfstil usw. wird genauer geprüft.

Kapitel 9 Allgemeine Programmierung

Was in diesem Kapitel geschrieben steht, ist "natürlich". Ich werde nur die Dinge erklären, die beachtet werden sollten.

Punkt 57 Minimieren Sie den Umfang lokaler Variablen

Es gibt nichts Besonderes zu erwähnen.

Punkt 58 Wählen Sie für jede Schleife eine herkömmliche for-Schleife aus

Es ist in Ordnung, aber denken wir nur an die folgenden Punkte.

  • Mit Collection.removeIf (Filter) können Sie bestimmte Elemente während des Scannens entfernen, ohne auf herkömmliche for-Schleifen zurückgreifen zu müssen.

Punkt 59 Kennen Sie die Bibliothek und verwenden Sie die Bibliothek

Wissen, welche Funktionen die wichtigsten Unterpakete haben, einschließlich java.lang, java.util, java.io, java.util.concurrent. Wenn Sie es nicht wissen, können Sie es nicht verwenden. Schauen Sie sich unbedingt die neuen Funktionen an, die in der Version hinzugefügt werden.

Ab Java 7 sollten Sie beispielsweise ThreadLocalRandom anstelle von Random verwenden. Das geht schneller.

Punkt 60 Vermeiden Sie Floats und Doubles, wenn Sie eine genaue Antwort benötigen

Floats und Doubles existieren für schnelle Berechnungen, die sich ungefähr den genauen Ergebnissen annähern. Es existiert nicht, um genaue Berechnungsergebnisse zu erhalten. Sie sollten diese also nicht verwenden, wenn Sie genaue Ergebnisse wünschen.

Verwenden Sie stattdessen Big Decimal.

Big Decimal hat jedoch den Nachteil, "langsam" zu sein. Wenn die Langsamkeit von BigDecimal nicht akzeptabel ist, verwenden Sie Ganzzahlen wie int und long (konvertieren Sie beispielsweise Dollar in Cent). Sie können int für bis zu 9 Stellen und long für bis zu 18 Stellen verwenden. Wenn Sie darüber hinausgehen, haben Sie keine andere Wahl, als Big Decimal zu verwenden.

Punkt 61 Wählen Sie einen Basisdatentyp anstelle von Boxed-Basisdaten aus

Es gibt nichts Besonderes zu erwähnen.

Punkt 62 Vermeiden Sie Zeichenketten, wenn andere Typen geeignet sind

Wenn es sich beispielsweise um eine Zeichenfolge handelt, die im Wesentlichen einen numerischen Wert darstellt, behandeln Sie sie mit int usw.

Es ist in Ordnung, aber denken wir nur an die folgenden Punkte.

  • String ist unveränderlich, daher wird dieselbe Instanz in der gesamten JVM verwendet, solange sie denselben String darstellt. Daher kann es nicht als Schlüssel verwendet werden, wenn die Threads denselben Namespace verwenden.

Punkt 63 Achten Sie auf die Leistung beim Verbinden von Zeichenfolgen

Da String unveränderlich ist, wird jedes Mal, wenn er mit + verkettet wird, eine neue Instanz erstellt.

Verwenden Sie StringBuilder, um + viele Male anzuwenden, um Zeichenfolgen zu kombinieren (beachten Sie, dass es nicht threadsicher ist).

Das bedeutet nicht, dass alle + Verbindungen schlecht sind. Wenn es ein oder zwei + ist, ist es prägnant und sollte keine Leistungsprobleme haben.

Punkt 64 Beziehen Sie sich auf ein Objekt in der Schnittstelle

Es ist flexibler, in einer Schnittstelle oder abstrakten Klasse darauf zu verweisen. Dies liegt daran, dass die Implementierungsklasse später ersetzt werden kann.

Punkt 65 Wählen Sie eine Schnittstelle über Reflexion

Die Aspekte, in denen Reflexion verwendet werden sollte, sind äußerst begrenzt. Benutze es nicht blind.

Punkt 66: Verwenden Sie native Methoden mit Vorsicht

JNI ist eine Funktion, mit der Sie in C oder C ++ implementierte Methoden aufrufen können, die Sie jedoch wahrscheinlich nicht selbst implementieren müssen. Der Inhalt ist, dass Sie vorsichtig sein sollten, wenn Sie es zufällig tun. Ich denke, Sie sollten diesen Artikel lesen, nachdem Sie wirklich mit einer solchen Situation konfrontiert sind.

In seltenen Fällen müssen Sie ein Produkt mit PJ verwenden, und JNI ist die einzige Möglichkeit, es zu verwenden. Da der von JNI aufgerufene Prozess außerhalb der Kontrolle von JVM ausgeführt wird, besteht das Risiko einer Speicherbeschädigung. Seien Sie sich dieser Gefahren bewusst und testen Sie sie gründlich.

Punkt 67 Sorgfältig optimieren

Beim Optimieren wird im Extremfall Code geschrieben, der sich auf die unteren Ebenen konzentriert, um "schneller" zu sein. Die Optimierung erfolgt nur bei Bedarf, nicht von Anfang an. Dies liegt daran, dass die Optimierung Ihren Code überladen und in erster Linie unwirksam sein kann.

Folgendes ist zu tun:

  • Auf Architekturebene sollten keine Leistungsprobleme auftreten. Gestalten Sie Ihre Architektur sorgfältig. Besondere Aufmerksamkeit sollte der öffentlichen API, der externen Kommunikation und der Datenpersistenz gewidmet werden. Diese haben einen erheblichen Einfluss auf die Leistung und können später kaum ersetzt werden.
  • Entwerfen Sie Ihre öffentliche API sorgfältig.
  • Wenn Sie den exponierten Typ variabel machen, benötigen Sie eine defensive Kopie.
  • Eine unsachgemäße Übernahme der Vererbung kann Unterklassen behindern und deren Optimierung verhindern.
  • Wenn Sie es nicht für die Schnittstelle verwenden, können Sie es später nicht durch eine effiziente Implementierung ersetzen.
  • Wenn Sie Ihr API-Design für eine bessere Leistung verdrehen, werden Sie mehr Probleme haben, diese API weiterhin zu unterstützen.
  • Wenn Sie der Meinung sind, dass Sie eine Optimierung benötigen, verwenden Sie Profiling-Tools, um Engpässe zu identifizieren. Zum Beispiel ist jmh ein gut sichtbares Mikro-Benchmark-Framework.
  • Wenn Sie wirklich optimieren möchten, messen Sie, ob es sich wirklich verbessert. Wie das Programm tatsächlich verarbeitet wird, hängt von der Hardware ab, auf der der Bytecode ausgeführt wird. Bei so viel Hardware kann man heute nicht sagen, ob die Optimierung funktioniert hat, ohne sie zu messen.

Punkt 68: Beachten Sie die allgemein anerkannte Namenskonvention

Es gibt nichts Besonderes zu erwähnen. Das ist selbstverständlich.

Kapitel 10 Ausnahmen

Punkt 69 Verwenden Sie Ausnahmen nur für außergewöhnliche Bedingungen

Wie der Titel schon sagt. Es besteht keine Notwendigkeit, den Grund zu erklären.

Punkt 70 Verwenden Sie geprüfte Ausnahmen für wiederherstellbare Zustände und Laufzeitausnahmen für Programmierfehler

Die Klasse, die eine abnormale Situation darstellt, hat die folgende Struktur (Vererbungsbeziehung). Ich habe auch die richtige Verwendung und Vorsichtsmaßnahmen für jeden beschrieben.

Erläuterung
Throwable
Fehler und seine Untertypen Es wird von der JVM verwendet. Mach deine eigenen nicht.
Exception
RuntimeException und ihre Untertypen Dies ist die sogenannte "ungeprüfte Ausnahme". Dies sollte ausgelöst werden, wenn der Benutzer einen Fehler macht. Dies liegt daran, dass es nur schädlich ist, selbst wenn es vom Benutzer gehandhabt wird.
Kein Subtyp von RuntimeException Dies ist die sogenannte "geprüfte Ausnahme". Dies sollte ausgelöst werden, wenn der Anrufer ordnungsgemäß wiederherstellen kann. Dies liegt daran, dass der Benutzer gezwungen werden kann, eine Wiederherstellungsverarbeitung durchzuführen. Bereiten wir eine Methode für die Informationserfassung in der Ausnahmeklasse vor, damit der Benutzer damit richtig umgehen kann. Wie in Punkt 71 gezeigt, sollten Sie jedoch zunächst die Option Optional zurückgeben.

Punkt 71 Vermeiden Sie unnötige Verwendung von aktivierten Ausnahmen

Wenn Sie eine Ausnahme auslösen, die von der API überprüft wird, sollten Sie prüfen, ob dies wirklich erforderlich ist. Selbst wenn der Benutzer die Ausnahme erhält, sollte sie nicht ausgelöst werden, wenn er praktisch nichts tun kann.

APIs, die geprüfte Ausnahmen auslösen, können für Benutzer auch ärgerlich sein, wenn es notwendig und sinnvoll ist, den Benutzer zu einer Wiederherstellung zu zwingen. Dies liegt daran, dass es die folgenden Nachteile hat.

  • Sie müssen einen Try-Catch schreiben. Es ist ein Ärger und der Code ist unübersichtlich.
  • Sie können diese API nicht in einem Stream verwenden. Dies liegt daran, dass der Stream die aktivierten Ausnahmen nicht verarbeiten kann.

Es gibt eine Möglichkeit, Optional zurückzugeben, um diese Schwierigkeiten zu lösen oder zu lindern. Anstatt eine Ausnahme auszulösen, wird eine leere Option zurückgegeben. Optional kann jedoch keine zusätzlichen Informationen wie Ausnahmeklassen enthalten. Lassen Sie uns anhand der Waage beurteilen.

Punkt 72 Verwenden Sie Standardausnahmen

Wie der Titel schon sagt. Es besteht keine Notwendigkeit, den Grund zu erklären.

Punkt 73 Wirf eine Ausnahme aus, die für das abstrakte Konzept geeignet ist.

Sie können Ausnahmen in Java weitergeben. Wenn sich die in der unteren Schicht ausgelöste Ausnahme über mehrere Schichten auf die obere Schicht ausbreitet, werden der Code in der unteren Schicht und der Code in der oberen Schicht kombiniert. Mit anderen Worten, es wird schwierig, beide unabhängig voneinander zu modifizieren.

Beachten Sie aus diesem Grund Folgendes.

  • Machen Sie überhaupt keine Ausnahme. Mit anderen Worten, wenn Sie die untere Ebene aufrufen, überprüfen Sie, ob die Voraussetzungen für das Aufrufen der unteren Ebene erfüllt sind. Versuchen Sie, die Ausbreitung so weit wie möglich zu vermeiden.
  • Wenn Sie sich wirklich verbreiten müssen, lösen Sie eine neue Ausnahme auf der entsprechenden abstrakten Ebene auf der entsprechenden Ebene der abstrakten Ebene aus, um die obere und die untere Ebene zu trennen. Zu diesem Zeitpunkt sollte die ursprüngliche Ausnahme als Ursache festgelegt werden. Dies liegt daran, dass es einfacher ist, zu untersuchen, wann eine Ausnahme auftritt.

Punkt 74 Dokumentieren Sie alle Ausnahmen, die von jeder Methode ausgelöst werden

Es überschneidet sich erheblich mit dem, was in Punkt 56 erläutert wird. Es wird in Ordnung sein, wenn Sie es gut verstehen.

Punkt 75 Fügen Sie Informationen zur Fehleraufzeichnung in die detaillierte Nachricht ein

Ausnahmedetailmeldungen spielen eine sehr wichtige Rolle bei der Untersuchung, wann eine Ausnahme auftritt.

  • Schließen Sie alle Parameter- und Feldwerte ein, die die Ausnahme verursacht haben.
  • Protokolle werden auch von allgemeinen Programmierern angezeigt, enthalten also natürlich keine sicheren Informationen.
  • Es ist sinnlos, in die Detailmeldung aufzunehmen, dass sie von Javadoc und dem Quellcode aus leicht lesbar ist.
  • Wenn Sie Ihre eigene Ausnahme machen, sollten Sie in der Lage sein, solche Informationen im Konstruktorargument der Ausnahmeklasse zu erhalten, damit der Benutzer immer die für die Untersuchung erforderlichen Informationen festlegt.

Punkt 76: Arbeit für Fehleratomizität

Angenommen, eine Methode eines Objekts mit einem Status wird aufgerufen und innerhalb der Methode läuft ein Fehler. Danach ist es wünschenswert, dass das Objekt in den Zustand zurückkehrt oder in diesem bleibt, in dem es sich vor dem Methodenaufruf befand. Diese Eigenschaft wird als "Fehleratomizität" bezeichnet.

Diese Eigenschaft ist wichtig, wenn geprüfte Ausnahmen ausgelöst werden. Eine Prüfausnahme wird ausgelöst, da der Benutzer eine Wiederherstellung durchführen kann. Wenn der Benutzer nicht in seinen ursprünglichen Zustand zurückkehrt, kann er den Wiederherstellungsprozess nicht ausführen.

Verwenden Sie die folgenden Methoden, um eine Fehleratomizität zu erreichen.

  • Lassen Sie uns die Klasse zunächst unveränderlich machen. Wenn es sich nicht ändert, müssen Sie an nichts denken.
  • Stellen Sie sicher, dass die folgende Verarbeitung ausgeführt wird, bevor Sie den Status des Objekts ändern.
  • Parametervalidierung
  • Verarbeitung, die fehlschlagen kann
  • Führen Sie eine Operation für die temporäre Kopie des Objekts aus und ersetzen Sie den Inhalt des Objekts durch den Inhalt der temporären Kopie, wenn die Operation abgeschlossen ist.

Fehleratomizität ist wünschenswert, aber nicht immer erreichbar, und in einigen Fällen kann es zu kostspielig sein, sie zu erreichen. Wenn keine Fehleratomizität erreicht werden kann, geben Sie im Javadoc an, in welchem Zustand sich das Objekt befindet, wenn die Methode fehlschlägt.

Punkt 77 Ausnahmen nicht ignorieren

Wie der Titel schon sagt. Es besteht keine Notwendigkeit, den Grund zu erklären.

In seltenen Fällen kann es angebracht sein, die Ausnahme zu ignorieren. In diesem Fall müssen Sie den Grund in den Kommentaren hinterlassen.

Kapitel 11

Punkt 78 Synchronisieren Sie den Zugriff auf gemeinsam genutzte variable Daten

"Gegenseitiger Ausschluss" und "Kommunikation zwischen Threads" werden synchronisiert

Beim Lesen und Schreiben der Daten eines Objekts in mehreren Threads müssen die folgenden zwei Punkte beachtet werden.

  • Gegenseitiger Ausschluss: Sie müssen verhindern, dass andere Threads das Objekt in einem inkonsistenten Zustand sehen, während ein Thread das Objekt ändert.
  • ** Kommunikation zwischen Threads: Änderungen, die von einem Thread vorgenommen werden, müssen für den anderen zuverlässig sichtbar sein. ** ** **

Letzteres wird oft vergessen, seien Sie also vorsichtig. Bei nicht ordnungsgemäßer Implementierung kann der Compiler die von einem Thread vorgenommenen Änderungen beliebig optimieren, sodass sie für andere für immer unsichtbar sind. Das Ergebnis sind störende Störungen, die je nach Zeitpunkt auftreten können oder nicht.

Letzteres konzentriert sich auf den flüchtigen Modifikator. Wenn Sie dies in ein Feld einfügen, wird sichergestellt, dass der zuletzt geschriebene Wert für den Lesethread sichtbar ist, da der Cache pro Thread nicht mehr verwendet wird. Volatile schließt sich jedoch nicht gegenseitig aus. In der Situation, in der Objekte von mehreren Threads gemeinsam genutzt werden, ist es fast immer erforderlich, beide oben genannten Punkte zu erfüllen. Daher gibt es nur wenige Situationen, in denen die Flüchtigkeit ausreicht.

Um beide der beiden oben genannten Punkte zu erfüllen, muss eine Synchronisation durchgeführt werden. Fügen wir der Methode den synchronisierten Modifikator hinzu (der synchronisierte Block allein garantiert nicht die Sichtbarkeit des Werts in anderen Threads). In einigen Fällen kann das Paket java.util.concurrent.atomic geeignet sein (z. B. AtomicLong).

Bonus: Was ist "Atomizität"?

Der Begriff "atomar" wird häufig im Zusammenhang mit Multithreading verwendet, daher ist es eine gute Idee, ihn zu verstehen. Atomicity ist die Eigenschaft, dass mehrere Operationen an Daten anderen Threads als eine einzige Operation angezeigt werden.

Insbesondere denke ich, dass Sie die folgenden Punkte verstehen sollten.

  • Normalerweise ist nicht garantiert, dass lange und doppelte Variablen atomar sind. Als Ergebnis von zwei Schreibvorgängen mit jeweils 32 Bits, die separat ausgeführt werden, kann ein halbfertiger Zustand von einem anderen Thread aus gesehen werden. Selbst wenn es lang und doppelt ist, wird es durch Hinzufügen von flüchtig zur Variablen für andere Threads als ein 64-Bit-Schreibzugriff sichtbar.
  • Der Inkrementoperator "i ++" ist nicht atomar. Es gibt zwei Operationen, Lese- und Schreibvariablen, aber für andere Threads scheinen sie separate Operationen zu sein. Infolgedessen können Lese- und Schreibvorgänge von anderen Threads zwischen Lese- und Schreibvorgängen in einem Thread liegen. Die Bibliothek, die dieses Problem behebt, ist das Paket java.util.concurrent.atomic.

Punkt 79 Vermeiden Sie übermäßige Synchronisation

Übermäßige Synchronisation kann zu schlechten Ergebnissen führen.

Was bedeutet es, die Synchronisation zu stark zu nutzen?

Übermäßige Verwendung der Synchronisation bedeutet Folgendes.

  • Verwenden Sie die Synchronisation dort, wo sie nicht sein sollte.
  • Sollte synchronisiert werden, aber der Umfang der Synchronisation ist zu groß.
  • Der Umfang ist einfach zu groß.
  • Geben Sie dem Benutzer die Kontrolle im Rahmen der Synchronisation (z. B. rufen Sie eine vom Benutzer überschriebene Methode oder ein vom Benutzer im Rahmen der Synchronisation übergebenes Funktionsobjekt auf).

Was passiert, wenn Sie eine übermäßige Synchronisation verwenden?

Übermäßige Synchronisation kann die folgenden Probleme verursachen:

  • Genauigkeitsprobleme (die Codebeispiele im Buch sind sehr hilfreich)

  • Ein Thread akzeptiert doppelte Sperren, und dieser Thread selbst unterbricht die Klasseninvarianz. Java-Sperren sind wiedereintrittsfähig. Mit anderen Worten, wenn Sie eine Sperre erwerben und dann versuchen, dieselbe Sperre erneut zu erwerben, wird kein Fehler angezeigt.

  • Dead Lock wird auftreten. Das heißt, mehrere Threads warten aufeinander, um ihre Sperren aufzuheben.

  • Performance-Probleme

  • Andere Threads als die, die das Schloss erworben haben, warten länger als nötig.

  • Es gibt eine Verzögerung, um allen CPU-Kernen eine konsistente Ansicht des Speichers zu geben. In der Multi-Core-Ära sind diese Kosten sehr hoch.

  • Die JVM kann die Codeausführung nicht vollständig optimieren.

Was soll ich machen?

Es ist wie folgt.

  • Versuchen Sie nicht so viel wie möglich zu synchronisieren.
  • Beim Erstellen einer Variablenklasse haben Sie folgende Möglichkeiten. Lassen Sie uns Ersteres so weit wie möglich übernehmen.
  • Benutzer synchronisieren. Lassen Sie uns dies so weit wie möglich übernehmen.
  • Innerhalb der Klasse synchronisieren. Wenn dies übernommen wird, ist es nicht möglich, dem Benutzer die Option zum Synchronisieren / Nicht-Synchronisieren bereitzustellen. Erwägen Sie, den Benutzer zuerst zur Synchronisierung zu bewegen, und verwenden Sie nur dann die interne Synchronisierung innerhalb der Klasse, wenn dies ein Problem darstellt.
  • Minimieren Sie den Bereich, wenn Sie synchronisieren möchten. Rufen Sie insbesondere keine Prozesse auf, die dem Benutzer im Rahmen der Synchronisierung die Kontrolle überlassen.

Punkt 80 Executor, Task, Stream vom Thread auswählen

Anstatt Ihre eigene Thread-Klasse zu verwenden, befindet sie sich im Paket java.util.concurrent Verwenden Sie das Executor-Framework. Das ist die Botschaft dieses Artikels.

Der Inhalt dieses Artikels ist zur Hälfte. Sie sollten die in diesem Abschnitt vorgestellte "Java-Parallelität in der Praxis" (Java-Parallelitätsprogrammierung - Finden der "Grundlage" und der neuesten API ") lesen.

Punkt 81 Wählen Sie während der Wartezeit das Dienstprogramm für die parallele Verarbeitung aus und benachrichtigen Sie

Was zuvor mit Warten und Benachrichtigen erreicht wurde, kann jetzt problemlos mit dem Dienstprogramm für gleichzeitige Parallelität im Paket java.util.concurrent erreicht werden.

Hochrangiges Dienstprogramm für Parallelität

Die allgemeinen Parallelitätsdienstprogramme im Paket java.util.concurrent werden in die folgenden drei Kategorien eingeteilt.

  • Executor Framework

  • Siehe Punkt 81.

  • Gleichzeitige Abholung

  • Implementiert Standardschnittstellen wie List, Queue und Map und führt intern die entsprechende Synchronisationsverarbeitung durch. Es erzielt eine hohe Leistung beim Synchronisieren.

  • Da es nicht möglich ist, von außen in den Synchronisationsprozess einzugreifen, ist es nicht möglich, mehrere grundlegende Operationen für gleichzeitige Sammlungen zu kombinieren und atomar zu machen. Um dies zu erreichen, werden APIs für atomare Operationen bereitgestellt, indem mehrere grundlegende Operationen kombiniert werden. (PutIfAbsent in Map usw.).

  • Die Verarbeitung, die mehrere grundlegende Operationen kombiniert, ist als Standardimplementierung in Schnittstellen wie Map integriert, aber nur die Implementierung gleichzeitiger Sammlungen wird atomar. Die Standardimplementierung ist in Schnittstellen wie Map integriert, da sie auch dann praktisch ist, wenn sie nicht atomar ist.

  • Synchronisierte Sammlungen (wie Collections.sysnchronizedMap) gehören der Vergangenheit an und sind langsam. Verwenden Sie gleichzeitige Sammlungen, es sei denn, Sie haben einen bestimmten Grund.

  • Implementierungen wie Queue wurden erweitert, damit "Blockierungsvorgänge" auf den Abschluss des Vorgangs warten können. Beispielsweise wartet die Methode take von BlockingQueue, wenn die Warteschlange leer ist, und verarbeitet sie, wenn sie in der Warteschlange registriert ist. Das Executor-Framework nutzt diesen Mechanismus.

  • Synchronizer

  • Verhält sich wie ein Schwarzes Brett zwischen den Threads, sodass Sie zwischen den Threads Schritt halten können.

  • Ein beliebtes ist CountDownLatch. Beispielsweise erstellt der übergeordnete Thread ein Objekt für "new CountDownLatch (3)", startet die untergeordneten Threads A, B und C, ruft das CountDownLatch-Objekt "await ()" auf und wartet. Wenn die Threads A, B und C für dieses CountDownLatch-Objekt "countDown ()" aufrufen, wird der übergeordnete Thread nicht gewartet und die nachfolgende Verarbeitung des übergeordneten Threads ausgeführt. Warten und Benachrichtigen werden hinter der Reihe der Verarbeitung ausgeführt, aber CountDownLatch ist für alle komplizierten Teile verantwortlich.

  • Obwohl dies nicht auf den Kontext der Parallelverarbeitung beschränkt ist, verwenden Sie System.nanoTime () anstelle von System.currentTimeMillis (), um das Zeitintervall zu messen. Letzteres ist genauer und genauer und wird durch die Echtzeituhreinstellung des Systems nicht beeinflusst.

warten und benachrichtigen

Sie können den Code auch durch Warten und Benachrichtigen für Wartungsarbeiten usw. pflegen. In diesem Fall sollten Sie den Standard für Warten und Benachrichtigen kennen.

Dieser Teil ist fast derselbe wie der folgende, daher wird eine ausführliche Erklärung weggelassen.

Punkt 82 Sicherheit des Dokumentenfadens

Alles, was geschrieben steht, ist wichtig. Der Inhalt dieses Artikels ist leicht zu verstehen, sodass Sie ihn nicht besonders erläutern müssen.

Punkt 83: Verwenden Sie die Verzögerungsinitialisierung mit Vorsicht

In seltenen Fällen kann es die Initialisierung eines Feldes verzögern, bis der Wert des Feldes benötigt wird. Dies wird als verzögerte Initialisierung bezeichnet.

Der Zweck der verzögerten Initialisierung ist wie folgt:

  • Zur Optimierung (zum "Befestigen")
  • Es gibt eine Art zyklische Verarbeitung bei der Initialisierung und um den Kreislauf zu unterbrechen

Wenn Sie eine Optimierung anstreben, überlegen Sie zweimal: "Ist das wirklich sinnvoll?" Der Effekt kann sehr gering oder kontraproduktiv sein. Es kommt nicht in Frage, den Code dafür zu überladen.

Wenn bei der normalen Initialisierung kein Problem auftritt, fügen Sie final wie unten gezeigt hinzu, um die Initialisierung durchzuführen.

private final FieldType field = computeFieldValue();

Gleiches gilt für statische Felder.

Verzögerte Initialisierungsmethode

Nehmen wir als Beispiel den Fall, in dem der Feldtyp eine Objektreferenz ist.

Die Basisdaten sind fast gleich. Bei diesen Daten besteht der einzige Unterschied darin, dass sie nicht null sind und mit dem Standardwert 0 verglichen werden.

① Wenn Sie den Initialisierungszyklus unterbrechen

Verwenden wir "synchronized accessor" wie folgt. Es ist eine einfache und unkomplizierte Methode.

private FieldType field;

//Durch die synchronisierte Methode
//Sowohl "gegenseitiger Ausschluss" als auch "Kommunikation zwischen Threads" werden realisiert.
private synchronized FieldType getField() {
    if (field == null) 
        field = computeFieldValue();
    return field;
}

Gleiches gilt für statische Felder.

(2) Bei Verzögerung beim Initialisieren eines statischen Feldes zur Optimierung

Wenn Sie die Initialisierung nicht verzögern möchten, gehen Sie wie folgt vor ...

private static final FieldType field = computeFieleValue();

Wenn Sie ein statisches Feld zur Optimierung verzögert initialisieren möchten, verwenden Sie die "Idiom der Inhaberklasse für verzögerte Initialisierung" (siehe unten).

private static class FieldHolder {
    //Geben Sie der statischen Elementklasse die Felder, deren Initialisierung verzögert werden soll.
    static final FieldType field = computeFieleValue();
}

private static FieldType getField() {
    //· Laden Sie die statische Elementklasse nur, wenn der Feldwert benötigt wird.
    //Als Ergebnis des Ladens der Klasse wird der statische Feldinitialisierungsprozess ausgeführt.
    //· Eine typische JVM synchronisiert den Feldzugriff beim Initialisieren einer Klasse
    //Es ist nicht erforderlich, einen expliziten Synchronisierungsprozess zu schreiben.
    return FieldHolder.field;
}

(3) Wenn die Initialisierung des Instanzfelds zur Optimierung verschoben wird

Verwenden Sie die "Double Check Idiom" wie unten gezeigt.

//Das initialisierte Feld kann von anderen Threads nicht nur mit dem synkronisierten Block in der getField-Methode gesehen werden.
//Durch Hinzufügen von flüchtig wird das initialisierte Feld sofort von anderen Threads gesehen.
private volatile FieldType field;

private FieldType getField() {
    //Um die Leistung zu verbessern, laden Sie das Feld nur einmal
    //Der Wert des Feldes wird dem Ergebnis der lokalen Variablen zugewiesen.
    FieldType result = field;

    //Nach der Initialisierung ist keine Sperre mehr erforderlich.
    //Bei der ersten Inspektion wird es nicht verriegelt.
    if (result != null)
        return result;

    //Bei der zweiten Inspektion wird es zum ersten Mal verriegelt.
    synchronized(this) {
        if (field == null)

            //Wenn nicht gesperrt, zu diesem Zeitpunkt (zwischen der if-Entscheidung und dem Aufruf der computeFieldValue-Methode)
            //Es besteht die Gefahr, dass ein anderer Thread das Feld initialisiert.

            field = computeFieldValue();
        return field;
    }
}

Punkt 84: Hängt nicht vom Thread-Scheduler ab

Warum ist es NG, sich auf den Thread-Scheduler zu verlassen?

Der Thread-Scheduler ist eine der Komponenten der JVM und plant, wie der Name schon sagt, Threads. Eine der Komponenten der JVM ist, dass sie schließlich die Funktionen des Betriebssystems verwendet und ihr Verhalten stark vom Betriebssystem abhängt.

Aus diesem Grund wissen wir weder genau, wie sich der Thread-Scheduler verhält, noch haben wir die Kontrolle darüber. Wenn die Richtigkeit und Leistung des Programms vom Thread-Scheduler abhängt, verhält es sich unregelmäßig, manchmal funktioniert es, manchmal nicht, manchmal ist es schnell, manchmal ist es langsam. Machen wir das. Wenn Sie auf eine JVM mit einem anderen Betriebssystem portieren, funktioniert dies möglicherweise nicht wie vor dem Port.

Aus diesem Grund sollte die Richtigkeit und Leistung Ihres Programms nicht vom Thread-Scheduler abhängen.

Was soll ich machen?

Beachten Sie die folgenden Punkte:

  • Halten Sie die durchschnittliche Anzahl von RUNNABLE-Threads niedrig. Machen Sie es nicht viel größer als die Anzahl der Prozessoren. Eine große Anzahl von RUNNABLE-Threads gibt dem Scheduler Auswahlmöglichkeiten und hängt daher vom Scheduler ab. Die möglichen Status der Threads finden Sie unter Offizielles Dokument hier. .. Beachten Sie Folgendes, um die Anzahl der RUNNABLE-Threads gering zu halten:
  • Wenn die Verarbeitung des Threads abgeschlossen ist, versetzen wir den Thread in den Wartezustand (WAITING).
  • Im Executor Framework
  • Stellen Sie den Thread-Pool auf die richtige Größe ein.
  • Machen Sie die Größe der Aufgabe moderat. Wenn die Granularität der Aufgabe zu gering ist, sind die Kosten für das Versenden von Threads an die Aufgabe hoch und langsam.
  • Während (wahr) wird das Wiederholen des Zustandsurteils und das Erkennen von "Warten" als "Besetztgewicht" bezeichnet. Dies führt zu folgenden Problemen:
  • Macht das Programm anfällig für unvorhersehbare Verarbeitung durch den Thread-Scheduler.
  • Erhöht die Belastung des Prozessors und erschwert die Verarbeitung durch andere Threads.
  • Verwenden Sie Thread.yield nicht. Diese Methode zeigt dem Scheduler an, dass "die dem eigenen Thread zugewiesene CPU-Auslastung auf einen anderen Thread übertragen werden kann". Verwenden Sie Thread.yield nicht, da es unvorhersehbar und instabil ist, wie sich der Scheduler als Reaktion darauf verhält.
  • Passen Sie die Thread-Prioritäten nicht an. Sie können die Thread-Priorität mit Thread.setPriority (int newPriority) anpassen, aber wie bei Thread.yield bin ich mir nicht sicher, wie sich der Scheduler als Reaktion darauf verhält.

Kapitel 12 Serialisierung

Punkt 85: Wählen Sie eine alternative Methode gegenüber der Java-Serialisierung

Wenn Sie auch nur einen Ort in Ihrem System deserialisieren, besteht die Gefahr, dass Sie ein von einem Angreifer erstelltes schädliches Objekt deserialisieren. Während der Deserialisierung wird bösartiger Code ausgeführt, was zu schwerwiegenden Problemen wie Systemstillstand führt.

Aufgrund dieser Sicherheitsprobleme sollten Sie Serialize / Deserialize überhaupt nicht verwenden. Verwenden Sie stattdessen Technologien wie JSON und Protobuf. Durch die Verwendung dieser Technologien können Sie viele der oben genannten Sicherheitsprobleme vermeiden und die Vorteile des plattformübergreifenden Supports, der hohen Leistung, eines Ökosystems von Tools und des Community-Supports nutzen.

Wenn Sie keine andere Wahl haben, als den Serialisierungsmechanismus für die Wartung eines vorhandenen Systems zu verwenden, ergreifen Sie die folgenden Maßnahmen.

  • Deserialisieren Sie nicht vertrauenswürdige Daten nicht.
  • Verwenden Sie java.io.ObjectInputFilter, das in Java 9 hinzugefügt wurde, um nur die Whitelist-Klassen zu deserialisieren. Dies unterstützt jedoch keinen Angriffscode, der nur aus allgemeinen Klassen besteht.

Punkt 86 Implementieren Serialisierbar mit der neuesten Aufmerksamkeit

Wenn Sie einen Serialisierungsmechanismus anwenden, sollten Sie sich der damit verbundenen Kosten bewusst sein. Der Preis ist wie folgt:

  • Nach der Veröffentlichung verlieren Sie fast die Flexibilität, die Implementierung einer Klasse zu ändern, die Serializable implementiert.
  • Erhöht die Wahrscheinlichkeit von Fehlern und Sicherheitslücken.
  • Um die Testlast zu erhöhen, die mit der Veröffentlichung einer neuen Version der Klasse verbunden ist.

Beachten Sie bei der Implementierung von Serializable Folgendes:

  • Legen Sie explizit die UID der seriellen Version für die Klasse fest. Wenn Sie es nicht explizit festlegen, wird die ID automatisch zugewiesen und als inkompatibel beurteilt, obwohl es sich um eine kompatible Änderung handelt.
  • Grundsätzlich sollten für die Vererbung konzipierte Klassen Serializable nicht implementieren. Die Schnittstelle sollte Serializable grundsätzlich nicht erweitern. Diejenigen, die diese Klassen und Schnittstellen verwenden, müssen Serializable implementieren.
  • Beachten Sie beim Implementieren einer Klasse, die serialisierbar, erweiterbar und mit Instanzfeldern versehen ist, Folgendes:
  • Unterklassen sollten die Finalisierungsmethode nicht überschreiben können. Verhindern Sie Finalizer-Angriffe.
  • Wenn Sie Probleme haben, die Instanzfelder auf ihre Standardwerte zu initialisieren, fügen Sie eine readObjectNoData-Methode hinzu.
  • Mit Ausnahme von statischen Elementklassen sollten innere Klassen Serializable nicht implementieren. Dies liegt daran, dass es Elemente wie Verweise auf einschließende Instanzen gibt, deren Serialisierungsformat ungewiss ist.

Artikel 87-90

Es tut mir leid, ich habe keine Zeit mehr, einen Artikel zu schreiben, also würde ich ihn gerne schreiben, wenn ich Zeit habe. Ich möchte jedoch erwähnen, dass es nicht zu spät ist, mit dem Lernen zu beginnen, nachdem Seirializable tatsächlich implementiert werden muss.

abschließend

Wenn Sie Fehler haben, lassen Sie es uns bitte wissen!

Recommended Posts