1つの変数が未設定(id
)のインスタンスを作成し、後でこの変数を初期化し、それを初期化後に不変にできるクラスが必要です。事実上、コンストラクターの外部で初期化できるfinal
変数が必要です。
現在、私はこれを次のようにException
をスローするセッターで即興しています:
public class Example {
private long id = 0;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) throws Exception {
if ( this.id == 0 ) {
this.id = id;
} else {
throw new Exception("Can't change id once set");
}
}
}
これは私がやろうとしていることについて良い方法ですか?初期化された後、何かを不変に設定できるようにしたり、これをよりエレガントにするために使用できるパターンがあると感じています。
もう少しエレガントな決定をお勧めします。最初のバリアント(例外をスローしない):
public class Example {
private Long id;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) {
this.id = this.id == null ? id : this.id;
}
}
2番目のバリアント(例外をスローする場合):
public void setId(long id) {
this.id = this.id == null ? id : throw_();
}
public int throw_() {
throw new RuntimeException("id is already set");
}
「一度だけ設定する」要件は少しbit意的です。あなたが探しているのは、初期化されていない状態から初期化された状態に永続的に遷移するクラスです。結局のところ、オブジェクトが「ビルド」された後に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()
を呼び出して「不変」にします。
このパターンには、最初のアプローチに比べていくつかの利点があります。
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
フィールドを使用しているため、コンパイラと開発者の両方がこれらの値を変更できないことを知っています。はい、維持するのは少し複雑ですが、私見の利点はコストを上回ります。
Googleの Guavaライブラリ (私は非常に強くお勧めします)には、この問題を非常によく解決するクラス SettableFuture
が付属しています。これにより、1回限りのセマンティクスが提供されるだけでなく、さらに多くの機能が提供されます。
setException
メソッド);ListenableFuture
interface )。Future
ファミリは、一般的にマルチスレッドプログラムのスレッド間の同期に使用されるタイプであるため、SettableFuture
はこれらと非常にうまく機能します。Java 8には、独自のバージョンの CompletableFuture
もあります。
ブール値フラグを追加するだけで、setId()でブール値を設定/確認できます。質問が正しければ、ここでは複雑な構造/パターンは必要ありません。これはどう:
_public class Example {
private long id = 0;
private boolean touched = false;
// Constructors and other variables and methods deleted for clarity
public long getId() {
return id;
}
public void setId(long id) throws Exception {
if ( !touchted ) {
this.id = id;
touched = true;
} else {
throw new Exception("Can't change id once set");
}
}
}
_
このように、setId(0l);
を使用すると、IDも設定されていると見なされます。ビジネスロジックの要件に合わない場合は変更できます。
iDEで編集していない場合は、タイプミス/フォーマットの問題で申し訳ありません。
JDKのAtomicReference
に似たこのクラスがあり、ほとんどレガシーコードに使用します。
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe
public class PermanentReference<T> {
private T reference;
public PermanentReference() {
}
public void set(final @Nonnull T reference) {
checkState(this.reference == null,
"reference cannot be set more than once");
this.reference = checkNotNull(reference);
}
public @Nonnull T get() {
checkState(reference != null, "reference must be set before get");
return reference;
}
}
single responsibilty があり、get
とset
の両方の呼び出しをチェックしているため、クライアントコードがそれを誤用すると早期に失敗します。
2つの方法があります。最初は他の回答で言及された他のいくつかのものと基本的に同じですが、ここでは秒と比較します。したがって、最初の方法は、1回は、セッターでそれを強制することによって1回だけ設定できる値を持つことです。私の実装は非ヌル値を必要としますが、ヌルに設定できるようにしたい場合は、他の回答で提案されているように「isSet」ブール値フラグを実装する必要があります。
2番目の方法であるLazyは、ゲッターが最初に呼び出されたときに値を遅延的に提供する関数を提供することです。
import javax.annotation.Nonnull;
public final class Once<T>
{
private T value;
public set(final @Nonnull T value)
{
if(null != this.value) throw new IllegalStateException("Illegal attempt to set a Once value after it's value has already been set.");
if(null == value) throw new IllegalArgumentException("Illegal attempt to pass null value to Once setter.");
this.value = value;
}
public @Nonnull T get()
{
if(null == this.value) throw new IllegalStateException("Illegal attempt to access unitialized Once value.");
return this.value;
}
}
public final class Lazy<T>
{
private Supplier<T> supplier;
private T value;
/**
* Construct a value that will be lazily intialized the
* first time the getter is called.
*
* @param the function that supplies the value or null if the value
* will always be null. If it is not null, it will be called
* at most one time.
*/
public Lazy(final Supplier<T> supplier)
{
this.supplier = supplier;
}
/**
* Get the value. The first time this is called, if the
* supplier is not null, it will be called to supply the
* value.
*
* @returns the value (which may be null)
*/
public T get()
{
if(null != this.supplier)
{
this.value = this.supplier.get();
this.supplier = null; // clear the supplier so it is not called again
// and can be garbage collected.
}
return this.value;
}
}
したがって、これらを次のように使用できます。
//
// using Java 8 syntax, but this is not a hard requirement
//
final Once<Integer> i = Once<>();
i.set(100);
i.get(); // returns 100
// i.set(200) would throw an IllegalStateException
final Lazy<Integer> j = Lazy<>(() -> i);
j.get(); // returns 100
上記の回答とコメントの一部、特にassertの使用に関する@KatjaChristiansenの回答に基づいて、私が思いついた解決策があります。
public class Example {
private long id = 0L;
private boolean idSet = false;
public long getId() {
return id;
}
public void setId(long id) {
// setId should not be changed after being set for the first time.
assert ( !idSet ) : "Can't change id from " + this.id + " to " + id;
this.id = id;
idSet = true;
}
public boolean isIdSet() {
return idSet;
}
}
結局のところ、これが必要なのは他の場所での不十分な設計決定の兆候だと思うので、IDがわかっている場合にのみオブジェクトを作成し、idをfinalに設定する方法を見つける必要があります。これにより、コンパイル時にさらに多くのエラーを検出できます。
のようなintチェッカーを持ってみてください
private long id = 0;
static int checker = 0;
public void methodThatWillSetValueOfId(stuff){
checker = checker + 1
if (checker==1){
id = 123456;
}
}
// uはこれを試すことができます:
class Star
{
private int i;
private int j;
static boolean a=true;
Star(){i=0;j=0;}
public void setI(int i,int j) {
this.i =i;
this.j =j;
something();
a=false;
}
public void printVal()
{
System.out.println(i+" "+j);
}
public static void something(){
if(!a)throw new ArithmeticException("can't assign value");
}
}
public class aClass
{
public static void main(String[] args) {
System.out.println("");
Star ob = new Star();
ob.setI(5,6);
ob.printVal();
ob.setI(6,7);
ob.printVal();
}
}