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>

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

3. 値型オブジェクト

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

3.1. 値型とは何ですか?

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

通常、値型は不変です。 これらのフィールドはfinalにする必要があり、 setter メソッドを使用しないでください。これにより、インスタンス化後にフィールドを変更できるようになります。

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

値型は、デフォルトまたはゼロ引数のコンストラクターがなく、セッターメソッドもないため、Java Beansではありません。同様に、データ転送オブジェクトでもプレーンオールドJavaオブジェクトでもありません。 。

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

3.2. 値型の作成

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

最終クラスを作成し、そのすべてのフィールドを最終としてマークします。 次に、IDEを使用してコンストラクター、 hashCode()メソッド、 equals(Object)メソッド、 getters を必須メソッドとして、[ X171X] 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. 値型のしくみ

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

したがって、2つの値型オブジェクトを比較する場合は常に、 Objectクラスのequals(Object)メソッドを使用する必要があります。

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

さらに、HashSetHashMapなどのハッシュベースのコレクションで値オブジェクトを中断せずに使用するには、 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でマネーオブジェクトを比較する方法を制御できます。 同等のテストを実行してみましょう。

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

このテストのセマンティクスに注意してください。両方のマネーオブジェクトが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. 救助へのIDE?

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

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

4.3. IDEはあまり役に立たない

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

このリファクタリングは手動で行う必要があります。 コードを編集するとバグの可能性が高まり、 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は常に更新されたコードを再生成します

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

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

@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 == 5000USDは真ではなくなります。

@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の基本的な使用法について説明しています。

すべてのフィールドが文字列の場合、静的ファクトリメソッドに渡すときに、 amount通貨の場所およびその逆。

これは、多くのフィールドがあり、すべてが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に代わるものは、Lombokプロジェクトです。Lombokの使用に関する紹介記事はこちらでご覧いただけます。

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