Einführung in AutoValue

Einführung in AutoValue

1. Überblick

AutoValue ist ein Quellcodegenerator für Java und insbesondere eine Bibliothek fürgenerating source code for value objects or value-typed objects.

Um ein Werttypobjekt zu generieren, müssen Sie nurannotate an abstract class with the @AutoValue annotation eingeben und Ihre Klasse kompilieren. Was generiert wird, ist ein Wertobjekt mit Zugriffsmethoden, parametrisiertem Konstruktor, ordnungsgemäß überschriebenen MethodentoString(), equals(Object) undhashCode().

Das folgende Codefragment ista quick example einer abstrakten Klasse, die beim Kompilieren zu einem Wertobjekt mit dem NamenAutoValue_Person führt.

@AutoValue
abstract class Person {
    static Person create(String name, int age) {
        return new AutoValue_Person(name, age);
    }

    abstract String name();
    abstract int age();
}

Lassen Sie uns fortfahren und mehr über Wertobjekte erfahren, warum wir sie benötigen und wie AutoValue dazu beitragen kann, das Generieren und Umgestalten von Code wesentlich zeitsparender zu gestalten.

2. Maven Setup

Um AutoValue in einem Maven-Projekt zu verwenden, müssen Sie die folgenden Abhängigkeiten inpom.xml aufnehmen:


    com.google.auto.value
    auto-value
    1.2

Die neueste Version finden Sie unterthis link.

3. Werttypisierte Objekte

Werttypen sind das Endprodukt der Bibliothek. Um ihren Platz in unseren Entwicklungsaufgaben zu schätzen, müssen wir Werttypen genau verstehen, was sie sind, was sie nicht sind und warum wir sie brauchen.

3.1. Was sind Werttypen?

Wertobjekte sind Objekte, deren Gleichheit sich nicht nach der Identität, sondern nach ihrem inneren Zustand richtet. Dies bedeutet, dass zwei Instanzen eines Objekts mit Werttyp als gleich betrachtet werden, solange sie gleiche Feldwerte haben.

Typically, value-types are immutable. Ihre Felder müssenfinal sein und sie dürfen keinesetter-Methoden haben, da sie dadurch nach der Instanziierung geändert werden können.

Sie müssen alle Feldwerte über einen Konstruktor oder eine Factory-Methode verarbeiten.

Werttypen sind keine JavaBeans, da sie keinen Standard- oder Nullargumentkonstruktor haben und auch keine Setter-Methoden, ähnlichthey are not Data Transfer Objects nor Plain Old Java Objects.

Darüber hinaus muss eine Klasse vom Typ Wert final sein, damit sie nicht erweiterbar ist, zumindest, wenn jemand die Methoden überschreibt. JavaBeans, DTOs und POJOs müssen nicht endgültig sein.

3.2. Werttyp erstellen

Angenommen, wir möchten einen Werttyp namensFoo mit Feldern namenstext undnumber. erstellen. Wie würden wir vorgehen?

Wir würden eine Abschlussklasse bilden und alle ihre Felder als endgültig markieren. Dann würden wir die IDE verwenden, um den Konstruktor, diehashCode()-Methode, dieequals(Object)-Methode, diegetters als obligatorische Methoden und einetoString()-Methode zu generieren, und wir hätten eine Klasse wie so:

public final class Foo {
    private final String text;
    private final int number;

    public Foo(String text, int number) {
        this.text = text;
        this.number = number;
    }

    // standard getters

    @Override
    public int hashCode() {
        return Objects.hash(text, number);
    }
    @Override
    public String toString() {
        return "Foo [text=" + text + ", number=" + number + "]";
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Foo other = (Foo) obj;
        if (number != other.number) return false;
        if (text == null) {
            if (other.text != null) return false;
        } else if (!text.equals(other.text)) {
            return false;
        }
        return true;
    }
}

Nach dem Erstellen einer Instanz vonFoo erwarten wir, dass der interne Zustand über den gesamten Lebenszyklus gleich bleibt.

Wie wir im folgenden Unterabschnittthe hashCode of an object must change from instance to instance sehen werden, müssen wir es für Werttypen an die Felder binden, die den internen Status des Wertobjekts definieren.

Daher würde selbst das Ändern eines Feldes desselben Objekts den Wert vonhashCodeändern.

