Immutables to wszechstronne narzędzie do pracy z klasami typu: value object. Biblioteka pozwala uniknąć pisania oraz utrzymywania powtarzalnego i niewiele wnoszącego kodu (boilerplate code). Jej głównym celem jest realizacja reguły DRY (ang. Don’t Repeat Yourself, pol. Nie powtarzaj się), co naprawdę całkiem fajnie zrealizuje.
Spis treści
- 1 Wprowadzenie do Immutables
- 1.1 Wnioski na podstawie wygenerowanego kodu
- 1.2 Walidacja oraz normalizacja danych
- 1.3 Leniwe dociąganie danych
- 1.4 Własna implementacja metod: hashCode(), equals() oraz toString()
- 1.5 Etapowy builder
- 1.6 Serializacja
- 1.7 Dodatkowa konfiguracja generowanej klasy
- 1.8 Modyfikowalne obiekty
- 1.9 Programowanie funkcyjne
- 2 Wnioski
- 3 20+ BONUSOWYCH materiałów z programowania
Wprowadzenie do Immutables
Immutables działa jako standardowy procesor adnotacji w obrębie kompilatora javac. W celu wygenerowanie klasy Immutables wystarczy dodać adnotację: @Value.Immutable. Na podstawie informacji z bazowej klasy procesor adnotacji podczas kompilacji utworzy pełną implementację nowej klasy o nazwie: Immutable + [nazwa klasy bazowej].
ZOBACZ : Immutable – niezmienne obiekty
@Value.Immutable public abstract class UserValueImmutable { abstract String getName(); abstract Integer getAge(); }
UserValueImmutable user = ImmutableUserValueImmutable.builder() .name("Tomasz") .age(100) .build();
//-no-import-rewrite package UserValue; import java.lang.Object; import java.lang.String; import java.lang.Float; import java.lang.Double; /** * Immutable implementation of {@link UserValueImmutable}. * * Use the builder to create immutable instances: * {@code ImmutableUserValueImmutable.builder()}. */ @SuppressWarnings({"all"}) @javax.annotation.Generated({"Immutables.generator", "UserValueImmutable"}) public final class ImmutableUserValueImmutable extends UserValue.UserValueImmutable { private final java.lang.String name; private final java.lang.Integer age; private ImmutableUserValueImmutable(java.lang.String name, java.lang.Integer age) { this.name = name; this.age = age; } /** * @return The value of the {@code name} attribute */ @Override java.lang.String getName() { return name; } /** * @return The value of the {@code age} attribute */ @Override java.lang.Integer getAge() { return age; } /** * Copy the current immutable object by setting a value for the {@link UserValueImmutable#getName() name} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for name * @return A modified copy of the {@code this} object */ public final ImmutableUserValueImmutable withName(java.lang.String value) { if (this.name.equals(value)) return this; java.lang.String newValue = java.util.Objects.requireNonNull(value, "name"); return new ImmutableUserValueImmutable(newValue, this.age); } /** * Copy the current immutable object by setting a value for the {@link UserValueImmutable#getAge() age} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for age * @return A modified copy of the {@code this} object */ public final ImmutableUserValueImmutable withAge(java.lang.Integer value) { if (this.age.equals(value)) return this; java.lang.Integer newValue = java.util.Objects.requireNonNull(value, "age"); return new ImmutableUserValueImmutable(this.name, newValue); } /** * This instance is equal to all instances of {@code ImmutableUserValueImmutable} that have equal attribute values. * @return {@code true} if {@code this} is equal to {@code another} instance */ @Override public boolean equals(Object another) { if (this == another) return true; return another instanceof ImmutableUserValueImmutable &amp;amp;amp;amp;&amp;amp;amp;amp; equalTo((ImmutableUserValueImmutable) another); } private boolean equalTo(ImmutableUserValueImmutable another) { return name.equals(another.name) &amp;amp;amp;amp;&amp;amp;amp;amp; age.equals(another.age); } /** * Computes a hash code from attributes: {@code name}, {@code age}. * @return hashCode value */ @Override public int hashCode() { int h = 31; h = h * 17 + name.hashCode(); h = h * 17 + age.hashCode(); return h; } /** * Prints the immutable value {@code UserValueImmutable} with attribute values. * @return A string representation of the value */ @Override public String toString() { return "UserValueImmutable{" + "name=" + name + ", age=" + age + "}"; } /** * Creates an immutable copy of a {@link UserValueImmutable} value. * Uses accessors to get values to initialize the new immutable instance. * If an instance is already immutable, it is returned as is. * @param instance The instance to copy * @return A copied immutable UserValueImmutable instance */ public static ImmutableUserValueImmutable copyOf(UserValueImmutable instance) { if (instance instanceof ImmutableUserValueImmutable) { return (ImmutableUserValueImmutable) instance; } return ImmutableUserValueImmutable.builder() .from(instance) .build(); } /** * Creates a builder for {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * @return A new ImmutableUserValueImmutable builder */ public static ImmutableUserValueImmutable.Builder builder() { return new ImmutableUserValueImmutable.Builder(); } /** * Builds instances of type {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * Initialize attributes and then invoke the {@link #build()} method to create an * immutable instance. * ;{@code Builder} is not thread-safe and generally should not be stored in a field or collection, * but instead used immediately to create instances.&amp;amp;amp;lt;/em&amp;amp;amp;gt; */ public static final class Builder { private static final long INIT_BIT_NAME = 0x1L; private static final long INIT_BIT_AGE = 0x2L; private long initBits = 0x3L; private java.lang.String name; private java.lang.Integer age; private Builder() { } /** * Fill a builder with attribute values from the provided {@code UserValueImmutable} instance. * Regular attribute values will be replaced with those from the given instance. * Absent optional values will not replace present values. * @param instance The instance from which to copy values * @return {@code this} builder for use in a chained invocation */ public final Builder from(UserValueImmutable instance) { java.util.Objects.requireNonNull(instance, "instance"); name(instance.getName()); age(instance.getAge()); return this; } /** * Initializes the value for the {@link UserValueImmutable#getName() name} attribute. * @param name The value for name * @return {@code this} builder for use in a chained invocation */ public final Builder name(java.lang.String name) { this.name = java.util.Objects.requireNonNull(name, "name"); initBits &amp;amp;amp;amp;= ~INIT_BIT_NAME; return this; } /** * Initializes the value for the {@link UserValueImmutable#getAge() age} attribute. * @param age The value for age * @return {@code this} builder for use in a chained invocation */ public final Builder age(java.lang.Integer age) { this.age = java.util.Objects.requireNonNull(age, "age"); initBits &amp;amp;amp;amp;= ~INIT_BIT_AGE; return this; } /** * Builds a new {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * @return An immutable instance of UserValueImmutable * @throws java.lang.IllegalStateException if any required attributes are missing */ public ImmutableUserValueImmutable build() { if (initBits != 0) { throw new java.lang.IllegalStateException(formatRequiredAttributesMessage()); } return new ImmutableUserValueImmutable(name, age); } private String formatRequiredAttributesMessage() { java.util.List<String> attributes = new java.util.ArrayList<>(); if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name"); if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age"); return "Cannot build UserValueImmutable, some of required attributes are not set " + attributes; } } }
Immutables poza abstrakcyjnymi klasami obsługuje również zwykłe klasy, interfejsy i adnotacje. Zasada działania pozostaje jednak prawie taka sama.
Wnioski na podstawie wygenerowanego kodu
- kompilator automatycznie dodał nową klasę, która dziedziczy po klasie bazowej napisanej przez użytkownika. Dzięki takiemu podejściu z klasy bazowej można korzystać jak z publicznego API, a cała reszta aplikacji może być nieświadoma, że klasa została wygenerowana z wykorzystaniem biblioteki Immutables.
- biblioteka nie narzuca żadnych dodatkowych zależności podczas działania aplikacji. Cały potrzebny kod jest generowany już podczas kompilacji
- kompilator sam utworzył pola klasy na podstawie abstrakcyjnych getterów
- w powstałym kodzie nie ma setterów. Ponieważ wszystkie pola są oznaczona jako final nie można też dodać ich w klasie bazowej. Dzięki temu powstałe obiekty są niemodyfikowalne (immutables)
- klasa ma wygenerowane metody equals, hashCode oraz toString uwzględniające wszystkie pola
- nowa klasa posiada prywatny konstruktor, dlatego jedynym sposobem na jej utworzenie jest wykorzystanie również przygotowanego buildera
- adnotacje z abstrakcyjnych getterów są przekopiowane do ich implementacji
- kod zawiera wygenerowane metody kopiujące. Są to metody w formacie: ’with’ + [nazwa pola]. Za ich pomocą można otrzymać nowy obiekt immutable ze zmodyfikowanym tylko jednym polem
Walidacja oraz normalizacja danych
Dzięki wbudowanemu mechanizmowi można bardzo łatwo zaimplementować walidację oraz normalizację danych. Wystarczy w klasie bazowej dodać nieprywatną metodę z adnotacją: @Value.Check.
Metody zwracające void mogą obsługiwać tylko walidację.
@Value.Immutable public abstract class UserValueCheck { public abstract String getName(); public abstract Integer getAge(); @Value.Check protected UserValueCheck validateAndNormalize() { if (getAge() < 0) { throw new IllegalStateException("Age variable must be positive."); } if (!getName().toUpperCase().equals(getName())) { return ImmutableUserValueCheck.builder() .from(this) .name(getName().toUpperCase()) .build(); } return this; } }
Można jednak rozszerzyć metodę o zwracanie walidowanego obiektu i przeprowadzić jego normalizację, tak jak zostało to zrobione w przykładzie. Trzeba jednak uważać by zawsze na końcu zwracany był obiekt this. W przeciwnym wypadku walidacja będzie odpalana bez końca i doprowadzi to do przepełnienia stosu.
Leniwe dociąganie danych
Kolejna ciekawą funkcjonalnością wprowadzoną przez framework jest leniwe ładowanie, czyli lazy loading. Dla wybranej metody wystarczy dodać adnotację: @Value.Lazy, żeby biblioteka wygenerowała resztę kodu.
Wynik takiej operacji zostanie wyliczony tylko raz i będzie przechowywany w lokalnej zmiennej.
@Value.Immutable(builder = false) public class LazyLoadingExample { @Value.Lazy public String longRunningMethod() { try { Thread.sleep(1000); } catch (InterruptedException ex) { throw new RuntimeException(ex); } return "" + System.currentTimeMillis(); } }
Powstały kod jest napisane z uwzględnieniem potencjalnych kłopotów z wyścigami wątków.
Atrybuty oznaczone w ten sposób nie są już uwzględniane w implementacjach metod equals oraz hashCode.
public String longRunningMethod() { if ((lazyInitBitmap & LONG_RUNNING_METHOD_LAZY_INIT_BIT) == 0) { synchronized (this) { if ((lazyInitBitmap & LONG_RUNNING_METHOD_LAZY_INIT_BIT) == 0) { this.longRunningMethod = Preconditions.checkNotNull(super.longRunningMethod(), "longRunningMethod"); lazyInitBitmap |= LONG_RUNNING_METHOD_LAZY_INIT_BIT; } } } return longRunningMethod; }
Własna implementacja metod: hashCode(), equals() oraz toString()
W celu nadpisania powyższych metod wystarczy zaimplementować je w bazowej klasie użytkownika. Kompilator sam wtedy rozpozna, żeby nie nadpisywać ich implementacji.
Jednak zanim nadpisze się te metody, warto zastanowić się, czy na pewno jest to konieczne. W wielu wypadkach wystarczy wykluczyć niektóre pola z implementacji tych metod i nie bawić się w ręczne ich pisanie. Do tego celu służy adnotacja: @Value.Auxiliary, którą trzeba dodać na niechcianych polach (getterach, na podstawie których zostaną wygenerowane te pola).
Etapowy builder
Biblioteka daje możliwość zaimplementowania tak zwanego staged builder’a. Dla klasy Immutables trzeba dodać adnotację: @Value.Style(stagedBuilder = true). Pomysł polega na tym, że podczas tworzenia obiektu przy pomocy buildera już na poziomie kompilacji kodu wymuszona jest kolejność podawania wymaganych parametrów obiektu.
Funkcjonalność jest o tyle ciekawa, że nie trzeba nawet uruchamiać aplikacji, żeby zweryfikować czy wszystkie wymagane argumenty zostały podane.
//-no-import-rewrite package UserValue; import java.lang.Object; import java.lang.String; import java.lang.Float; import java.lang.Double; /** * Immutable implementation of {@link UserValueImmutable}. * * Use the builder to create immutable instances: * {@code ImmutableUserValueImmutable.builder()}. */ @SuppressWarnings({"all"}) @javax.annotation.Generated({"Immutables.generator", "UserValueImmutable"}) public final class ImmutableUserValueImmutable extends UserValue.UserValueImmutable { private final java.lang.String name; private final java.lang.Integer age; private ImmutableUserValueImmutable(java.lang.String name, java.lang.Integer age) { this.name = name; this.age = age; } /** * @return The value of the {@code name} attribute */ @Override java.lang.String getName() { return name; } /** * @return The value of the {@code age} attribute */ @Override java.lang.Integer getAge() { return age; } /** * Copy the current immutable object by setting a value for the {@link UserValueImmutable#getName() name} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for name * @return A modified copy of the {@code this} object */ public final ImmutableUserValueImmutable withName(java.lang.String value) { if (this.name.equals(value)) return this; java.lang.String newValue = java.util.Objects.requireNonNull(value, "name"); return new ImmutableUserValueImmutable(newValue, this.age); } /** * Copy the current immutable object by setting a value for the {@link UserValueImmutable#getAge() age} attribute. * An equals check used to prevent copying of the same value by returning {@code this}. * @param value A new value for age * @return A modified copy of the {@code this} object */ public final ImmutableUserValueImmutable withAge(java.lang.Integer value) { if (this.age.equals(value)) return this; java.lang.Integer newValue = java.util.Objects.requireNonNull(value, "age"); return new ImmutableUserValueImmutable(this.name, newValue); } /** * This instance is equal to all instances of {@code ImmutableUserValueImmutable} that have equal attribute values. * @return {@code true} if {@code this} is equal to {@code another} instance */ @Override public boolean equals(Object another) { if (this == another) return true; return another instanceof ImmutableUserValueImmutable &amp;amp;amp;amp;&amp;amp;amp;amp; equalTo((ImmutableUserValueImmutable) another); } private boolean equalTo(ImmutableUserValueImmutable another) { return name.equals(another.name) &amp;amp;amp;amp;&amp;amp;amp;amp; age.equals(another.age); } /** * Computes a hash code from attributes: {@code name}, {@code age}. * @return hashCode value */ @Override public int hashCode() { int h = 31; h = h * 17 + name.hashCode(); h = h * 17 + age.hashCode(); return h; } /** * Prints the immutable value {@code UserValueImmutable} with attribute values. * @return A string representation of the value */ @Override public String toString() { return "UserValueImmutable{" + "name=" + name + ", age=" + age + "}"; } /** * Creates an immutable copy of a {@link UserValueImmutable} value. * Uses accessors to get values to initialize the new immutable instance. * If an instance is already immutable, it is returned as is. * @param instance The instance to copy * @return A copied immutable UserValueImmutable instance */ public static ImmutableUserValueImmutable copyOf(UserValueImmutable instance) { if (instance instanceof ImmutableUserValueImmutable) { return (ImmutableUserValueImmutable) instance; } return ImmutableUserValueImmutable.builder() .from(instance) .build(); } /** * Creates a builder for {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * @return A new ImmutableUserValueImmutable builder */ public static ImmutableUserValueImmutable.Builder builder() { return new ImmutableUserValueImmutable.Builder(); } /** * Builds instances of type {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * Initialize attributes and then invoke the {@link #build()} method to create an * immutable instance. * ;{@code Builder} is not thread-safe and generally should not be stored in a field or collection, * but instead used immediately to create instances.&amp;amp;amp;lt;/em&amp;amp;amp;gt; */ public static final class Builder { private static final long INIT_BIT_NAME = 0x1L; private static final long INIT_BIT_AGE = 0x2L; private long initBits = 0x3L; private java.lang.String name; private java.lang.Integer age; private Builder() { } /** * Fill a builder with attribute values from the provided {@code UserValueImmutable} instance. * Regular attribute values will be replaced with those from the given instance. * Absent optional values will not replace present values. * @param instance The instance from which to copy values * @return {@code this} builder for use in a chained invocation */ public final Builder from(UserValueImmutable instance) { java.util.Objects.requireNonNull(instance, "instance"); name(instance.getName()); age(instance.getAge()); return this; } /** * Initializes the value for the {@link UserValueImmutable#getName() name} attribute. * @param name The value for name * @return {@code this} builder for use in a chained invocation */ public final Builder name(java.lang.String name) { this.name = java.util.Objects.requireNonNull(name, "name"); initBits &amp;amp;amp;amp;= ~INIT_BIT_NAME; return this; } /** * Initializes the value for the {@link UserValueImmutable#getAge() age} attribute. * @param age The value for age * @return {@code this} builder for use in a chained invocation */ public final Builder age(java.lang.Integer age) { this.age = java.util.Objects.requireNonNull(age, "age"); initBits &amp;amp;amp;amp;= ~INIT_BIT_AGE; return this; } /** * Builds a new {@link ImmutableUserValueImmutable ImmutableUserValueImmutable}. * @return An immutable instance of UserValueImmutable * @throws java.lang.IllegalStateException if any required attributes are missing */ public ImmutableUserValueImmutable build() { if (initBits != 0) { throw new java.lang.IllegalStateException(formatRequiredAttributesMessage()); } return new ImmutableUserValueImmutable(name, age); } private String formatRequiredAttributesMessage() { java.util.List<String> attributes = new java.util.ArrayList<>(); if ((initBits & INIT_BIT_NAME) != 0) attributes.add("name"); if ((initBits & INIT_BIT_AGE) != 0) attributes.add("age"); return "Cannot build UserValueImmutable, some of required attributes are not set " + attributes; } } }
Minusem tego rozwiązania jest generowania przez implementację Immutables dodatkowych interfejsów oraz bardziej skomplikowanego kodu. Może to przełożyć się na odrobinę dłuższy czas budowania projektu oraz większe zużycie pamięci na wygenerowane klasy.
Serializacja
Immutables wspiera również standardową serializację Javy. Klasę należy oznaczyć adnotacją: @Serial.Version(1), gdzie 1 jest wersją klasy.
Klasa użytkownika nie musi implementować interfejsu Serializable, ponieważ zostanie on automatycznie dodany do klasy Immutables.
Dodatkowa konfiguracja generowanej klasy
W celu dopasowania generowanej implementacji do swoich potrzeb można posłużyć się opcjonalnymi parametrami adnotacji @Value.Immutable.
public @interface Immutable { public boolean singleton() default false; public boolean intern() default false; public boolean copy() default true; public boolean prehash() default false; public boolean builder() default true; }
Dzięki tym parametrom można:
- singleton – oznaczyć klasę jako singleton. Warto jednocześnie wyłączyć opcję generowania buildera (builder=false). Wtedy obiekt singletonu będzie dostępny z metody: UserSingleton userSingleton = ImmutableUserSingleton.of();
- intern – parametr optymalizacyjny. Wprowadza wewnętrzną pulę obiektów danego typu. Dzięki tej opcji obiekty o tych samych wartościach atrybutów będą przechowywane w tym samym miejscu w pamięci
- copy – generowanie metod kopiujących. Dla przykładu użytkownika będą to metody withName i withAge
- prehash – kolejna opcja optymalizacyjna. Dzięki niej hashe dla metody hashCode są wyliczane tylko raz
- builder – generowanie klasy buildera
Modyfikowalne obiekty
Mimo iż nazwa biblioteki na to nie wskazuje, posiada ona również wsparcie dla obiektów modyfikowalnych. Nie są to funkcjonalności tak mocno rozbudowane, jak te z głównego nurtu biblioteki, ale mimo to stanową ciekawą alternatywą np. dla generowanie getterów i setterów przy pomocy Lomboka.
@Value.Modifiable public abstract class UserModifiable { abstract String getName(); abstract Integer getAge(); }
Podobnie jak w przypadku @Value.Immutables, na podstawie klasy użytkownika zostanie wygenerowana nowa klasa. W tym wypadku będzie to:
package pl.stormit.immutables; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.annotation.Generated; /** * A modifiable implementation of the {@link UserModifiable UserModifiable} type. * Use the {@link #create()} static factory methods to create new instances. * Use the {@link #toImmutable()} method to convert to canonical immutable instances. * <em>ModifiableUserModifiable is not thread-safe</em> * @see ImmutableUserModifiable */ @SuppressWarnings({"all"}) @Generated({"Modifiables.generator", "UserModifiable"}) public final class ModifiableUserModifiable extends UserModifiable { private static final long INIT_BIT_NAME = 0x1L; private static final long INIT_BIT_AGE = 0x2L; private long initBits = 0x3L; private String name; private Integer age; private ModifiableUserModifiable() {} /** * Construct a modifiable instance of {@code UserModifiable}. * @return A new modifiable instance */ public static ModifiableUserModifiable create() { return new ModifiableUserModifiable(); } /** * @return value of {@code name} attribute */ @Override final String getName() { if (!nameIsSet()) { checkRequiredAttributes(); } return name; } /** * @return value of {@code age} attribute */ @Override final Integer getAge() { if (!ageIsSet()) { checkRequiredAttributes(); } return age; } /** * Clears the object by setting all attributes to their initial values. * @return {@code this} for use in a chained invocation */ public ModifiableUserModifiable clear() { initBits = 0x3L; name = null; age = null; return this; } /** * Fill this modifiable instance with attribute values from the provided {@link UserModifiable} instance. * Regular attribute values will be overridden, i.e. replaced with ones of an instance. * Any of the instance's absent optional values will not be copied (will not override current values). * @param instance The instance from which to copy values * @return {@code this} for use in a chained invocation */ public ModifiableUserModifiable from(UserModifiable instance) { Objects.requireNonNull(instance, "instance"); setName(instance.getName()); setAge(instance.getAge()); return this; } /** * Assigns a value to the {@link UserModifiable#getName() name} attribute. * @param name The value for name * @return {@code this} for use in a chained invocation */ public ModifiableUserModifiable setName(String name) { this.name = Objects.requireNonNull(name, "name"); initBits &= ~INIT_BIT_NAME; return this; } /** * Assigns a value to the {@link UserModifiable#getAge() age} attribute. * @param age The value for age * @return {@code this} for use in a chained invocation */ public ModifiableUserModifiable setAge(Integer age) { this.age = Objects.requireNonNull(age, "age"); initBits &= ~INIT_BIT_AGE; return this; } /** * Returns {@code true} if the required attribute {@link UserModifiable#getName() name} is set. * @return {@code true} if set */ public final boolean nameIsSet() { return (initBits & INIT_BIT_NAME) == 0; } /** * Returns {@code true} if the required attribute {@link UserModifiable#getAge() age} is set. * @return {@code true} if set */ public final boolean ageIsSet() { return (initBits & INIT_BIT_AGE) == 0; } /** * Reset an attribute to its initial value. * @return {@code this} for use in a chained invocation */ public final ModifiableUserModifiable unsetName() { initBits |= INIT_BIT_NAME; name = null; return this; } /** * Reset an attribute to its initial value. * @return {@code this} for use in a chained invocation */ public final ModifiableUserModifiable unsetAge() { initBits |= INIT_BIT_AGE; age = null; return this; } /** * Returns {@code true} if all required attributes are set, indicating that the object is initialized. * @return {@code true} if set */ public final boolean isInitialized() { return initBits == 0; } private void checkRequiredAttributes() { if (!isInitialized()) { throw new IllegalStateException(formatRequiredAttributesMessage()); } } private String formatRequiredAttributesMessage() { List<String> attributes = new ArrayList<String>(); if (!nameIsSet()) attributes.add("name"); if (!ageIsSet()) attributes.add("age"); return "UserModifiable in not initialized, some of the required attributes are not set " + attributes; } /** * Converts to {@link ImmutableUserModifiable ImmutableUserModifiable}. * @return An immutable instance of UserModifiable */ public final ImmutableUserModifiable toImmutable() { checkRequiredAttributes(); return ImmutableUserModifiable.copyOf(this); } /** * This instance is equal to all instances of {@code ModifiableUserModifiable} that have equal attribute values. * An uninitialized instance is equal only to itself. * @return {@code true} if {@code this} is equal to {@code another} instance */ @Override public boolean equals(Object another) { if (this == another) return true; if (!(another instanceof ModifiableUserModifiable)) return false; ModifiableUserModifiable other = (ModifiableUserModifiable) another; if (!isInitialized() || !other.isInitialized()) { return false; } return equalTo(other); } private boolean equalTo(ModifiableUserModifiable another) { return name.equals(another.name) && age.equals(another.age); } /** * Computes a hash code from attributes: {@code name}, {@code age}. * @return hashCode value */ @Override public int hashCode() { int h = 31; h = h * 17 + name.hashCode(); h = h * 17 + age.hashCode(); return h; } /** * Generates a string representation of this {@code UserModifiable}. * If uninitialized, some attribute values may appear as question marks. * @return A string representation */ @Override public String toString() { return "ModifiableUserModifiable{" + "name=" + (nameIsSet() ? getName() : "?") + ", age=" + (ageIsSet() ? getAge() : "?") + "}"; } }
Wygenerowana klasa zawiera:
- pola utworzone na podstawie abstrakcyjnych getterów
- gettery
- settery
- metody equals oraz hashCode
- metodę toString
Ciekawostką jest to, że klasa może zawierać jednocześnie dwie adnotacje: @Value.Modifiable oraz @Value.Immutable. Dla tej klasy zostaną wtedy wygenerowane dwie implementacje: ImmutableUserModifiable oraz ModifiableUserModifiable.
Programowanie funkcyjne
Biblioteka posiada również wsparcie dla programowania funkcyjnego. Funkcjonalności dostępne są w ramach osobnego modułu org.immutables:func.
@Value.Immutable @Functional abstract class UserFunctional { public abstract String getName(); public abstract Integer getAge(); }
ImmutableUserFunctional user = ImmutableUserFunctional.builder() .name("Tomasz") .age(100).build(); List<UserFunctional> users = Arrays.asList(user.withAge(1), user.withAge(2), user.withAge(3), user.withAge(4)); List<Integer> ages = Lists.transform(users, UserFunctionalFunctions.getAge()); assertEquals(Arrays.asList(1, 2, 3, 4), ages);
Wnioski
Biblioteka bazuje na bardzo podobnych założeniach jak opisywana wcześniej AutoValue. Dlatego bardzo wiele wniosków oraz funkcjonalności pokrywa się dla tych bibliotek.
Rozwiązania tego typu rozszerzają standardowe funkcjonalności oferowane przez Javę. Dzięki temu programista może poświęcić więcej swojego czasu i wysiłku na obsługę problemów typowo biznesowych, zamiast wymyślać koło na nowo.
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!