「一度だけ設定する」という要件は、少し恣意的に感じます。あなたが探しているのは、初期化されていない状態から初期化された状態に永久に遷移するクラスであると確信しています。結局のところ、オブジェクトが「構築」された後に ID を変更することが許可されていない限り、(コードの再利用などによって) オブジェクトの ID を複数回設定すると便利な場合があります。
かなり合理的なパターンの 1 つは、この「構築済み」の状態を別のフィールドで追跡することです。
public final class Example {
private long id;
private boolean isBuilt;
public long getId() {
return id;
}
public void setId(long id) {
if (isBuilt) throw new IllegalArgumentException("already built");
this.id = id;
}
public void build() {
isBuilt = true;
}
}
使用法:
Example e = new Example();
// do lots of stuff
e.setId(12345L);
e.build();
// at this point, e is immutable
このパターンでは、オブジェクトを構築し、その値を (都合のよい回数だけ) 設定してから、それbuild()
を "immutify" するために呼び出します。
このパターンには、最初のアプローチよりもいくつかの利点があります。
- 初期化されていないフィールドを表すために使用される魔法の値はありません。たとえば、
0
は他のlong
値と同じように有効な ID です。
- セッターの動作は一貫しています。呼び出される前
build()
に、それらは機能します。が呼び出された後build()
、渡した値に関係なく、スローされます。(便宜上、非チェック例外の使用に注意してください)。
- クラスは とマークされ
final
ています。そうしないと、開発者がクラスを拡張してセッターをオーバーライドする可能性があります。
しかし、このアプローチにはかなり大きな欠点があります。このクラスを使用する開発者は、特定のオブジェクトが初期化されているかどうかをコンパイル時に知ることができません。確かに、isBuilt()
メソッドを追加して、オブジェクトが初期化されているかどうかを開発者が実行時に確認できるようにすることもできますが、コンパイル時にこの情報を知る方がはるかに便利です。そのためには、ビルダー パターンを使用できます。
public final class Example {
private final long id;
public Example(long id) {
this.id = id;
}
public long getId() {
return id;
}
public static class Builder {
private long id;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Example build() {
return new Example(id);
}
}
}
使用法:
Example.Builder builder = new Example.Builder();
builder.setId(12345L);
Example e = builder.build();
これは、いくつかの理由ではるかに優れています。
final
フィールドを使用しているため、コンパイラと開発者の両方がこれらの値を変更できないことを知っています。
- オブジェクトの初期化された形式と初期化されていない形式の違いは、Java の型システムによって記述されます。一度構築されたオブジェクトに対して呼び出すセッターはありません。
- 構築されたクラスのインスタンスは、スレッドセーフであることが保証されています。
はい、維持するのはもう少し複雑ですが、IMHO の利点はコストを上回ります。