3.3. Wie Werttypen funktionieren

Der Grund, warum Werttypen unveränderlich sein müssen, besteht darin, dass die Anwendung nach der Instanziierung keine Änderung ihres internen Status durchführt.

Wann immer wir zwei beliebige werttypisierte Objekte vergleichen möchten,we must, therefore, use the equals(Object) method of the Object class.

Dies bedeutet, dass wir diese Methode in unseren eigenen Werttypen immer überschreiben und nur dann true zurückgeben müssen, wenn die Felder der zu vergleichenden Wertobjekte gleiche Werte haben.

Darüber hinaus können wir unsere Wertobjekte in Hash-basierten Sammlungen wieHashSets undHashMaps verwenden, ohne zu brechen,we must properly implement the hashCode() method.

3.4. Warum wir Werttypen brauchen

Das Bedürfnis nach Werttypen taucht ziemlich oft auf. In diesen Fällen möchten wir das Standardverhalten der ursprünglichen KlasseObjectüberschreiben.

Wie wir bereits wissen, werden bei der Standardimplementierung der KlasseObjectzwei Objekte gleich betrachtet, wenn sie dieselbe Identität haben, jedochfor our purposes we consider two objects equal when they have the same internal state.

Angenommen, wir möchten ein Geldobjekt wie folgt erstellen:

public class MutableMoney {
    private long amount;
    private String currency;

    public MutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    // standard getters and setters

}

Wir können den folgenden Test durchführen, um die Gleichheit zu testen:

@Test
public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() {
    MutableMoney m1 = new MutableMoney(10000, "USD");
    MutableMoney m2 = new MutableMoney(10000, "USD");
    assertFalse(m1.equals(m2));
}

Beachten Sie die Semantik des Tests.

Wir betrachten es als bestanden, wenn die beiden Geldobjekte nicht gleich sind. Dies liegt daran, dasswe have not overridden the equals method so gleich ist, indem die Speicherreferenzen der Objekte verglichen werden, die natürlich nicht unterschiedlich sein werden, da es sich um unterschiedliche Objekte handelt, die unterschiedliche Speicherplätze belegen.

Jedes Objekt repräsentiert 10.000 USD, aberJava tells us our money objects are not equal. Wir möchten, dass die beiden Objekte nur dann ungleich sind, wenn entweder die Währungsbeträge oder die Währungstypen unterschiedlich sind.

Jetzt erstellen wir ein Objekt mit gleichem Wert und dieses Mal lässt die IDE den größten Teil des Codes generieren:

public final class ImmutableMoney {
    private final long amount;
    private final String currency;

    public ImmutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (amount ^ (amount >>> 32));
        result = prime * result + ((currency == null) ? 0 : currency.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        ImmutableMoney other = (ImmutableMoney) obj;
        if (amount != other.amount) return false;
        if (currency == null) {
            if (other.currency != null) return false;
        } else if (!currency.equals(other.currency))
            return false;
        return true;
    }
}

Der einzige Unterschied besteht darin, dass wir die Methodenequals(Object) undhashCode() überschrieben haben. Jetzt haben wir die Kontrolle darüber, wie Java unsere Geldobjekte vergleichen soll. Lassen Sie uns den entsprechenden Test ausführen:

@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
    ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
    ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
    assertTrue(m1.equals(m2));
}

Beachten Sie die Semantik dieses Tests. Wir erwarten, dass er bestanden wird, wenn beide Geldobjekte über die Methodeequalsgleich getestet werden.

4. Warum AutoValue?

Nachdem wir die Wertetypen gründlich verstanden haben und wissen, warum sie benötigt werden, können wir uns AutoValue und die Art und Weise ansehen, wie sie in die Gleichung eingehen.

4.1. Probleme mit der Handcodierung

Wenn wir Werttypen erstellen, wie wir es im vorhergehenden Abschnitt getan haben, werden wir auf eine Reihe von Problemen stoßen, die sich aufbad design and a lot of boilerplate code beziehen.

Eine Klasse mit zwei Feldern hat 9 Codezeilen: eine für die Paketdeklaration, zwei für die Klassensignatur und ihre schließende Klammer, zwei für die Felddeklaration, zwei für Konstruktoren und ihre schließende Klammer und zwei für die Initialisierung der Felder, aber dann brauchen wir Getter für die Felder jeweils drei weitere Codezeilen und sechs zusätzliche Zeilen.

