Introduzione ad AutoValue

1. Panoramica

AutoValue è un generatore di codice sorgente per Java e, più specificamente, è una libreria per generare codice sorgente per oggetti valore o oggetti tipizzati valore .

Per generare un oggetto di tipo valore, tutto ciò che devi fare è annotare una classe astratta con l' annotazione @AutoValue e compilare la tua classe. Ciò che viene generato è un oggetto valore con metodi di accesso, costruttore parametrizzato, metodi toString (), equals (Object) e hashCode () opportunamente sovrascritti .

Il frammento di codice seguente è un rapido esempio di una classe astratta che, una volta compilata, darà come risultato un oggetto valore denominato AutoValue_Person .

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

Continuiamo e scopriamo di più sugli oggetti valore, perché ne abbiamo bisogno e in che modo AutoValue può aiutare a rendere l'attività di generazione e refactoring del codice molto meno dispendiosa in termini di tempo.

2. Installazione di Maven

Per utilizzare AutoValue in un progetto Maven, è necessario includere la seguente dipendenza nel pom.xml :

 com.google.auto.value auto-value 1.2 

L'ultima versione può essere trovata seguendo questo link.

3. Oggetti di tipo Value

I tipi di valore sono il prodotto finale della libreria, quindi per apprezzare il suo posto nelle nostre attività di sviluppo, dobbiamo comprendere a fondo i tipi di valore, cosa sono, cosa non sono e perché ne abbiamo bisogno.

3.1. Cosa sono i tipi di valore?

Gli oggetti di tipo valore sono oggetti la cui uguaglianza non è determinata dall'identità ma piuttosto dal loro stato interno. Ciò significa che due istanze di un oggetto di tipo valore sono considerate uguali fintanto che hanno valori di campo uguali.

In genere, i tipi di valore sono immutabili . I loro campi devono essere resi definitivi e non devono avere metodi setter in quanto ciò li renderà modificabili dopo l'istanza.

Devono utilizzare tutti i valori di campo tramite un costruttore o un metodo factory.

I tipi di valore non sono JavaBeans perché non hanno un costruttore di argomenti predefinito o zero e non hanno metodi setter, allo stesso modo, non sono oggetti di trasferimento dati né semplici oggetti Java vecchi .

Inoltre, una classe di tipo valore deve essere finale, in modo che non siano estendibili, almeno che qualcuno sovrascriva i metodi. JavaBeans, DTO e POJO non devono essere definitivi.

3.2. Creazione di un tipo di valore

Supponendo di voler creare un tipo di valore chiamato Foo con campi chiamati testo e numero. Come lo faremmo?

Faremmo una lezione finale e contrassegneremo tutti i suoi campi come finali. Quindi useremmo l'IDE per generare il costruttore, il metodo hashCode () , il metodo equals (Object) , i getter come metodi obbligatori e un metodo toString () , e avremmo una classe come questa:

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

Dopo aver creato un'istanza di Foo , ci aspettiamo che il suo stato interno rimanga lo stesso per l'intero ciclo di vita.

Come vedremo nella seguente sottosezione, il codice hash di un oggetto deve cambiare da istanza a istanza , ma per i tipi valore, dobbiamo legarlo ai campi che definiscono lo stato interno dell'oggetto valore.

Pertanto, anche la modifica di un campo dello stesso oggetto cambierebbe il valore hashCode .

3.3. Come funzionano i tipi di valore

Il motivo per cui i tipi di valore devono essere immutabili è impedire qualsiasi modifica al loro stato interno da parte dell'applicazione dopo essere stati istanziati.

Ogni volta che vogliamo confrontare due oggetti di tipo valore, dobbiamo, quindi, utilizzare il metodo equals (Object) della classe Object .

Ciò significa che dobbiamo sempre sovrascrivere questo metodo nei nostri tipi di valore e restituire true solo se i campi degli oggetti valore che stiamo confrontando hanno valori uguali.

Inoltre, per noi di usare i nostri oggetti di valore in collezioni hash-based come HashSet s e HashMap s senza rompere, dobbiamo attuare correttamente la hashCode () metodo .

3.4. Perché abbiamo bisogno di tipi di valore

La necessità di tipi di valore emerge abbastanza spesso. Questi sono casi in cui vorremmo sovrascrivere il comportamento predefinito della classe Object originale .

Come già sappiamo, l'implementazione predefinita della classe Object considera due oggetti uguali quando hanno la stessa identità, tuttavia per i nostri scopi consideriamo due oggetti uguali quando hanno lo stesso stato interno .

Supponendo di voler creare un oggetto denaro come segue:

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 }

Possiamo eseguire il seguente test su di esso per verificarne l'uguaglianza:

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

Notare la semantica del test.

Lo consideriamo passato quando i due oggetti denaro non sono uguali. Questo perché non abbiamo sovrascritto il metodo uguale , quindi l'uguaglianza viene misurata confrontando i riferimenti di memoria degli oggetti, che ovviamente non saranno diversi perché sono oggetti diversi che occupano posizioni di memoria diverse.

Ogni oggetto rappresenta 10.000 USD, ma Java ci dice che i nostri oggetti denaro non sono uguali . Vogliamo che i due oggetti risultino disuguali solo quando gli importi in valuta sono diversi o i tipi di valuta sono diversi.

Ora creiamo un oggetto di valore equivalente e questa volta lasceremo che l'IDE generi la maggior parte del codice:

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

L'unica differenza è che abbiamo sovrascritto i metodi equals (Object) e hashCode () , ora abbiamo il controllo su come vogliamo che Java confronti i nostri oggetti denaro. Eseguiamo il suo test equivalente:

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

Notare la semantica di questo test, ci aspettiamo che passi quando entrambi gli oggetti denaro risultano uguali tramite il metodo uguale .

4. Perché AutoValue?

Ora che abbiamo compreso a fondo i tipi di valore e perché ne abbiamo bisogno, possiamo esaminare AutoValue e come entra nell'equazione.

4.1. Problemi con la codifica manuale

Quando creiamo tipi di valore come abbiamo fatto nella sezione precedente, ci imbatteremo in una serie di problemi relativi a una cattiva progettazione e molto codice standard .

Una classe a due campi avrà 9 righe di codice: una per la dichiarazione del pacchetto, due per la firma della classe e la sua parentesi graffa di chiusura, due per le dichiarazioni dei campi, due per i costruttori e la sua parentesi graffa di chiusura e due per l'inizializzazione dei campi, ma poi abbiamo bisogno di getter per i campi, ciascuno prendendo altre tre righe di codice, creando sei righe extra.

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.

L'implementazione completa di tutti questi esempi e frammenti di codice può essere trovata nel progetto AutoValue GitHub.