2011-07-05 5 views
3

私は本のEffective Javaを読んでいます。java:不変と最終に関する質問

Mutability Minimizeの項目では、Joshua Blochがクラスを不変にすることについて話しています。

  1. オブジェクトの状態を変更するメソッドを提供しないでください。これは問題ありません。

  2. クラスを拡張できないようにしてください。 - 本当にこれをする必要がありますか?

  3. すべてのフィールドを最終的にする - 本当にこれを行う必要はありますか?例えば

のは、私は不変クラスを持っていると仮定しましょう、

class A{ 
private int a; 

public A(int a){ 
    this.a =a ; 
} 

public int getA(){ 
    return a; 
} 
} 

がどのようにAから拡張するクラスは、Aの不変性を損なうことができますか?

public class B extends A { 
    private int b; 

    public B() { 
     super(0); 
    } 

    @Override 
    public int getA() { 
     return b++; 
    } 
} 

は技術的には、あなたがAから継承されたフィールドを変更していないが、不変オブジェクトでは、同じゲッターの繰り返し呼び出しはもちろん、同じ番号を生成すると予想されて、その:このように

+0

権限には疑問がありますが、ここではJoshua Blochについて話しています。 – duffymo

+3

不変クラスとはどういう意味ですか?そのインスタンスが不変でなければならないか、このタイプにキャストできるオブジェクト(サブクラスからインスタンス化されたオブジェクトなど)ですか? 2番目のケースでは、サブクラスBを作成するだけで十分です。サブクラスBには、設定可能な新しいプロパティbがあります。その後、Bのインスタンスは変更可能です。 –

+0

Brian Goetzの* Java Concurrency in Practice *をお勧めします。 Chapter 3(または4?)を読んだ後に、不変のスレッドセーフなオブジェクトのフィールドが最終的であるべき理由が明確になります。プレビューとして、以下の私の答えを見てください。 –

答えて

6

ここではそうではありません。

もちろん、ルール1に固執すると、このオーバーライドを作成することはできません。しかし、他の人々がその規則に従うことは確実ではありません。メソッドの1つがAをパラメータとして受け取り、getA()を呼び出すと、他の人が上記のようにクラスBを作成し、そのインスタンスをメソッドに渡すことができます。その後、あなたのメソッドは、それを知らずに、オブジェクトを変更します。

+0

しかし、私は私的に 'a'を宣言していることに注意してください。 Bは 'a'へのアクセス権を持っていません – vinoth

+0

@cmv:*テーブルの頭を強打する* - あなたはまったく正しいです。コードを変更しました。 –

+0

@Aasmundあなたのソリューションはコンパイルされません。代わりにBのコンストラクタを定義していないので、super()はBから呼び出されています.Aにはデフォルトのコンストラクタがないのでコンパイルできません。 –

4

Liskov置換原則によれば、サブクラスはスーパークラスのどこにでも使用できます。クライアントの視点から見ると、子供はIS-Aの親です。

子のメソッドをオーバーライドして変更可能にすると、それが不変であると予想される親のクライアントとの契約に違反しています。

+1

私の質問は、親クラスにプライベート属性だけが含まれている場合、親クラスはセッターを提供しないので、子クラスはその値を変更できません。 – vinoth

+0

これは、シリアライズとデシリアライズを使用しても実行できます。そして、あなたは究極の悪を使用し、反射を使用することができます。 – duffymo

+0

うーん..直列化..それについて考えないでください。 – vinoth

0

クラスが拡張されている場合、派生クラスは不変ではない可能性があります。

クラスが不変の場合、すべてのフィールドは作成後に変更されません。最終的なキーワードは、これを強制し、将来のメンテナに明白にするでしょう。

3

フィールドを宣言した場合は、フィールドを変更しようとするとコンパイル時エラーになるか、初期化されないままにするよりも多くのことがあります。

クラスAのインスタンスをデータレース(すなわち、同期のない状態で、つまり静的フィールドなどのグローバルに利用可能な場所に格納する)で共有する場合、マルチスレッドコードでは、いくつかのスレッドはgetA()の値を変更します!

Finalフィールドがあっても、同期せずに、コンストラクタが終了した後、すべてのスレッドから見えるの値を持つ(JVM specsによって)保証されています。

これら2つのクラスを考えてみましょう:

final class A { 
    private final int x; 
    A(int x) { this.x = x; } 
    public getX() { return x; } 
} 

final class B { 
    private int x; 
    B(int x) { this.x = x; } 
    public getX() { return x; } 
} 

ABどちらも、あなたは(の反射を忘れてみましょう)初期化後にフィールドxの値を変更することができないという意味では、不変です。唯一の違いは、フィールドxにはAにfinalというマークが付けられていることです。この小さな違いの大きな影響をすぐに実感できます。

は今、次のコードを考えてみます。

class Main { 
    static A a = null; 
    static B b = null; 
    public static void main(String[] args) { 
    new Thread(new Runnable() { void run() { try { 
     while (a == null) Thread.sleep(50); 
     System.out.println(a.getX()); } catch (Throwable t) {} 
    }}).start() 
    new Thread(new Runnable() { void run() { try { 
     while (b == null) Thread.sleep(50); 
     System.out.println(b.getX()); } catch (Throwable t) {} 
    }}).start() 
    a = new A(1); b = new B(1); 
    } 
} 

