Javaのorg.Immutablesの使い方について整理する。
公式サイトは以下。(基本的に本記事の内容は、これを参考にしている)
https://immutables.github.io/
事前設定
mavenのpom.xmlにdependencyを設定。
<dependency>
<groupId>org.immutables</groupId>
<artifactId>value</artifactId>
<version>2.8.2</version>
<scope>provided</scope>
</dependency>
基本的な使い方
データを格納するValueクラスを作成し、それを元に自動生成されたImmutableクラスをbuildすることで、Immutableオブジェクトを生成する。
変更を加えられたくないオブジェクトを生成する場合などに使用。
Value
- Immutableアノテーションで宣言する。
- interfaceでもclassでもOK。
- コンパイルされるとImmutableクラスが自動生成される。
- Immutableクラスのクラス名はImmutableXxx。(XxxはValueクラスのクラス名)
- 自動生成されたImmutableクラスは、maven compileでコンパイルした場合、デフォルトならtarget/generated-sources/annotationsに作成されているはず。
import java.util.List;
import org.immutables.value.Value.Immutable;
@Immutable
public interface User {
String name();
String address();
int age();
List<String> hobbies();
}
Builder
- Valueクラス(上記例ではUserクラス)から自動生成されたImmutableクラスを使用する。
- Immutableクラスのbuilderメソッドを呼び出し、データを代入、最後にbuildメソッドを呼び出す。
- これでImmutableなオブジェクトが生成される。
- Valueクラスのメソッドで、データが取得できる。
User user = ImmutableUser.builder()
.name("Tom")
.age(21)
.address("Tokyo, Japan")
.addHobbies("reading", "skiing")
.build();
System.out.println(user.toString());
// User{name=Tom, address=Tokyo, Japan, age=21, hobbies=[reading, skiing]}
System.out.println(user.name());
// Tom
System.out.println(user.age());
// 21
- Immutableオブジェクトにデータを追加することはできない。(ListクラスやMapクラスにデータを追加しようとすると、UnsupportedOperationExceptionが発生する)
// listMapはImmutableオブジェクト
listMap.list().add("item"); // java.lang.UnsupportedOperationException
listMap.map().put("key", "value"); //java.lang.UnsupportedOperationException
from()
- あるImmutableオブジェクトを元に別のImmutableオブジェクトを生成する場合はfromメソッドを使う。
User userTemp = ImmutableUser.builder()
.name("Tom")
.age(21)
.address("Tokyo, Japan")
.addHobbies("reading", "skiing")
.build();
User user = ImmutableUser.builder()
.from(userTemp)
.address("Osaka, Japan")
.build();
System.out.println(user.toString());
// User{name=Tom, address=Osaka, Japan, age=21, hobbies=[reading, skiing]}
Constructor
- コンストラクターでImmutableオブジェクトを生成する場合は、Parameterアノテーションをメソッドへ付与する。
import org.immutables.value.Value.Immutable;
import org.immutables.value.Value.Parameter;
@Immutable
public interface Item {
@Parameter
String name();
@Parameter
int price();
}
- Immutableクラスのofメソッドにデータを与えて、Immutableオブジェクトを生成する。
Item item = ImmutableItem.of("apple", 213);
System.out.println(item);
// Item{name=apple, price=213}
Optional
- Immutableクラスに任意項目を定義する場合、Optionalを使用する。
import java.util.Optional;
import org.immutables.value.Value.Immutable;
@Immutable
public interface Address {
String country();
String city();
String blockNmuber();
Optional<String> apartmentName();
}
- Optionalで定義された項目はデータを設定しなくてもよいが、そうでない項目はデータを設定しないとIllegalStateExceptionが発生する。
Address address1=ImmutableAddress.builder()
.country("Japan")
.city("Yokohama")
.blockNmuber("1-2-3")
.build(); // OK
Address address2=ImmutableAddress.builder()
.country("Japan")
.city("Chiba")
.blockNmuber("1-2-3")
.apartmentName("YYY Apartment")
.build(); // OK
Address address3=ImmutableAddress.builder()
.country("Japan")
.city("Chiba")
.build(); // java.lang.IllegalStateException: Cannot build Address, some of required attributes are not set [blockNmuber]
Default
- デフォルト値を持たせたい項目にDefaultアノテーションを付与し、デフォルトメソッドを定義する。
import org.immutables.value.Value.Default;
import org.immutables.value.Value.Immutable;
@Immutable
public interface AddressWithDefault {
@Default
default String country(){
return "Japan";
}
String city();
String blockNmuber();
}
- Immutableオブジェクト生成時にデータを設定しないと、デフォルト値が設定される。
AddressWithDefault address = ImmutableAddressWithDefault.builder()
.city("Saitama")
.blockNmuber("4-5-6")
.build();
System.out.println(address.toString());
// AddressWithDefault{country=Japan, city=Saitama, blockNmuber=4-5-6}
Derived
- Derivedアノテーションを付与して、Immutableオブジェクトに設定されたデータを元に処理を行う項目を定義する。
import java.util.Map;
import org.immutables.value.Value.Derived;
import org.immutables.value.Value.Immutable;
@Immutable
public abstract class Receipt {
abstract Map<String, Integer> items();
@Derived
public int total() {
// itemsのvalueを合計する
return items().values().stream().reduce(0, Integer::sum);
}
}
Receipt receipt = ImmutableReceipt.builder()
.putItems("bread", 210)
.putItems("milk", 100)
.build();
System.out.println(receipt.total());
// 310
Precondition check
- Checkアノテーションを付与してImmutableオブジェクトが生成される際に、チェック処理を定義する。
import java.util.List;
import org.immutables.value.Value.Check;
import org.immutables.value.Value.Immutable;
@Immutable
public abstract class Cart {
abstract List<String> items();
@Check
protected void check() {
if (items() == null || items().isEmpty()) {
throw new IllegalStateException();
}
}
}
- チェック条件を満たさないとImmutableオブジェクト生成時にエラーを発生させることができる。
ImmutableCart.builder().build(); // java.lang.IllegalStateException
応用編
Wrapper Types
-
WrappedインタフェースとWrapperクラスを用意しておけば、Valueクラスの定義がシンプルになり、同じようなコードを何度も書かなくてよくなる。
-
Wrappedインタフェース
@Value.Style(
typeAbstract = "_*",
typeImmutable = "*",
visibility = ImplementationVisibility.PUBLIC,
defaults = @Value.Immutable(builder = false, copy = false))
public @interface Wrapped {
}
- Wrapperクラス
public static abstract class Wrapper<T> {
@Value.Parameter
public abstract T value();
@Override
public String toString() {
return getClass().getSimpleName() + "(" + value() + ")";
}
}
- Valueクラスとその呼び出し方は以下の通り。
@Value.Immutable
@Wrapped
public static abstract class _FirstName extends Wrapper<String> {
}
@Value.Immutable
@Wrapped
public static abstract class _LastName extends Wrapper<String> {
}
@Value.Immutable
@Wrapped
public static abstract class _Age extends Wrapper<Integer> {
}
FirstName firstName = FirstName.of("Taro");
LastName lastName = LastName.of("Yamada");
Age age = Age.of(45);
コンパイルに失敗する場合
-
mvn clean compile
のように、クリーンしてからコンパイルする。 - useIncrementalCompilationオプションにfalseを設定する。