[JAVA] Punkt 37: Verwenden Sie EnumMap anstelle der ordinalen Indizierung

37. Verwenden Sie EnumMap anstelle der Ordnungszahl

Betrachten Sie als Beispiel für die Verwendung der Ordnungsmethode (Punkt 35) die folgende Plant-Klasse.

class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
 final String name;
    final LifeCycle lifeCycle;
     Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }
 @Override public String toString() {
        return name;
    }
}

Angenommen, Sie möchten für jeden Lebenszyklus ein Array von Pflanzen erstellen. Dazu können Sie mit dem Original Folgendes tun.

// Using ordinal() to index into an array - DON'T DO THIS!
Set<Plant>[] plantsByLifeCycle =
    (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++)
    plantsByLifeCycle[i] = new HashSet<>();
 for (Plant p : garden)
    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
 // Print the results
for (int i = 0; i < plantsByLifeCycle.length; i++) {
    System.out.printf("%s: %s%n",
        Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

Das größte Problem hierbei ist, dass Sie beim Versuch, auf ein Array zuzugreifen, den richtigen ganzzahligen Wert für das Array auswählen müssen, das sich im Original von enum befindet. Wenn Sie die falsche Nummer wählen, erhalten Sie eine ArrayIndexOutOfBoundsException, wenn Sie Glück haben. Wenn Sie dies nicht tun, wird der Vorgang mit der falschen Auswahl fortgesetzt. Es gibt einen besseren Weg, um dasselbe zu erreichen. Da sich das Array hier wie eine Map verhält, die mit enum als Schlüssel einen Wert erhält, ist es besser, Map für die Implementierung zu verwenden. Darüber hinaus wird eine effiziente Implementierung als Map mit Enum als Schlüssel in `` `EnumMap``` durchgeführt, sodass wir unten ein Beispiel sehen werden, das dies verwendet.

// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>>  plantsByLifeCycle =
    new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
    plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
    plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);

Diese Implementierung ist kürzer, übersichtlicher und sicherer. Insbesondere hat es die folgenden Vorteile.

Außerdem verwendet der EnumMap-Konstruktor ein Class-Objekt als Argument, das ein begrenzter Typ ist und zur Laufzeit Informationen zum generischen Typ bereitstellt (Item33). Das vorherige Beispiel ist mit einem Stream noch kürzer (Item45):

// Naive stream-based approach - unlikely to produce an EnumMap!
System.out.println(Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle)));

Das Problem bei dieser Implementierung ist, dass EnumMap nicht verwendet wird und seine Leistung schlechter ist als bei Verwendung. Um dies zu beheben, zeigen Sie die Karte an, die explizit wie folgt verwendet wird.

// Using a stream and an EnumMap to associate data with an enum
System.out.println(Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle,
            () -> new EnumMap<>(LifeCycle.class), toSet())));

Diese Optimierung wird wichtig, wenn Map häufig verwendet wird.

Zwei Aufzählungen können unter Verwendung eines Arrays von Sequenzen dargestellt werden, die die Ordnungswerte verwenden. Der folgende Quellcode ist ein Beispiel für Änderungen zwischen zwei Zuständen.

public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        // Rows indexed by from-ordinal, cols by to-ordinal
        private static final Transition[][] TRANSITIONS = { { null, MELT, SUBLIME }, { FREEZE, null, BOIL },
                { DEPOSIT, CONDENSE, null } };

        // Returns the phase transition from one phase to another
        public static Transition from(Phase from, Phase to) {
            return TRANSITIONS[from.ordinal()][to.ordinal()];
        }

    }
}

Es gibt ein Problem mit diesem Programm. Der Compiler hat keine Möglichkeit, die Beziehung zwischen dem ursprünglichen Wert und dem Index des Arrays zu kennen. Wenn er das Array nicht erstellt oder vergisst, das Array mit der Informationsaktualisierung zu aktualisieren, schlägt er entweder zur Laufzeit oder zur ArrayIndexOutOfBoundsException fehl Oder es tritt eine NullPointerException auf oder der Prozess wird mit falschem Verhalten fortgesetzt. Die obige Aufzählung kann besser mit "EnumMap" geschrieben werden.

// Using a nested EnumMap to associate data with enum pairs
public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID), BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), SUBLIME(SOLID,
                GAS), DEPOSIT(GAS, SOLID);
        private final Phase from;
        private final Phase to;

        Transition(Phase from, Phase to) {
            this.from = from;
            this.to = to;
        }

        // Initialize the phase transition map
        private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
                .collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
                        toMap(t -> t.to, t -> t, (x, y) -> y, () -> new EnumMap<>(Phase.class))));

        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
    }
}

Der Initialisierungsteil dieses Codes ist etwas kompliziert. Das dritte Argument in toMap, `` `(x, y) -> y```, wird nicht verwendet und wird nur zum Abrufen der EnumMap benötigt. Nehmen wir nun an, wir wollen einen neuen Zustand definieren, Plasma. Die hier hinzugefügten Zustandsübergangsdefinitionen sind Ionisation und Deionisation. Wenn Sie in einer Array-basierten Aufzählung versuchen, diese Änderung zu übernehmen, fügen Sie eine neue Konstante zu "Phase" hinzu, fügen Sie zwei Konstanten zu "Phase.Transition" und 9 Elementen hinzu Das vorhandene zweidimensionale Array muss neu geschrieben werden, damit es 16 Elemente enthält. Wenn die Aufzählung hingegen auf "EnumMap" basiert, fügen Sie eine neue Konstante zu "Phase" hinzu und fügen Sie zwei Konstanten zu "Phase.Transition" hinzu, wie unten gezeigt. Alles was Sie tun müssen, ist es hinzuzufügen.

// Adding a new phase using the nested EnumMap implementation
public enum Phase {
    SOLID, LIQUID, GAS, PLASMA;
     public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
        ... // Remainder unchanged
    }
}

Dieser Code kann keine menschlichen Fehler verursachen, und da EnumMap ein Array von Arrays verwendet, gibt es keine minderwertige Leistung.

Recommended Posts

Punkt 37: Verwenden Sie EnumMap anstelle der ordinalen Indizierung
Punkt 36: Verwenden Sie EnumSet anstelle von Bitfeldern
Verwenden Sie enum anstelle der int-Konstante
Punkt 71: Vermeiden Sie die unnötige Verwendung geprüfter Ausnahmen
Punkt 72: Bevorzugen Sie die Verwendung von Standardausnahmen
[Java] Warum StringUtils.isEmpty () anstelle von String.isEmpty () verwenden?
Punkt 87: Erwägen Sie die Verwendung eines benutzerdefinierten serialisierten Formulars
[Read Effective Java] Kapitel 2 Punkt 1 "Betrachten Sie statische Factory-Methoden anstelle von Konstruktoren"
Punkt 36: Verwenden Sie EnumSet anstelle von Bitfeldern
Punkt 37: Verwenden Sie EnumMap anstelle der ordinalen Indizierung
Punkt 44: Bevorzugen Sie die Verwendung von Standardfunktionsschnittstellen
[Swift] Warum FlowLayout Delegate anstelle von Instanz verwenden sollte
Verwendung der Datumsklasse
Punkt 53: Verwenden Sie Varargs mit Bedacht
Punkt 45: Verwenden Sie Streams mit Bedacht