は、両方のスレッドは、メインスレッドが、この仮定は些細に見えるかもしれませんが、彼らは(なお、設定した後、彼らが見ているフィールドがnullではないことを確認するために起こると仮定しますJVMでは保証されていません)。そう、コンストラクタが終了した後に、それはすべてのスレッドのオブジェクトを参照してくださいということが保証されている - この場合

、我々はaを監視スレッドがそのxフィールドがfinalであるため、値1を印刷することを確認することができますxの正しい値が表示されます。

しかし、他のスレッドが何をするかはわかりません。仕様では、0または1のいずれかしか印刷されないことが保証されています。フィールドはfinalではないため、同期化(​​またはvolatile)を使用していないため、スレッドは初期化されていないフィールドを参照して0を出力する可能性があります。もう1つの可能性は、実際にフィールドが初期化されていることを確認し、1を印刷することです。他の値は印刷できません。

また、何が起こるかもしれませんが、あなたがbgetX()の値を読み取り、印刷し続けるならば、それは0を印刷するしばらく1の印刷を開始することができ、ということです!この場合、不変オブジェクトのフィールドがfinalでなければならない理由は明らかです:セッターを提供しないことで不変であると思われる場合でも、第2スレッドの観点からはbが変更されました!

2番目のスレッドがフィールドfinalせずにxの正しい値を確認することを保証したい場合は、揮発性Bのインスタンスを保持するフィールド宣言することができます:

class Main { 
    // ... 
    volatile static B b; 
    // ... 
} 

を他の可能性があります

final class B { 
    private int x; 
    private synchronized setX(int x) { this.x = x; } 
    public synchronized getX() { return x; } 
    B(int x) { setX(x); } 
} 

又は、メインのコードを変更するときFに同期を追加することによって:、いずれかのクラスBを変更することによって設定し、フィールドを読み取るときに同期させる場合bが読み込まれ、書かれているときは、両方の操作が同じオブジェクト上で同期しなければならないことに注意してください!

ご覧のとおり、最もエレガントで信頼性が高く、実行可能なソリューションは、フィールドxを最終的にすることです。最後の注意、として


それは最終すべてのフィールドを持っている不変、スレッド・セーフ・クラスのために絶対に必要というわけではありません。ただし、これらのクラス(スレッドセーフ、不変、非最終フィールドを含む)は、細心の注意を払って設計する必要があり、専門家のために残す必要があります。

この例は、クラスjava.lang.Stringです。それは最終的ではありませんprivate int hash;フィールドを、持っている、とhashCode()のためにキャッシュとして使用されます。

private int hash; 
public int hashCode() { 
    int h = hash; 
    int len = count; 
    if (h == 0 && len > 0) { 
    int off = offset; 
    char val[] = value; 
    for (int i = 0; i < len; i++) 
     h = 31*h + val[off++]; 
    hash = h; 
    } 
    return h; 
} 

あなたが見ることができるように、のhashCode()メソッドは、最初の(非最終)を読み込むフィールドhash 。初期化されていない場合(つまり、0の場合)、値を再計算して設定します。ハッシュコードを計算してフィールドに書き込んだスレッドの場合、その値は永久に保持されます。

しかし、他のスレッドは、スレッドがそれを他の何かに設定した後でも、そのフィールドにはまだ0が表示されることがあります。この場合、これらの他のスレッドは、ハッシュを再計算し、正確に同じ値を取得してから設定します。

ここで、クラスの不変性とスレッドセーフティを正当化する理由は、最終的なフィールドにキャッシュされていなくても、すべてのスレッドがhashCode()に対して正確に同じ値を取得することです。まったく同じ値が得られます。 この理由は非常に微妙なので、は、すべてのフィールドが不変のスレッドセーフクラスでfinalとマークされていることをお勧めします。

0

この回答を追加すると、不変なクラスでスレッドセーフであるためにメンバ変数をfinalにする必要がある理由が記載されています(the exact section of the JVM specを参照)。再び

class FinalFieldExample { 
    final int x; 
    int y; 
    static FinalFieldExample f; 

    public FinalFieldExample() { 
     x = 3; 
     y = 4; 
    } 

    static void writer() { 
     f = new FinalFieldExample(); 
    } 

    static void reader() { 
     if (f != null) { 
      int i = f.x; // guaranteed to see 3 
      int j = f.y; // could see 0 
     } 
    } 
} 

、スペックから:

クラスFinalFieldExampleが最終int型のフィールドxと非最終int型のフィールドを持っているここで私は非常に明確だと思うスペックで使用される例は、ですy。 1つのスレッドがメソッドライターを実行し、別のスレッドがメソッドリーダーを実行する可能性があります。

オブジェクトのコンストラクタが終了した後にwriterメソッドがfを書き込むため、readerメソッドはf.xの適切な初期化値を保証します。したがって、リーダメソッドは、値4を保証するものではありません。

関連する問題