Das Überschreiben der MethodenhashCode() undequalTo(Object)erfordert etwa 9 Zeilen bzw. 18 Zeilen, und das Überschreiben der MethodetoString() fügt weitere fünf Zeilen hinzu.

Das bedeutet, dass eine gut formatierte Codebasis für unsere beiden Feldklassenabout 50 lines of code benötigt.

4.2 IDEs to The Rescue?

Dies ist mit einer IDE wie Eclipse oder IntilliJ und nur mit ein oder zwei wertetypisierten Klassen einfach zu erstellen. Denken Sie über eine Vielzahl solcher Klassen nach, wäre es immer noch so einfach, selbst wenn die IDE uns hilft?

Schneller Vorlauf, einige Monate später, nehmen wir an, wir müssen unseren Code überarbeiten und Änderungen an unserenMoney-Klassen vornehmen und möglicherweise dascurrency-Feld vomString-Typ in einen anderen Werttyp konvertieren genanntCurrency.

4.3 IDEs Not Really so Helpful

Eine IDE wie Eclipse kann für uns weder unsere Zugriffsmethoden noch die MethodentoString(),hashCode() oderequals(Object) einfach bearbeiten.

This refactoring would have to be done by hand. Das Bearbeiten von Code erhöht das Fehlerpotential und mit jedem neuen Feld, das wir der KlasseMoneyhinzufügen, steigt die Anzahl der Zeilen exponentiell an.

Das Erkennen der Tatsache, dass dieses Szenario auftritt, dass es häufig und in großen Mengen auftritt, lässt uns die Rolle von AutoValue wirklich schätzen.

5. AutoValue-Beispiel

Das Problem, das AutoValue löst, besteht darin, den gesamten Code, über den wir im vorhergehenden Abschnitt gesprochen haben, aus dem Weg zu räumen, damit wir ihn nie schreiben, bearbeiten oder sogar lesen müssen.

Wir werden uns das gleicheMoney-Beispiel ansehen, diesmal jedoch mit AutoValue. Wir werden diese KlasseAutoValueMoney aus Gründen der Konsistenz nennen:

@AutoValue
public abstract class AutoValueMoney {
    public abstract String getCurrency();
    public abstract long getAmount();

    public static AutoValueMoney create(String currency, long amount) {
        return new AutoValue_AutoValueMoney(currency, amount);
    }
}

Was passiert ist, ist, dass wir eine abstrakte Klasse schreiben, abstrakte Accessoren dafür definieren, aber keine Felder, die Klasse mit@AutoValue annotieren, die alle nur 8 Codezeilen umfassen, undjavac eine konkrete Unterklasse für generiert uns was so aussieht:

public final class AutoValue_AutoValueMoney extends AutoValueMoney {
    private final String currency;
    private final long amount;

    AutoValue_AutoValueMoney(String currency, long amount) {
        if (currency == null) throw new NullPointerException(currency);
        this.currency = currency;
        this.amount = amount;
    }

    // standard getters

    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= currency.hashCode();
        h *= 1000003;
        h ^= amount;
        return h;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof AutoValueMoney) {
            AutoValueMoney that = (AutoValueMoney) o;
            return (this.currency.equals(that.getCurrency()))
              && (this.amount == that.getAmount());
        }
        return false;
    }
}

Wir müssen uns nie direkt mit dieser Klasse befassen und sie auch nicht bearbeiten, wenn wir weitere Felder hinzufügen oder Änderungen an unseren Feldern vornehmen müssen, wie im Szenariocurrencyim vorherigen Abschnitt.

Javac will always regenerate updated code for us.

Bei Verwendung dieses neuen Wertetyps wird allen Anrufern nur der übergeordnete Typ angezeigt, wie wir in den folgenden Unit-Tests sehen werden.

Hier ist ein Test, der bestätigt, dass unsere Felder korrekt eingestellt sind:

@Test
public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoney m = AutoValueMoney.create("USD", 10000);
    assertEquals(m.getAmount(), 10000);
    assertEquals(m.getCurrency(), "USD");
}

