Einführung in AutoValue

1. Übersicht

AutoValue ist ein Quellcodegenerator für Java und insbesondere eine Bibliothek zum Generieren von Quellcode für Wertobjekte oder werttypisierte Objekte .

Um ein Werttypobjekt zu generieren, müssen Sie lediglich eine abstrakte Klasse mit der Annotation @AutoValue versehen und Ihre Klasse kompilieren. Was generiert wird, ist ein Wertobjekt mit Zugriffsmethoden, einem parametrisierten Konstruktor, der ordnungsgemäß mit den Methoden String (), equals (Object) und hashCode () überschrieben wird .

Das folgende Codefragment ist ein kurzes Beispiel für eine abstrakte Klasse, die beim Kompilieren zu einem Wertobjekt mit dem Namen AutoValue_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 folgende Abhängigkeit in die pom.xml aufnehmen :

 com.google.auto.value auto-value 1.2 

Die neueste Version finden Sie unter diesem Link.

3. Werttypisierte Objekte

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

3.1. Was sind Werttypen?

Werteobjekte sind Objekte, deren Gleichheit nicht durch die Identität bestimmt wird, sondern durch ihren internen Zustand. Dies bedeutet, dass zwei Instanzen eines werttypisierten Objekts als gleich betrachtet werden, solange sie gleiche Feldwerte haben.

Werttypen sind normalerweise unveränderlich . Ihre Felder müssen endgültig sein und sie dürfen keine Setter- 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 verbrauchen.

Werttypen sind keine JavaBeans, da sie keinen Standard- oder Nullargumentkonstruktor haben und auch keine Setter-Methoden haben. Ebenso sind sie keine Datenübertragungsobjekte oder einfache alte Java-Objekte .

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

3.2. Werttyp erstellen

Angenommen, wir möchten einen Werttyp namens Foo mit Feldern namens Text und Zahl 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, die hashCode () -Methode, die equals (Object) -Methode, die Getter als obligatorische Methoden und eine toString () -Methode zu generieren , und wir hätten eine Klasse wie diese:

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 von Foo erwarten wir, dass der interne Status über den gesamten Lebenszyklus gleich bleibt.

Wie wir im folgenden Unterabschnitt sehen werden, muss sich der Hashcode eines Objekts von Instanz zu Instanz ändern , aber für Werttypen müssen wir ihn mit den Feldern verknüpfen, die den internen Status des Wertobjekts definieren.

Daher würde selbst das Ändern eines Felds desselben Objekts den hashCode- Wert ändern .

3.3. Wie Werttypen funktionieren

Der Grund, warum Werttypen unveränderlich sein müssen, besteht darin, zu verhindern, dass die Anwendung ihren internen Status ändert, nachdem sie instanziiert wurden.

Wann immer wir zwei werttypisierte Objekte vergleichen möchten, müssen wir daher die Methode equals (Object) der Object- Klasse verwenden .

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 für uns , unsere Wertobjekte in Hash-basierte verwenden Sammlungen wie HashSet s und HashMap s , ohne zu brechen, müssen wir richtig die Umsetzung hashCode () Methode .

3.4. Warum wir Werttypen brauchen

Der Bedarf an Werttypen kommt ziemlich oft vor. In diesen Fällen möchten wir das Standardverhalten der ursprünglichen Object- Klasse überschreiben .

Wie wir bereits wissen, betrachtet die Standardimplementierung der Object- Klasse zwei Objekte als gleich, wenn sie dieselbe Identität haben. Für unsere Zwecke betrachten wir jedoch zwei Objekte als gleich, wenn sie denselben internen Status haben .

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 seine 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 vorbei, wenn die beiden Geldobjekte nicht gleich sind. Dies liegt daran, dass wir die Methode equals nicht überschrieben haben , sodass die Gleichheit durch Vergleichen der Speicherreferenzen der Objekte gemessen wird, 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, aber Java sagt uns, dass unsere Geldobjekte nicht gleich sind . Wir möchten, dass die beiden Objekte nur dann ungleich getestet werden, wenn entweder die Währungsbeträge oder die Währungstypen unterschiedlich sind.

