AutoValueの紹介

1概要

AutoValue は、Javaのソースコードジェネレータであり、より具体的には、値オブジェクトまたは値型オブジェクトのソースコードを生成するためのライブラリです。

値型オブジェクトを生成するためにあなたがしなければならないのは、 @ AutoValue アノテーションで抽象クラスにアノテーションを付けて、あなたのクラスをコンパイルすることだけです。生成されるのは、アクセサメソッド、パラメータ化コンストラクタ、 toString()、equals(Object) 、および hashCode() メソッドを適切にオーバーライドした値オブジェクトです。

次のコードスニペットは、コンパイル時に 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();
}

続けて、値オブジェクトについて、なぜそれらが必要なのか、そしてAutoValueがコードの生成とリファクタリングの作業を非常に時間のかかる作業にするのに役立つのかを調べましょう。

2 Mavenのセットアップ

MavenプロジェクトでAutoValueを使用するには、 pom.xml に次の依存関係を含める必要があります。

<dependency>
    <groupId>com.google.auto.value</groupId>
    <artifactId>auto-value</artifactId>
    <version>1.2</version>
</dependency>

最新版はhttps://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22auto-value%22[このリンク]をたどることによって見つけることができます。

3値型オブジェクト

値型はライブラリの最終製品なので、開発作業におけるその位置を理解するためには、値型、それらが何であるか、何がそうでないか、そしてなぜそれらが必要なのかを完全に理解する必要があります。

** 3.1. 値型とは

値型オブジェクトは、同一性が同一性によって決まるのではなく、それらの内部状態によって決まるオブジェクトです。つまり、値型オブジェクトの2つのインスタンスは、等しいフィールド値を持つ限り、等しいと見なされます。

  • 通常、値型は不変です** 。それらのフィールドは final にする必要があり、 setter メソッドを持つことはできません。インスタンス化後にフィールドを変更可能にするからです。

それらはコンストラクタかファクトリメソッドを通してすべてのフィールド値を消費しなければなりません。

値型は、デフォルトまたはゼロ引数コンストラクタを持たず、セッターメソッドも持たないため、JavaBeansではありません。

さらに、値型クラスはfinalにする必要があります。そのため、それらは拡張不可能で、少なくとも誰かがメソッドをオーバーライドすることはできません。 JavaBeans、DTO、POJOは最終的なものである必要はありません。

3.2. 値タイプの作成

text numberという名前のフィールドを持つ Foo__という名前の値型を作成するとします。

最終クラスを作成し、そのすべてのフィールドを最終クラスとしてマークします。次に、IDEを使用してコンストラクタ、 hashCode() メソッド、 equals(Object) メソッド、必須メソッドとしての getters 、および toString() メソッドを生成します。そして、次のようなクラスがあります。

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

Foo のインスタンスを作成した後は、そのライフサイクル全体を通じて、その内部状態は同じままであることが期待されます。

次のサブセクションで見るように、オブジェクトの hashCode はインスタンスからインスタンスへ** 変更しなければなりませんが、値型の場合は、値オブジェクトの内部状態を定義するフィールドに関連付ける必要があります。

したがって、同じオブジェクトのフィールドを変更しても、 hashCode 値は変わります。

3.3. 値型のしくみ

value-typesが不変でなければならない理由は、それらがインスタンス化された後にアプリケーションによって内部状態が変更されるのを防ぐためです。

2つの値型オブジェクトを比較したいときはいつでも 、したがって、私たちは Object クラスの equals(Object) メソッドを使わなければなりません

つまり、このメソッドは常に自分のvalue型でオーバーライドし、比較しているvalueオブジェクトのフィールドの値が等しい場合にのみtrueを返す必要があります。

さらに、 __HashSet sや HashMap sのようなハッシュベースのコレクションで値オブジェクトを壊れずに使用するには、 hashCode()__メソッドを正しく実装する必要があります 。

3.4. なぜ値型が必要なのか

値型の必要性はかなり頻繁に発生します。これらは、元の Object クラスのデフォルトの動作をオーバーライドしたい場合です。

すでに知っているように、 Object クラスのデフォルト実装は、2つのオブジェクトが同じIDを持つ場合は等しいと見なしますが、私たちの目的のために、2つのオブジェクトが同じ内部状態を持つ場合は等しいと見なします** 。

次のようにMoneyオブジェクトを作成したいとします。

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

}

その同等性をテストするために、それに対して次のテストを実行できます。

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

テストの意味に注意してください。

2つのお金のオブジェクトが等しくない場合、合格したと見なします。

これは、 equals メソッドをオーバーライドしていないため、オブジェクトのメモリ参照を比較することによって同等性が測定されるためです。もちろん、これらは異なるオブジェクトが異なるメモリロケーションを占有するため、異なることはありません。

各オブジェクトは10,000米ドルを表しますが、 Javaは私たちのお金のオブジェクトが等しくないことを私たちに教えてくれます 。 2つのオブジェクトは、通貨の金額が異なる場合、または通貨の種類が異なる場合にのみ、等しくないとテストする必要があります。

それでは、同等の値のオブジェクトを作成してみましょう。今回は、IDEにほとんどのコードを生成させます。

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

唯一の違いは、 equals(Object) メソッドと hashCode() メソッドをオーバーライドしたことです。これで、JavaによるMoneyオブジェクトの比較方法を制御できます。同等のテストを実行しましょう。

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

このテストのセマンティクスに注意してください。両方のmoneyオブジェクトが equals メソッドを介してテストされるときに合格すると予想されます。

4なぜAutoValueなのですか?

値型とその必要性を完全に理解したので、次にAutoValueとそれが式にどのように入るかを見てみましょう。

