Java におけるインスタンス生成パターンについて


コンストラクタの課題

多数のフィールドを持つクラスを考えます。

例えば以下のような Circle クラスを考えます。

public class Circle {  
    private final double x, y;  
    private final double radius;  
    private final double lineWidth;  
    private final Color lineColor;  
    private final Color fillColor;  
}

ミュータブルで良い場合は、JavaBean スタイルとすることができます。

public class Circle {  
    private double x = 0, y = 0;  
    private double radius = 10;  
    private double lineWidth = 1;  
    private Color lineColor = Color.BLACK;  
    private Color fillColor = Color.TRANSPARENT;  
  
    public double getX() { return x; }  
    public void setX(double x) { this.x = x; }  
    public double getY() { return y; }  
    public void setY(double y) { this.y = y; }  
    // ... 
}

JavaBean スタイルとした場合は、フィールドを追加した場合にクライアント側でそのことに気付けないという問題があります。また、並列処理では同期を考慮する必要があり、現代では避けられる傾向にあります。


ミュータブルとする場合には、全てのフィールドを初期化する必要があるため、コンストラクタは長くなり、クライアント側の負担が大きくなります。

    public Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = lineWidth;  
        this.lineColor = lineColor;  
        this.fillColor = fillColor;  
    }  

クライアント側の負荷を軽減するため、適切なデフォルト値を用意し、インスタンス化のバリエーションに応じたコンストラクタを準備することができます。

    public Circle() {  
        this.x = 0;  
        this.y = 0;  
        this.radius = 10;  
        this.lineWidth = 1;  
        this.lineColor = Color.BLACK;  
        this.fillColor = Color.TRANSPARENT;  
    }  
  
    public Circle(double x, double y) {  
        this.x = x;  
        this.y = y;  
        this.radius = 10;  
        this.lineWidth = 1;  
        this.lineColor = Color.BLACK;  
        this.fillColor = Color.TRANSPARENT;  
    }  
  
    public Circle(double x, double y, double radius) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = 1;  
        this.lineColor = Color.BLACK;  
        this.fillColor = Color.TRANSPARENT;  
    }  
  
    public Circle(double x, double y, double radius, double lineWidth) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = lineWidth;  
        this.lineColor = Color.BLACK;  
        this.fillColor = Color.TRANSPARENT;  
    }  
  
    public Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = lineWidth;  
        this.lineColor = lineColor;  
        this.fillColor = fillColor;  
    }

このままでは、デフォルト値の定義が複数現れており、保守性に乏しいため、Telescoping Constructor にて改善することができます。


Telescoping Constructor

Telescoping は伸縮の意味で、コンストラクタを段々に繋げていきます。

public class Circle {  
    // ...
    public Circle() {  
        this(0, 0);  
    }  
  
    public Circle(double x, double y) {  
        this(x, y, 10);  
    }  
  
    public Circle(double x, double y, double radius) {  
        this(x, y, radius, 1);  
    }  
  
    public Circle(double x, double y, double radius, double lineWidth) {  
        this(x, y, radius, lineWidth, Color.BLACK, Color.TRANSPARENT);  
    }  
  
    public Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = lineWidth;  
        this.lineColor = lineColor;  
        this.fillColor = fillColor;  
    }  
}


Static Factory Method

完全コンストラクタを用意し、 Static Factory Method でインスタンスを生成するというアプローチもあります。

public class Circle {  
    // ...
    public static Circle of() {  
        return new Circle(0, 0, 10, 1, Color.BLACK, Color.TRANSPARENT);  
    }  
  
    public static Circle of(double x, double y) {  
        return new Circle(x, y, 10, 1, Color.BLACK, Color.TRANSPARENT);  
    }  
  
    public static Circle of(double x, double y, double radius) {  
        return new Circle(x, y, radius, 1, Color.BLACK, Color.TRANSPARENT);  
    }  
  
    public static Circle of(double x, double y, double radius, double lineWidth) {  
        return new Circle(x, y, radius, lineWidth, Color.BLACK, Color.TRANSPARENT);  
    }  
  
    public static Circle of(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);  
    }  
  
    private Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
        this.x = x;  
        this.y = y;  
        this.radius = radius;  
        this.lineWidth = lineWidth;  
        this.lineColor = Color.BLACK;  
        this.fillColor = Color.TRANSPARENT;  
    }  
}

Static Factory Method の利点は、コンストラクタのように単一名ではなく、明示的な名前を与えることで、クライアントに意図を伝えることができる点にあります。

public static Circle fillRedOf(double x, double y, double radius) {  
    return new Circle(x, y, radius, 0, Color.RED, Color.RED);  
}

その他、コンストラクタと比較した利点には以下があります。

  • 呼び出されるたびに新しいオブジェクトを作成するのではなく、キャッシュした(イミュータブルな)インスタンスを返すことができる
  • 戻り値の型の任意のサブタイプのオブジェクトを返すことができる


Builder

多数のフィールドを持つオブジェクトを構成する際に良く利用されるのが Builder を介したものになります。

