1. Información general
AutoValue es un generador de código fuente para Java y, más específicamente, es una biblioteca para generar código fuente para objetos de valor u objetos de tipo valor .
Para generar un objeto de tipo valor, todo lo que tiene que hacer es anotar una clase abstracta con la anotación @AutoValue y compilar su clase. Lo que se genera es un objeto de valor con métodos de acceso, constructor parametrizado, métodos toString (), equals (Object) y hashCode () correctamente anulados .
El siguiente fragmento de código es un ejemplo rápido de una clase abstracta que cuando se compila dará como resultado un objeto de valor llamado 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(); }
Continuemos y descubramos más sobre los objetos de valor, por qué los necesitamos y cómo AutoValue puede ayudar a que la tarea de generar y refactorizar código consuma mucho menos tiempo.
2. Configuración de Maven
Para usar AutoValue en proyectos de Maven, debe incluir la siguiente dependencia en pom.xml :
com.google.auto.value auto-value 1.2
La última versión se puede encontrar siguiendo este enlace.
3. Objetos de tipo valor
Los tipos de valor son el producto final de la biblioteca, por lo que para apreciar su lugar en nuestras tareas de desarrollo, debemos comprender a fondo los tipos de valor, qué son, qué no son y por qué los necesitamos.
3.1. ¿Qué son los tipos de valor?
Los objetos de tipo valor son objetos cuya igualdad entre sí no está determinada por la identidad, sino por su estado interno. Esto significa que dos instancias de un objeto con tipo de valor se consideran iguales siempre que tengan valores de campo iguales.
Normalmente, los tipos de valor son inmutables . Sus campos deben estar hecha definitiva y no deben tener setter métodos como esto hará que se puede cambiar después de la creación de instancias.
Deben consumir todos los valores de campo mediante un constructor o un método de fábrica.
Los tipos de valor no son JavaBeans porque no tienen un constructor de argumento predeterminado o cero y tampoco tienen métodos de establecimiento, de manera similar, no son Objetos de transferencia de datos ni Objetos Java antiguos simples .
Además, una clase con tipo de valor debe ser final, de modo que no se puedan ampliar, al menos que alguien anule los métodos. Los JavaBeans, DTO y POJO no necesitan ser definitivos.
3.2. Crear un tipo de valor
Suponiendo que queremos crear un tipo de valor llamado Foo con campos llamados texto y número. ¿Cómo lo haríamos?
Haríamos una clase final y marcaríamos todos sus campos como definitivos. Luego usaríamos el IDE para generar el constructor, el método hashCode () , el método equals (Object) , los getters como métodos obligatorios y un método toString () , y tendríamos una clase así:
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; } }
Después de crear una instancia de Foo , esperamos que su estado interno permanezca igual durante todo su ciclo de vida.
Como veremos en la siguiente subsección, el código hash de un objeto debe cambiar de una instancia a otra , pero para los tipos de valor, tenemos que vincularlo a los campos que definen el estado interno del objeto de valor.
Por lo tanto, incluso cambiar un campo del mismo objeto cambiaría el valor hashCode .
3.3. Cómo funcionan los tipos de valor
La razón por la que los tipos de valor deben ser inmutables es para evitar cualquier cambio en su estado interno por parte de la aplicación después de haber sido instanciados.
Siempre que queramos comparar dos objetos de tipo valor, debemos, por lo tanto, usar el método equals (Object) de la clase Object .
Esto significa que siempre debemos anular este método en nuestros propios tipos de valor y solo devolver verdadero si los campos de los objetos de valor que estamos comparando tienen valores iguales.
Además, para que podamos usar nuestros objetos de valor en colecciones basadas en hash como HashSet sy HashMap s sin romper, debemos implementar correctamente el método hashCode () .
3.4. Por qué necesitamos tipos de valor
La necesidad de tipos de valor surge con bastante frecuencia. Estos son casos en los que nos gustaría anular el comportamiento predeterminado de la clase Object original .
Como ya sabemos, la implementación predeterminada de la clase Object considera dos objetos iguales cuando tienen la misma identidad, sin embargo, para nuestros propósitos, consideramos dos objetos iguales cuando tienen el mismo estado interno .
Suponiendo que nos gustaría crear un objeto de dinero de la siguiente manera:
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 }
Podemos ejecutar la siguiente prueba para probar su igualdad:
@Test public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() { MutableMoney m1 = new MutableMoney(10000, "USD"); MutableMoney m2 = new MutableMoney(10000, "USD"); assertFalse(m1.equals(m2)); }
Observe la semántica de la prueba.
Consideramos que ha pasado cuando los dos objetos monetarios no son iguales. Esto se debe a que no hemos anulado el método equals , por lo que la igualdad se mide comparando las referencias de memoria de los objetos, que por supuesto no van a ser diferentes porque son objetos diferentes que ocupan diferentes ubicaciones de memoria.
Cada objeto representa 10,000 USD, pero Java nos dice que nuestros objetos de dinero no son iguales . Queremos que los dos objetos prueben que son desiguales solo cuando las cantidades de moneda son diferentes o los tipos de moneda son diferentes.
Ahora creemos un objeto de valor equivalente y esta vez dejaremos que el IDE genere la mayor parte del código:
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; } }
La única diferencia es que anulamos los métodos equals (Object) y hashCode () , ahora tenemos control sobre cómo queremos que Java compare nuestros objetos de dinero. Ejecutemos su prueba equivalente:
@Test public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() { ImmutableMoney m1 = new ImmutableMoney(10000, "USD"); ImmutableMoney m2 = new ImmutableMoney(10000, "USD"); assertTrue(m1.equals(m2)); }
Observe la semántica de esta prueba, esperamos que pase cuando ambos objetos de dinero prueben iguales a través del método de iguales .
4. ¿Por qué AutoValue?
Ahora que entendemos completamente los tipos de valor y por qué los necesitamos, podemos ver AutoValue y cómo entra en la ecuación.
4.1. Problemas con la codificación manual
Cuando creamos tipos de valor como lo hicimos en la sección anterior, nos encontraremos con una serie de problemas relacionados con un mal diseño y una gran cantidad de código repetitivo .
Una clase de dos campos tendrá 9 líneas de código: una para la declaración del paquete, dos para la firma de la clase y su llave de cierre, dos para las declaraciones de campo, dos para los constructores y su llave de cierre y dos para inicializar los campos, pero luego necesitamos getters para los campos, cada uno toma tres líneas más de código, lo que hace seis líneas adicionales.
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.
La implementación completa de todos estos ejemplos y fragmentos de código se puede encontrar en el proyecto AutoValue GitHub.