Lassen Sie uns nun ein Objekt mit äquivalentem Wert erstellen und dieses Mal lassen wir 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 Methoden equals (Object) und hashCode () ü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 Methode equals gleich getestet werden.

4. Warum AutoValue?

Nachdem wir die Werttypen genau verstanden haben und wissen, warum wir sie brauchen, können wir uns AutoValue ansehen und wie es in die Gleichung kommt.

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 mit schlechtem Design und viel Boilerplate-Code zusammenhängen .

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 Felddeklarationen, 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 werden jeweils drei weitere Codezeilen verwendet, wodurch sechs zusätzliche Zeilen entstehen.

Overriding the hashCode() and equalTo(Object) methods require about 9 lines and 18 lines respectively and overriding the toString() method adds another five lines.

That means a well-formatted code base for our two field class would take about 50 lines of code.

4.2 IDEs to The Rescue?

This is is easy with an IDE like Eclipse or IntilliJ and with only one or two value-typed classes to create. Think about a multitude of such classes to create, would it still be as easy even if the IDE helps us?

Fast forward, some months down the road, assume we have to revisit our code and make amendments to our Money classes and perhaps convert the currency field from the String type to another value-type called Currency.

4.3 IDEs Not Really so Helpful

An IDE like Eclipse can't simply edit for us our accessor methods nor the toString(), hashCode() or equals(Object) methods.

This refactoring would have to be done by hand. Editing code increases the potential for bugs and with every new field we add to the Money class, the number of lines increases exponentially.

Recognizing the fact that this scenario happens, that it happens often and in large volumes will make us really appreciate the role of AutoValue.

5. AutoValue Example

The problem AutoValue solves is to take all the boilerplate code that we talked about in the preceding section, out of our way so that we never have to write it, edit it or even read it.

We will look at the very same Money example, but this time with AutoValue. We will call this class AutoValueMoney for the sake of consistency:

@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); } }

What has happened is that we write an abstract class, define abstract accessors for it but no fields, we annotate the class with @AutoValue all totalling to only 8 lines of code, and javac generates a concrete subclass for us which looks like this:

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

We never have to deal with this class directly at all, neither do we have to edit it when we need to add more fields or make changes to our fields like the currency scenario in the previous section.

Javac will always regenerate updated code for us.

While using this new value-type, all callers see is only the parent type as we will see in the following unit tests.

Here is a test that verifies that our fields are being set correctly:

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

A test to verify that two AutoValueMoney objects with the same currency and same amount test equal follow:

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

When we change the currency type of one money object to GBP, the test: 5000 GBP == 5000 USD is no longer true:

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

6. AutoValue With Builders

The initial example we have looked at covers the basic usage of AutoValue using a static factory method as our public creation API.

Notice that if all our fields were Strings, it would be easy to interchange them as we passed them to the static factory method, like placing the amount in the place of currency and vice versa.

This is especially likely to happen if we have many fields and all are of String type. This problem is made worse by the fact that with AutoValue, all fields are initialized through the constructor.

To solve this problem we should use the builder pattern. Fortunately. this can be generated by AutoValue.

Our AutoValue class does not really change much, except that the static factory method is replaced by a builder:

@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(); } }

The generated class is just the same as the first one but a concrete inner class for the builder is generated as well implementing the abstract methods in the builder:

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

Notice also how the test results don't change.

If we want to know that the field values are actually correctly set through the builder, we can execute this test:

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

To test that equality depends on internal state:

@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)); }

And when the field values are different:

@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. Conclusion

In this tutorial, we have introduced most of the basics of Google's AutoValue library and how to use it to create value-types with a very little code on our part.

An alternative to Google's AutoValue is the Lombok project – you can have a look at the introductory article about using Lombok here.

Die vollständige Implementierung all dieser Beispiele und Codefragmente finden Sie im AutoValue GitHub-Projekt.