Inleiding tot AutoValue

1. Overzicht

AutoValue is een broncodegenerator voor Java, en meer specifiek is het een bibliotheek voor broncode genereren voor waardeobjecten of waarde-getypeerde objecten.

Om een ​​waardetype-object te genereren, hoeft u alleen maar te doen annoteer een abstracte klasse met de @AutoValue annotatie en stel je klas samen. Wat wordt gegenereerd is een waardeobject met accessormethoden, geparametriseerde constructor, correct overschreven toString (), is gelijk aan (Object) en hashCode () methoden.

Het volgende codefragment is een snel voorbeeld van een abstracte klasse die, wanneer gecompileerd, zal resulteren in een waardeobject met de naam AutoValue_Person.

@AutoValue abstracte klasse Persoon {statische Persoon create (String naam, int leeftijd) {retourneer nieuwe AutoValue_Person (naam, leeftijd); } abstract String naam (); abstract int age (); } 

Laten we doorgaan en meer te weten komen over waardeobjecten, waarom we ze nodig hebben en hoe AutoValue kan helpen om het genereren en herstructureren van code veel minder tijdrovend te maken.

2. Maven-instellingen

Om AutoValue in een Maven-project te gebruiken, moet u de volgende afhankelijkheid opnemen in het pom.xml:

 com.google.auto.value auto-waarde 1.2 

De laatste versie is te vinden via deze link.

3. Waarde-getypeerde objecten

Waardetypes zijn het eindproduct van een bibliotheek, dus om zijn plaats in onze ontwikkelingstaken te waarderen, moeten we waardetypes grondig begrijpen, wat ze zijn, wat ze niet zijn en waarom we ze nodig hebben.

3.1. Wat zijn waardetypes?

Waardetype-objecten zijn objecten waarvan de gelijkheid met elkaar niet wordt bepaald door identiteit, maar eerder door hun interne toestand. Dit betekent dat twee instanties van een object met een waarde-type als gelijk worden beschouwd, zolang ze maar gelijke veldwaarden hebben.

Waardetypes zijn doorgaans onveranderlijk. Hun velden moeten worden gemaakt laatste en ze mogen niet hebben setter methoden, aangezien dit ze na instantiatie veranderbaar maakt.

Ze moeten alle veldwaarden consumeren via een constructor- of fabrieksmethode.

Waardetypes zijn geen JavaBeans omdat ze geen standaard- of nul-argumentconstructor hebben en evenmin hebben ze setter-methoden, op dezelfde manier, het zijn geen gegevensoverdrachtobjecten of gewone oude Java-objecten.

Bovendien moet een klasse met een waardetype definitief zijn, zodat ze niet uitbreidbaar zijn, tenminste dat iemand de methoden overschrijft. JavaBeans, DTO's en POJO's hoeven niet definitief te zijn.

3.2. Een waardetype creëren

Ervan uitgaande dat we een waardetype willen creëren met de naam Foo met velden genaamd tekst en aantal. Hoe zouden we dat aanpakken?

We zouden een laatste klas maken en al zijn velden als definitief markeren. Vervolgens zouden we de IDE gebruiken om de constructor, de hashCode () methode, de is gelijk aan (Object) methode, de getters als verplichte methoden en een toString () methode, en we zouden een klasse als volgt hebben:

openbare laatste klasse Foo {privé finale String-tekst; privé definitief int nummer; openbare Foo (String-tekst, int-nummer) {this.text = text; this.number = nummer; } // standaard getters @Override public int hashCode () {return Objects.hash (tekst, nummer); } @Override public String toString () {return "Foo [text =" + text + ", number =" + number + "]"; } @Override public boolean equals (Object obj) {if (this == obj) retourneert true; if (obj == null) return false; if (getClass ()! = obj.getClass ()) return false; Foo other = (Foo) obj; if (nummer! = ander. nummer) return false; if (text == null) {if (other.text! = null) return false; } else if (! text.equals (other.text)) {return false; } retourneren waar; }}

Na het maken van een instantie van Fooverwachten we dat de interne toestand gedurende de hele levenscyclus hetzelfde zal blijven.

Zoals we zullen zien in de volgende paragraaf de hashCode van een object moet van instantie naar instantie veranderen, maar voor waardetypes moeten we het koppelen aan de velden die de interne toestand van het waardeobject definiëren.

Daarom zou zelfs het veranderen van een veld van hetzelfde object de hashCode waarde.

3.3. Hoe waardetypes werken

De reden dat waardetypen onveranderlijk moeten zijn, is om te voorkomen dat de toepassing hun interne status verandert nadat ze zijn geïnstantieerd.

Telkens wanneer we twee objecten met een waarde-type willen vergelijken, we moeten daarom de is gelijk aan (Object) methode van de Voorwerp klasse.

Dit betekent dat we deze methode altijd moeten overschrijven in onze eigen waardetypes en alleen true moeten retourneren als de velden van de waardeobjecten die we vergelijken gelijke waarden hebben.

Bovendien, voor ons om onze waardeobjecten te gebruiken in hash-gebaseerde collecties zoals HashSets en Hash kaarts zonder te breken, we moeten het hashCode () methode.

3.4. Waarom we waardetypes nodig hebben

De behoefte aan waardetypes komt vrij vaak naar voren. Dit zijn gevallen waarin we het standaardgedrag van het origineel willen opheffen Voorwerp klasse.

Zoals we al weten, is de standaardimplementatie van de Voorwerp class beschouwt twee objecten echter als gelijk als ze dezelfde identiteit hebben voor onze doeleinden beschouwen we twee objecten als gelijk wanneer ze dezelfde interne toestand hebben.

Ervan uitgaande dat we als volgt een geldobject willen creëren:

openbare klasse MutableMoney {privé lang bedrag; privé String-valuta; openbaar MutableMoney (lang bedrag, stringvaluta) {this.amount = bedrag; this.currency = valuta; } // standaard getters en setters}

We kunnen de volgende test uitvoeren om de gelijkheid te testen:

@Test openbare ongeldig gegevenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect () {MutableMoney m1 = nieuwe MutableMoney (10000, "USD"); MutableMoney m2 = nieuwe MutableMoney (10000, "USD"); assertFalse (m1.equals (m2)); }

Let op de semantiek van de test.

We beschouwen het als geslaagd als de twee geldobjecten niet gelijk zijn. Dit is zo omdat we hebben de is gelijk aan methode dus gelijkheid wordt gemeten door de geheugenreferenties van de objecten te vergelijken, die natuurlijk niet verschillend zullen zijn omdat het verschillende objecten zijn die verschillende geheugenlocaties innemen.

Elk object vertegenwoordigt 10.000 USD maar Java vertelt ons dat onze geldobjecten niet gelijk zijn. We willen dat de twee objecten alleen ongelijk testen als de valutabedragen verschillen of de valutatypen verschillen.

Laten we nu een object met een equivalente waarde maken en deze keer laten we de IDE de meeste code genereren:

openbare finale klasse ImmutableMoney {privé finale lang bedrag; private laatste String-valuta; openbaar ImmutableMoney (lang bedrag, String-valuta) {this.amount = bedrag; this.currency = valuta; } @Override public int hashCode () {final int prime = 31; int resultaat = 1; resultaat = prime * resultaat + (int) (bedrag ^ (bedrag >>> 32)); result = prime * resultaat + ((valuta == null)? 0: currency.hashCode ()); resultaat teruggeven; } @Override public boolean equals (Object obj) {if (this == obj) retourneert true; if (obj == null) return false; if (getClass ()! = obj.getClass ()) return false; ImmutableMoney other = (ImmutableMoney) obj; if (amount! = other.amount) false retourneren; if (currency == null) {if (other.currency! = null) return false; } else if (! currency.equals (other.currency)) retourneert false; terugkeer waar; }}

Het enige verschil is dat we de is gelijk aan (Object) en hashCode () methoden, nu hebben we controle over hoe we willen dat Java onze geldobjecten vergelijkt. Laten we de gelijkwaardige test uitvoeren:

@Test openbare ongeldig gegevenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect () {ImmutableMoney m1 = nieuw ImmutableMoney (10000, "USD"); ImmutableMoney m2 = nieuw ImmutableMoney (10000, "USD"); assertTrue (m1.equals (m2)); }

Let op de semantiek van deze test, we verwachten dat deze slaagt als beide geldobjecten gelijk testen via de is gelijk aan methode.

4. Waarom AutoValue?

Nu we waardetypes grondig begrijpen en waarom we ze nodig hebben, kunnen we kijken naar AutoValue en hoe het in de vergelijking komt.

4.1. Problemen met handcodering

Wanneer we waardetypes creëren zoals we in de vorige sectie hebben gedaan, zullen we een aantal problemen tegenkomen die verband houden met slecht ontwerp en veel boilerplate-code.

Een klasse met twee velden heeft 9 coderegels: één voor pakketdeclaratie, twee voor de handtekening van de klasse en de sluitende accolade, twee voor veldverklaringen, twee voor constructors en de sluitende accolade en twee voor het initialiseren van de velden, maar dan hebben we getters nodig voor de velden, elk met drie extra regels code, dus zes extra regels.

Het hashCode () en equalTo (Object) methoden vereisen respectievelijk ongeveer 9 regels en 18 regels en overschrijven de toString () methode voegt nog eens vijf regels toe.

Dat betekent dat een goed opgemaakte codebasis voor onze klasse met twee velden zou duren ongeveer 50 regels code.

4.2 IDE's te redden?

Dit is gemakkelijk met een IDE zoals Eclipse of IntilliJ en met slechts een of twee waardetypeklassen om te maken. Denk na over een groot aantal van dergelijke klassen om te maken, zou het nog steeds zo gemakkelijk zijn als de IDE ons helpt?

Snel vooruit, enkele maanden later, neem aan dat we onze code opnieuw moeten bekijken en onze Geld klassen en misschien de valuta veld uit de Draad typ naar een ander waardetype genaamd Valuta.

4.3 IDE's zijn niet echt zo nuttig

Een IDE zoals Eclipse kan niet simpelweg onze accessormethoden of de toString (), hashCode () of is gelijk aan (Object) methoden.

Deze refactoring zou met de hand moeten gebeuren. Het bewerken van code vergroot de kans op bugs en met elk nieuw veld voegen we toe aan het Geld klasse, het aantal regels exponentieel toeneemt.

Als we beseffen dat dit scenario zich voordoet, dat het vaak en in grote hoeveelheden gebeurt, zullen we de rol van AutoValue echt waarderen.

5. AutoValue-voorbeeld

Het probleem dat AutoValue oplost, is om alle standaardcode waar we het in de vorige sectie over hadden weg te nemen, zodat we het nooit hoeven te schrijven, bewerken of zelfs maar te lezen.

We zullen naar hetzelfde kijken Geld voorbeeld, maar deze keer met AutoValue. We zullen deze klas noemen AutoValueMoney omwille van de consistentie:

@AutoValue openbare abstracte klasse AutoValueMoney {openbare abstracte String getCurrency (); openbaar abstract lang getAmount (); openbare statische AutoValueMoney create (String-valuta, lang bedrag) {retourneer nieuwe AutoValue_AutoValueMoney (valuta, bedrag); }}

Wat er is gebeurd, is dat we een abstracte klasse schrijven, er abstracte accessors voor definiëren maar geen velden, we annoteren de klasse met @AutoValue allemaal in totaal slechts 8 regels code, en Javac genereert een concrete subklasse voor ons die er als volgt uitziet:

openbare laatste klasse AutoValue_AutoValueMoney breidt AutoValueMoney {privé laatste String-valuta uit; privé eindbedrag; AutoValue_AutoValueMoney (String-valuta, lang bedrag) {if (currency == null) gooi nieuwe NullPointerException (valuta); this.currency = valuta; this.amount = bedrag; } // standaard getters @Override public int hashCode () {int h = 1; h * = 100.0003; h ^ = valuta.hashCode (); h * = 100.0003; h ^ = bedrag; terug h; } @Override public boolean equals (Object o) {if (o == this) {return true; } if (o instantie van AutoValueMoney) {AutoValueMoney that = (AutoValueMoney) o; return (this.currency.equals (that.getCurrency ())) && (this.amount == that.getAmount ()); } return false; }}

We hebben helemaal nooit met deze klasse te maken, en we hoeven deze ook niet te bewerken als we meer velden moeten toevoegen of wijzigingen in onze velden moeten aanbrengen, zoals de valuta scenario in de vorige sectie.

Javac zal altijd bijgewerkte code voor ons opnieuw genereren.

Bij het gebruik van dit nieuwe waardetype, zien alle bellers alleen het bovenliggende type, zoals we zullen zien in de volgende unit-tests.

Hier is een test die controleert of onze velden correct worden ingesteld:

@Test openbare ongeldig gegevenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect () {AutoValueMoney m = AutoValueMoney.create ("USD", 10000); assertEquals (m.getAmount (), 10000); assertEquals (m.getCurrency (), "USD"); }

Een test om dat te verifiëren AutoValueMoney objecten met dezelfde valuta en hetzelfde bedrag test gelijk volgen:

@Test openbare ongeldig gegeven2EqualValueTypesWithAutoValue_whenEqual_thenCorrect () {AutoValueMoney m1 = AutoValueMoney.create ("USD", 5000); AutoValueMoney m2 = AutoValueMoney.create ("USD", 5000); assertTrue (m1.equals (m2)); }

Als we het valutatype van een geldobject veranderen in GBP, dan is de test: 5000 GBP == 5000 USD is niet langer waar:

@Test openbare ongeldig gegeven2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect () {AutoValueMoney m1 = AutoValueMoney.create ("GBP", 5000); AutoValueMoney m2 = AutoValueMoney.create ("USD", 5000); assertFalse (m1.equals (m2)); }

6. AutoWaarde met bouwers

Het eerste voorbeeld dat we hebben bekeken, betreft het basisgebruik van AutoValue met behulp van een statische fabrieksmethode als onze openbare creatie-API.

Merk op dat als al onze velden dat waren Snaren, het zou gemakkelijk zijn om ze uit te wisselen als we ze doorgaven aan de statische fabrieksmethode, zoals het plaatsen van de bedrag in plaats van valuta en vice versa.

Dit is vooral waarschijnlijk als we veel velden hebben en allemaal van Draad type. Dit probleem wordt verergerd door het feit dat met AutoValue, alle velden worden geïnitialiseerd via de constructor.

Om dit probleem op te lossen, moeten we de bouwer patroon. Gelukkig. dit kan worden gegenereerd door AutoValue.

Onze AutoValue-klasse verandert niet echt veel, behalve dat de statische fabrieksmethode wordt vervangen door een builder:

@AutoValue openbare abstracte klasse AutoValueMoneyWithBuilder {openbare abstracte String getCurrency (); openbaar abstract lang getAmount (); static Builder builder () {retourneer nieuwe AutoValue_AutoValueMoneyWithBuilder.Builder (); } @ AutoValue.Builder abstracte statische klasse Builder {abstracte Builder setCurrency (String-valuta); abstract Builder setAmount (lang bedrag); abstracte AutoValueMoneyWithBuilder build (); }}

De gegenereerde klasse is precies hetzelfde als de eerste, maar er wordt een concrete innerlijke klasse voor de builder gegenereerd en de abstracte methoden worden geïmplementeerd in de builder:

statische laatste klasse Builder breidt AutoValueMoneyWithBuilder.Builder {private String-valuta uit; lang privébedrag; Builder () {} Builder (AutoValueMoneyWithBuilder-bron) {this.currency = source.getCurrency (); this.amount = source.getAmount (); } @Override openbare AutoValueMoneyWithBuilder.Builder setCurrency (String-valuta) {this.currency = currency; dit teruggeven; } @Override openbare AutoValueMoneyWithBuilder.Builder setAmount (lang bedrag) {this.amount = bedrag; dit teruggeven; } @Override openbare AutoValueMoneyWithBuilder build () {String missing = ""; if (currency == null) {missing + = "currency"; } if (amount == 0) {missing + = "amount"; } if (! missing.isEmpty ()) {throw new IllegalStateException ("Ontbrekende vereiste eigenschappen:" + ontbreekt); } retourneer nieuwe AutoValue_AutoValueMoneyWithBuilder (this.currency, this.amount); }}

Merk ook op hoe de testresultaten niet veranderen.

Als we willen weten of de veldwaarden daadwerkelijk correct zijn ingesteld via de builder, kunnen we deze test uitvoeren:

@Test openbare ongeldig gegevenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect () {AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder (). setAmount (5000) .setCurrency ("USD"). build (); assertEquals (m.getAmount (), 5000); assertEquals (m.getCurrency (), "USD"); }

Om die gelijkheid te testen, hangt af van de interne toestand:

@Test openbare ongeldig gegeven2EqualValueTypesWithBuilder_whenEqual_thenCorrect () {AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder () .setAmount (5000) .setCurrency ("USD"). Build (); AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder () .setAmount (5000) .setCurrency ("USD"). Build (); assertTrue (m1.equals (m2)); }

En als de veldwaarden anders zijn:

@Test openbare ongeldig gegeven2DifferentValueTypesBuilder_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. Conclusie

In deze tutorial hebben we de meeste basisprincipes van Google's AutoValue-bibliotheek geïntroduceerd en hoe je deze kunt gebruiken om waardetypes te creëren met een heel klein beetje code van onze kant.

Een alternatief voor Google's AutoValue is het Lombok-project - u kunt hier het inleidende artikel over het gebruik van Lombok bekijken.

De volledige implementatie van al deze voorbeelden en codefragmenten is te vinden in het AutoValue GitHub-project.