AutoValueの紹介

AutoValueの概要

1. 概要

AutoValueはJavaのソースコードジェネレーターであり、より具体的にはgenerating source code for value objects or value-typed objectsのライブラリです。

値型オブジェクトを生成するには、annotate an abstract class with the @AutoValue annotationを実行してクラスをコンパイルするだけです。 生成されるのは、アクセサメソッド、パラメータ化されたコンストラクタ、適切にオーバーライドされたtoString(), equals(Object)およびhashCode()メソッドを持つ値オブジェクトです。

次のコードスニペットは、コンパイル時にAutoValue_Personという名前の値オブジェクトになる抽象クラスのa quick exampleです。

@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に次の依存関係を含める必要があります。


    com.google.auto.value
    auto-value
    1.2

最新バージョンは、this linkをたどることで見つけることができます。

3. 値型オブジェクト

バリュータイプはライブラリの最終製品であるため、開発タスクにおける価値の位置を理解するには、バリュータイプ、それが何であるか、何がそうではないか、なぜ必要なのかを完全に理解する必要があります。

3.1. 値型とは何ですか?

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

Typically, value-types are immutable。 それらのフィールドはfinalにする必要があり、setterメソッドを使用しないでください。これにより、インスタンス化後に変更可能になります。

これらは、コンストラクターまたはファクトリーメソッドを通じてすべてのフィールド値を消費する必要があります。

値型はJavaBeansではありません。これは、デフォルトまたはゼロ引数のコンストラクターがなく、同様にthey are not Data Transfer Objects nor Plain Old Java Objectsのようにセッターメソッドもないためです。

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

3.2. 値型の作成

textおよびnumber.というフィールドを持つFooという値型を作成したいとします。どうすればよいでしょうか。

最終クラスを作成し、そのすべてのフィールドをfinalとしてマークします。 次に、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のインスタンスを作成した後、その内部状態はライフサイクル全体にわたって同じままであると予想されます。

次のサブセクションthe hashCode of an object must change from instance to instanceで説明しますが、値型の場合は、値オブジェクトの内部状態を定義するフィールドに関連付ける必要があります。

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

3.3. 値型のしくみ

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

2つの値型オブジェクト、we must, therefore, use the equals(Object) method of the Object classを比較するときはいつでも。

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

さらに、HashSetsやHashMapsなどのハッシュベースのコレクションで値オブジェクトを中断せずに使用するには、we must properly implement the hashCode() methodを使用します。

3.4. 値型が必要な理由

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

すでに知っているように、Objectクラスのデフォルトの実装では、2つのオブジェクトが同じIDであるが、for our purposes we consider two objects equal when they have the same internal stateである場合、それらは等しいと見なされます。

次のように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つのmoneyオブジェクトが等しくないときに合格したと考えます。 これは、we have not overridden the equals methodがオブジェクトのメモリ参照を比較することによって同等性が測定されるためです。もちろん、オブジェクトは異なるメモリ位置を占める異なるオブジェクトであるため、違いはありません。

各オブジェクトは10,000米ドルを表しますが、Java tells us our money objects are not equalです。 通貨額が異なるか、通貨タイプが異なる場合にのみ、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. 手作業によるコーディングの問題

前のセクションで行ったように値型を作成すると、bad design and a lot of boilerplate codeに関連するいくつかの問題が発生します。

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

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

つまり、2つのフィールドクラスの適切にフォーマットされたコードベースはabout 50 lines of codeを必要とします。

4.2 IDEs to The Rescue?

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

早送りして、数か月後に、コードを再検討してMoneyクラスを修正し、おそらくcurrencyフィールドをString型から別の値型に変換する必要があると想定します。 Currency.と呼ばれる

4.3 IDEs Not Really so Helpful

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

This refactoring would have to be done by hand。 コードを編集するとバグの可能性が高まり、Moneyクラスに新しいフィールドを追加するたびに、行数が指数関数的に増加します。

このシナリオが発生するという事実を認識し、それが頻繁に発生し、大量に発生すると、AutoValueの役割に本当に感謝します。

5. AutoValueの例

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

何が起こったのかというと、抽象クラスを記述し、その抽象アクセサーを定義しますが、フィールドは定義しません。クラスに@AutoValueで注釈を付け、合計で8行のコードになり、javacは次の具体的なサブクラスを生成します。このように見える私たち:

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 will always regenerate updated code for us

この新しい値型を使用している間、すべての呼び出し元は、次の単体テストで見るように、親型のみを参照します。

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

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

1つのマネーオブジェクトの通貨タイプを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. ビルダーを使用したAutoValue

見てきた最初の例は、パブリックファクトリAPIとして静的ファクトリメソッドを使用したAutoValueの基本的な使用法をカバーしています。

Notice that if all our fields were*Strings*,は、currencyの代わりにamountを配置するなど、静的ファクトリメソッドに渡すときに簡単に交換できます。

これは、多くのフィールドがあり、すべてがStringタイプである場合に特に発生する可能性があります。 この問題は、AutoValueではall fields are initialized through the constructorであるという事実によってさらに悪化します。

この問題を解決するには、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の代わりにLombok projectがあります–Lombok hereの使用に関する紹介記事を見ることができます。

これらすべての例とコードスニペットの完全な実装は、AutoValueGitHub projectにあります。