4.1. ハンドコーディングに関する問題

前のセクションで行ったように値型を作成すると、 不適切なデザインや多くの定型コード に関連するいくつかの問題に遭遇します。

2つのフィールドクラスには9行のコードがあります。1つはパッケージ宣言用、2つはクラスシグネチャとその閉じ括弧用、2つはフィールド宣言用、2つはコンストラクタとその閉じ括弧用、そして2つはフィールドの初期化用です。各フィールドには、それぞれ3行のコードが追加され、さらに6行追加されます。

hashCode() メソッドと equalTo(Object) メソッドをそれぞれオーバーライドするには、それぞれ約9行と18行が必要です。また、 toString() メソッドをオーバーライドすると、さらに5行が追加されます。

それは私たちの2つのフィールドクラスのためのよくフォーマットされたコードベースが およそ50行のコード を要するであろうことを意味します。

4.2 The RescueのIDE?

これはEclipseやIntilliJのようなIDEと、作成する値型クラスが1つか2つだけの場合は簡単です。作成するクラスの数を考えてみてください。たとえIDEが私たちを助けてくれたとしても、それはまだ簡単でしょうか?

数か月後の早送りで、コードを見直して Money クラスを修正し、 currency フィールドを String タイプから Currency. という名前の別の値タイプに変換する必要があるとします。

4.3 IDEはあまり役に立ちません

EclipseのようなIDEでは、アクセサメソッドや toString() hashCode() equals(Object) メソッドを編集することはできません。

  • このリファクタリングは手動で行わなければならないでしょう** 。コードを編集するとバグの可能性が高まり、 Money クラスに新しいフィールドを追加するたびに、行数が急激に増加します。

このシナリオが発生するという事実を認識すること、それが頻繁にそして大量に発生するということは、AutoValueの役割を本当に理解するでしょう。

5自動値の例

AutoValueが解決する問題は、前のセクションで説明したすべての定型コードを邪魔にならないようにすることです。これにより、書き込み、編集、または読み取りを行う必要がなくなります。

私たちはまったく同じ Money の例を見ますが、今回はAutoValueを使います。一貫性のためにこのクラスを AutoValueMoney と呼びます。

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

何が起こったのかというと、抽象クラスを書き、それに対して抽象アクセサを定義しますが、フィールドは定義せず、クラス全体に合計8行のコードで @ AutoValue をアノテーションし、

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

このクラスを直接処理する必要はまったくありません。前のセクションの currency シナリオのように、フィールドを追加したりフィールドに変更を加えたりする必要があるときに編集する必要もありません。

  • Javac は常に更新されたコードを再生成します** 。

この新しいvalue-typeを使用している間、以下の単体テストで見られるように、すべての呼び出し元は親タイプのみを見ます。

これは、フィールドが正しく設定されていることを確認するためのテストです。

@Test
public void givenValueTypeWithAutoValue__whenFieldsCorrectlySet__thenCorrect() {
    AutoValueMoney m = AutoValueMoney.create("USD", 10000);
    assertEquals(m.getAmount(), 10000);
    assertEquals(m.getCurrency(), "USD");
}

同じ通貨と同じ金額を持つ2つの AutoValueMoney オブジェクトが等しいことを確認するためのテストは、

@Test
public void given2EqualValueTypesWithAutoValue__whenEqual__thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertTrue(m1.equals(m2));
}

ある通貨オブジェクトの通貨タイプをGBPに変更すると、テストは次のようになります。

5000 GBP == 5000 USD はもう当てはまりません。

@Test
public void given2DifferentValueTypesWithAutoValue__whenNotEqual__thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertFalse(m1.equals(m2));
}

6. ビルダーによる自動値

最初の例では、パブリック作成APIとして静的ファクトリメソッドを使用したAutoValueの基本的な使用方法について説明しました。

  • すべてのフィールドが Strings ** の場合は、amount currency__の代わりに配置するなど、静的ファクトリメソッドに渡したときにそれらを交換するのは簡単です。

これは、多くのフィールドがあり、すべてが String 型である場合に特に起こりやすいです。この問題は、AutoValueでは、すべてのフィールドがコンストラクタを通じて初期化されるため、さらに悪化します。

この問題を解決するためには builder パターンを使うべきです。幸運なことに。

これはAutoValueによって生成されます。

私たちのAutoValueクラスは、静的ファクトリメソッドがビルダーに置き換えられていることを除けば、それほど大きな変更はありません。

@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();
    }
}

生成されたクラスは最初のクラスとまったく同じですが、ビルダーの抽象メソッドを実装すると同時にビルダーの具象内部クラスが生成されます。

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

テスト結果がどのように変化しないかにも注意してください。

フィールド値が実際にビルダーを通して正しく設定されていることを知りたい場合は、次のテストを実行できます。

@Test
public void givenValueTypeWithBuilder__whenFieldsCorrectlySet__thenCorrect() {
    AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder().
      setAmount(5000).setCurrency("USD").build();
    assertEquals(m.getAmount(), 5000);
    assertEquals(m.getCurrency(), "USD");
}

同等性をテストするには、内部状態に依存します。

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

そしてフィールドの値が異なる場合

@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. 結論

このチュートリアルでは、GoogleのAutoValueライブラリの基本のほとんどと、それを使用してごくわずかなコードで値型を作成する方法を紹介しました。

GoogleのAutoValueに代わるものとしてhttps://projectlombok.org/[Lombokプロジェクト]があります - Lombok をご覧ください。

これらすべての例とコードスニペットの完全な実装は、AutoValue GitHubプロジェクト にあります。