1. 概要

不変の値オブジェクトを作成すると、不要な定型文が少し導入されます。 また、 Javaの標準コレクションタイプは、この特性が望ましくない値オブジェクトに可変性を導入する可能性があります。

このチュートリアルでは、 AutoValue を使用してコレクションの防御コピーを作成する方法を示します。これは、不変の値オブジェクトを定義するための定型コードを減らすための便利なツールです。

2. バリューオブジェクトと防御コピー

Javaコミュニティは通常、値オブジェクトを不変のデータレコードを表すタイプの分類と見なします。 もちろん、そのようなタイプには、java.util.Listのような標準のJavaコレクションタイプへの参照が含まれる場合があります。

たとえば、Person値オブジェクトについて考えてみます。

class Person {
    private final String name;
    private final List<String> favoriteMovies;

    // accessors, constructor, toString, equals, hashcode omitted
}

Javaの標準コレクションタイプは変更可能である可能性があるため、不変の Person タイプは、新しいPersonを作成した後にfavoriteMoviesリストを変更する呼び出し元から自身を保護する必要があります。 ]:

var favoriteMovies = new ArrayList<String>();
favoriteMovies.add("Clerks"); // fine
var person = new Person("Katy", favoriteMovies);
favoriteMovies.add("Dogma"); // oh, no!

Person クラスは、favoriteMoviesコレクションの防御コピーを作成する必要があります。 そうすることで、 Person クラスは、Personが作成されたときに存在していたfavoriteMoviesリストの状態をキャプチャします。

Person クラスコンストラクターは、 List.copyOf 静的ファクトリメソッドを使用して、favoriteMoviesリストの防御コピーを作成できます。

public Person(String name, List<String> favoriteMovies) {
    this.name = name;
    this.favoriteMovies = List.copyOf(favoriteMovies);
}

Java 10では、List.copyOfなどの防御コピー静的ファクトリメソッドが導入されました。 古いバージョンのJavaを使用するアプリケーションは、コピーコンストラクターとCollectionsクラスの「変更不可能な」静的ファクトリメソッドの1つを使用して防御コピーを作成する場合があります。

public Person(String name, List<String> favoriteMovies) {
    this.name = name;
    this.favoriteMovies = Collections.unmodifiableList(new ArrayList<>(favoriteMovies));
}

String インスタンスは不変であるため、 Stringnameパラメーターの防御コピーを作成する必要がないことに注意してください。

3. AutoValueと防御コピー

AutoValueは、値オブジェクトタイプを定義するためのボイラープレートコードを生成するための注釈処理ツールです。 ただし、 AutoValueは、値オブジェクトを作成するときに防御コピーを作成しません。

@AutoValue アノテーションは、 Person を拡張し、アクセサー、コンストラクター、 toString を含むクラスAutoValue_Personを生成するようにAutoValueに指示します。 ] equals 、および hashCode メソッドは、以前にPersonクラスから省略しました。

最後に、静的ファクトリメソッドを Person クラスに追加し、生成されたAutoValue_Personコンストラクターを呼び出します。

@AutoValue
public abstract class Person {

    public static Person of(String name, List<String> favoriteMovies) {
        return new AutoValue_Person(name, favoriteMovies);
    }

    public abstract String name();
    public abstract List<String> favoriteMovies();
}

AutoValueが生成するコンストラクターは、 favoriteMovies コレクション用のものを含め、防御コピーを自動的に作成しません。

したがって、定義した静的ファクトリメソッドでfavoriteMoviesコレクションの防御コピーを作成する必要があります。

public abstract class Person {

    public static Person of(String name, List<String> favoriteMovies) {
        // create defensive copy before calling constructor
        var favoriteMoviesCopy = List.copyOf(favoriteMovies);
        return new AutoValue_Person(name, favoriteMoviesCopy);
    }

    public abstract String name();
    public abstract List<String> favoriteMovies();
}

4. AutoValueビルダーと防御コピー

必要に応じて、 @ AutoValue.Builder アノテーションを使用できます。これは、AutoValueにBuilderクラスを生成するように指示します。

@AutoValue
public abstract class Person {

    public abstract String name();
    public abstract List<String> favoriteMovies();

    public static Builder builder() {
        return new AutoValue_Person.Builder();
    }

    @AutoValue.Builder
    public static class Builder {
        public abstract Builder name(String value);
        public abstract Builder favoriteMovies(List<String> value);
        public abstract Person build();
    }
}

AutoValueはすべての抽象メソッドの実装を生成するため、Listの防御コピーを作成する方法が明確ではありません。 ビルダーが新しいPersonインスタンスを構築する直前に、AutoValueで生成されたコードとカスタムコードを組み合わせてコレクションの防御コピーを作成する必要があります。

まず、ビルダーを2つの新しいパッケージプライベート抽象メソッド favoriteMovies() autoBuild()で補完します。 これらのメソッドは、 build()メソッドのカスタム実装で使用するため、パッケージプライベートですが、このAPIのコンシューマーに使用させたくありません。

@AutoValue.Builder
public static abstract class Builder {

    public abstract Builder name(String value);
    public abstract Builder favoriteMovies(List<String> value);

    abstract List<String> favoriteMovies();
    abstract Person autoBuild();

    public Person build() {
        // implementation omitted
    }
}

最後に、 Person を作成する前に、リストの防御コピーを作成するbuild()メソッドのカスタム実装を提供します。 favoriteMovies()メソッドを使用して、ユーザーが設定したListを取得します。 次に、 autoBuild()を呼び出して Person を作成する前に、リストを新しいコピーに置き換えます。

public Person build() {
    List<String> favoriteMovies = favoriteMovies();
    List<String> copy = Collections.unmodifiableList(new ArrayList<>(favoriteMovies));
    favoriteMovies(copy);
    return autoBuild();
}

5. 結論

このチュートリアルでは、AutoValueが防御コピーを自動的に作成しないことを学びました。これはJavaコレクションにとってしばしば重要です。

AutoValueで生成されたクラスのインスタンスを作成する前に、静的ファクトリメソッドで防御コピーを作成する方法を示しました。 次に、AutoValueの Builder クラスを使用するときに、カスタムコードと生成されたコードを組み合わせて防御コピーを作成する方法を示しました。

いつものように、このチュートリアルで使用されるコードスニペットは、GitHubから入手できます。