Es folgt ein Test, um zu überprüfen, ob zweiAutoValueMoney Objekte mit derselben Währung und demselben Betrag gleich sind:

@Test
public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertTrue(m1.equals(m2));
}

Wenn wir den Währungstyp eines Geldobjekts in GBP ändern, ist der Test:5000 GBP == 5000 USD nicht mehr wahr:

@Test
public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertFalse(m1.equals(m2));
}

6. AutoValue mit Buildern

Das erste Beispiel, das wir uns angesehen haben, behandelt die grundlegende Verwendung von AutoValue unter Verwendung einer statischen Factory-Methode als öffentliche Erstellungs-API.

Notice that if all our fields were*Strings*, Es wäre einfach, sie auszutauschen, wenn wir sie an die statische Factory-Methode übergeben würden, z. B. indem wiramount anstelle voncurrency platzieren und umgekehrt.

Dies ist besonders wahrscheinlich, wenn wir viele Felder haben und alle vom TypStringind. Dieses Problem wird durch die Tatsache verschlimmert, dass mit AutoValueall fields are initialized through the constructor.

Um dieses Problem zu lösen, sollten wir das Musterbuilderverwenden. Glücklicherweise. Dies kann durch AutoValue generiert werden.

Unsere AutoValue-Klasse ändert sich kaum, außer dass die statische Factory-Methode durch einen Builder ersetzt wird:

@AutoValue
public abstract class AutoValueMoneyWithBuilder {
    public abstract String getCurrency();
    public abstract long getAmount();
    static Builder builder() {
        return new AutoValue_AutoValueMoneyWithBuilder.Builder();
    }

    @AutoValue.Builder
    abstract static class Builder {
        abstract Builder setCurrency(String currency);
        abstract Builder setAmount(long amount);
        abstract AutoValueMoneyWithBuilder build();
    }
}

Die generierte Klasse ist dieselbe wie die erste, jedoch wird eine konkrete innere Klasse für den Builder generiert und die abstrakten Methoden im Builder implementiert:

static final class Builder extends AutoValueMoneyWithBuilder.Builder {
    private String currency;
    private long amount;
    Builder() {
    }
    Builder(AutoValueMoneyWithBuilder source) {
        this.currency = source.getCurrency();
        this.amount = source.getAmount();
    }

    @Override
    public AutoValueMoneyWithBuilder.Builder setCurrency(String currency) {
        this.currency = currency;
        return this;
    }

    @Override
    public AutoValueMoneyWithBuilder.Builder setAmount(long amount) {
        this.amount = amount;
        return this;
    }

    @Override
    public AutoValueMoneyWithBuilder build() {
        String missing = "";
        if (currency == null) {
            missing += " currency";
        }
        if (amount == 0) {
            missing += " amount";
        }
        if (!missing.isEmpty()) {
            throw new IllegalStateException("Missing required properties:" + missing);
        }
        return new AutoValue_AutoValueMoneyWithBuilder(this.currency,this.amount);
    }
}

Beachten Sie auch, dass sich die Testergebnisse nicht ändern.

Wenn wir wissen wollen, dass die Feldwerte über den Builder tatsächlich richtig eingestellt sind, können wir diesen Test ausführen:

@Test
public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder().
      setAmount(5000).setCurrency("USD").build();
    assertEquals(m.getAmount(), 5000);
    assertEquals(m.getCurrency(), "USD");
}

Um zu testen, ob die Gleichheit vom internen Zustand abhängt:

@Test
public void given2EqualValueTypesWithBuilder_whenEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    assertTrue(m1.equals(m2));
}

Und wenn die Feldwerte unterschiedlich sind:

@Test
public void given2DifferentValueTypesBuilder_whenNotEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("GBP").build();
    assertFalse(m1.equals(m2));
}

7. Fazit

In diesem Tutorial haben wir die meisten Grundlagen der AutoValue-Bibliothek von Google vorgestellt und erläutert, wie Sie damit Werttypen mit sehr wenig Code erstellen können.

Eine Alternative zu AutoValue von Google istLombok project. Sie können den einleitenden Artikel über die Verwendung vonLombok here lesen.

Die vollständige Implementierung all dieser Beispiele und Codefragmente finden Sie in den AutoValueGitHub project.