public class Circle {
    // ...
    private Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.lineWidth = lineWidth;
        this.lineColor = lineColor;
        this.fillColor = fillColor;
    }
    
    public static CircleBuilder builder() {
        return new CircleBuilder();
    }

    public static class CircleBuilder {

        private double x = 0, y = 0;
        private double radius = 10;
        private double lineWidth = 1;
        private Color lineColor = Color.BLACK;
        private Color fillColor = Color.TRANSPARENT;

        public CircleBuilder x(double x) {
            this.x = x;
            return this;
        }

        public CircleBuilder y(double y) {
            this.y = y;
            return this;
        }

        public CircleBuilder radius(double radius) {
            this.radius = radius;
            return this;
        }

        public CircleBuilder lineWidth(double lineWidth) {
            this.lineWidth = lineWidth;
            return this;
        }

        public CircleBuilder lineColor(Color lineColor) {
            this.lineColor = lineColor;
            return this;
        }

        public CircleBuilder fillColor(Color fillColor) {
            this.fillColor = fillColor;
            return this;
        }

        public Circle build() {
            return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
        }
    }
}

ミュータブルなオブジェクトに Fluent に値を設定していき、その内容を使ってインスタンスを生成するアプローチになります。

概念的に重くなりますが、LombokJilt によりアノテーションプロセッサでビルダーを自動生成できるので、広く普及しています。


Wither

近年は、インスタンス生成の低コスト化やGCの効率化の追い風もあり、withXX という形で新しいインスタンスを生成するアプローチがも増えてきています。

public class Circle {
    //...
    private Circle(double x, double y, double radius, double lineWidth, String lineColor, String fillColor) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.lineWidth = lineWidth;
        this.lineColor = lineColor;
        this.fillColor = fillColor;
    }
    public static Circle of() {
        return new Circle(0, 0, 10, 1, Color.BLACK, Color.TRANSPARENT);
    }
    public Circle withX(double x) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withY(double y) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withRadius(double radius) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withLineWidth(double lineWidth) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withLineColor(Color lineColor) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withFillColor(Color fillColor) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
}
var c = Circle.of().withRadius(5).withFillColor(Color.RED);

特に、ミュータブルなRecord クラスでは、このアプローチにより一部の値を変更した新しいオブジェクトを生成することが一般的です。

public record Circle(double x, double y, double radius, double lineWidth, Color lineColor, Color fillColor) {  
    public static Circle of() {
        return new Circle(0, 0, 10, 1, Color.BLACK, Color.TRANSPARENT);
    }
    public Circle withX(double x) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withY(double y) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withRadius(double radius) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withLineWidth(double lineWidth) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withLineColor(Color lineColor) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
    public Circle withFillColor(Color fillColor) {
        return new Circle(x, y, radius, lineWidth, lineColor, fillColor);
    }
}

ただし、都度インスタンスが生成されるため、性能面でのトレードオフを考慮する必要があります。

なお、Record の Builder や Wither は RecordBuilder などを使ってアノテーションプロセッサで自動生成することもできます。


JEP 468: Derived Record Creation (Preview)

現在プレビュー段階の Derived Record Creation では、Wither によるアプローチが言語機能に組み込むことが提案されています。

record Point(int x, int y, int z) { }

var p1 = new Point(0, 0, 0);
Point p2 = p1 with { 
    x += 1; 
    y += 1;
};

e with { ... } という構文で Record の一部のパラメータ値を変えた、新しいインスタンスを生成することができます。

未だプレビュー段階であり、Record のみの機能であり、通常のクラスには適用できない点に注意が必要です。

非Recordに対しては以下のように言及されています。

通常の非レコード値に対して派生した作成式を提供することは目標ではありません。これは将来のJEPの主題となる可能性があります。


名前付き引数

多くの言語では、名前付き引数(named arguments)がサポートされています。

例えば、Kotlin では以下のようにインスタンスを生成できます。

class Person(val name: String, val age: Int = 0)

val p1 = Person(name = "aa")
val p2 = Person(age = 30, name = "bb")

Derived Record Creation に続き、Java で名前付き引数のサポートが追加されれば、先に述べてきたインスタンス生成のパターンは全て不要になります。


しかし、Java で名前付き引数がサポートされる可能性はゼロに近いです。

Java では、パラメータ名はAPIの一部ではありません。 つまり、パラメータの型と位置が公開APIであり、パラメータ名はローカル変数と同じ扱いとなっています。

ローカル変数なので、その名前は、クライアントに影響を与えずにいつでも書き換えることができます。

名前付き引数は、引数の名前を外部に公開することになり、後方互換性を重視する Java では採用することが困難です(パラメータ名の変更が利用者側への破壊的変更になってしまう)。

では、Record ではどうでしょうか。Record は型パラメータとしてそのプロパティ名が外部APIの一部となっています(アクセッサの名前として利用される)。 なので、Record で名前付き引数をサポートすることに対する障壁はありません。

しかし現在は、Record として定義したものを通常のクラスに変更するというパスを確保しておきたい という理由で、Record で名前付き引数をサポートすることは否定されています。

ということで、今後も Builder や Wither を使ってインスタンスを生成する状況は続きます。