なぜ不変のクラスが必要なのですか?


83

不変のクラスが必要なシナリオを理解できません。
そのような要件に直面したことがありますか?または、このパターンを使用する必要がある実際の例を教えてください。


13
誰もこれを重複と呼んでいないことに驚いています。皆さんは簡単なポイントを獲得しようとしているようです...:)(1)stackoverflow.com/questions/1284727/mutable-or-immutable-class(2)stackoverflow.com/questions/3162665/immutable-class( 3)stackoverflow.com/questions/752280/...
Javid Jamae

回答:


82

他の答えは、不変性が優れている理由を説明することに焦点を合わせすぎているようです。とても良いですし、可能な限り使っています。 しかし、それはあなたの質問ではありません。必要な答えと例が確実に得られるように、質問を1つずつ取り上げます。

不変のクラスが必要なシナリオを理解できません。

「必要」はここでは相対的な用語です。不変クラスは、他のパラダイム/パターン/ツールと同様に、ソフトウェアの構築を容易にするために存在するデザインパターンです。同様に、オブジェクト指向パラダイムが登場する前にたくさんのコードが書かれていましたが、オブジェクト指向を「必要とする」プログラマーの中に私を数えてください。OOのような不変のクラスは厳密には必要ありません、私はそれらが必要なように行動します。

そのような要件に直面したことがありますか?

問題のあるドメイン内のオブジェクトを正しい視点で見ていない場合、不変オブジェクトの要件が表示されない可能性があります。問題のあるドメインはいつ有利に使用するかをよく知らない場合、不変のクラスを必要としない考えるのは簡単かもしれません。

問題のあるドメイン内の特定のオブジェクトを値または固定インスタンスと見なす不変クラスをよく使用します。この概念は、パースペクティブまたはパースペクティブに依存する場合がありますが、理想的には、適切なパースペクティブに切り替えて、適切な候補オブジェクトを識別するのは簡単です。

さまざまな本やオンライン記事を読んで、不変クラスについての考え方をよく理解することで、不変オブジェクトが本当に役立つ場所をよりよく理解できます(厳密に必要ではない場合)。始めるための良い記事の1つは、Javaの理論と実践です。変更するかどうか。

パースペクティブの意味を明確にするために、さまざまなパースペクティブ(可変と不変)でオブジェクトを表示する方法の例をいくつか以下に示します。

...このパターンを使用する実際の例を教えてください。

あなたが実際の例を求めたので、私はあなたにいくつかを与えるでしょう、しかし最初に、いくつかの古典的な例から始めましょう。

クラシック値オブジェクト

文字列と整数は、多くの場合、値と見なされます。したがって、StringクラスとIntegerラッパークラス(および他のラッパークラス)がJavaで不変であることに気付くのは当然のことです。通常、色は値、つまり不変のColorクラスと見なされます。

反例

対照的に、車は通常、価値のあるオブジェクトとは見なされません。車のモデリングとは、通常、状態(走行距離計、速度、燃料レベルなど)が変化するクラスを作成することを意味します。ただし、車が値オブジェクトになる可能性のあるドメインがいくつかあります。たとえば、車(または具体的には車のモデル)は、特定の車両に適切なモーターオイルを検索するためのアプリの値オブジェクトと見なされる場合があります。

トランプ

トランププログラムを書いたことはありますか?やった。トランプを、可変のスーツとランクを持つ可変のオブジェクトとして表すこともできます。ドローポーカーハンドは5つの固定インスタンスである可能性があり、私の手札の5番目のカードを交換すると、スーツとランクのivarを変更して、5番目のトランプインスタンスを新しいカードに変更することになります。

しかし、私はトランプを、一度作成されると固定された不変のスーツとランクを持つ不変のオブジェクトと考える傾向があります。私のドローポーカーハンドは5インスタンスであり、私のハンドのカードを交換するには、それらのインスタンスの1つを破棄し、新しいランダムインスタンスを私のハンドに追加する必要があります。

地図投影法

最後の例は、マップがさまざまな投影法で表示されるマップコードを作成したときです。元のコードでは、マップで固定されているが変更可能なプロジェクションインスタンス(上記の変更可能なトランプなど)が使用されていました。地図投影法を変更するということは、地図の投影インスタンスのivar(投影法の種類、中心点、ズームなど)を変更することを意味します。

ただし、プロジェクションを不変の値または固定インスタンスと考えると、デザインはよりシンプルだと感じました。地図投影法を変更するということは、地図の固定投影法インスタンスを変更するのではなく、地図に別の投影法インスタンスを参照させることを意味しました。これにより、などの名前付きプロジェクションのキャプチャも簡単になりましたMERCATOR_WORLD_VIEW


42

不変クラスは、一般に、正しく設計、実装、および使用するのがはるかに簡単です。例は文字列です。主にその不変性のために、の実装はC ++の実装java.lang.Stringよりも大幅に単純ですstd::string

不変性が特に大きな違いを生む1つの特定の領域は並行性です。不変オブジェクトは複数のスレッド間で安全に共有できますが、可変オブジェクトは注意深い設計と実装によってスレッドセーフにする必要があります。通常、これは簡単な作業ではありません。

更新: Effective Java 2nd Editionは、この問題に詳細に取り組んでいます-項目15:可変性の最小化を参照してください。

これらの関連記事も参照してください。


39

Joshua Blochによる効果的なJavaは、不変のクラスを作成するいくつかの理由を概説しています。

  • シンプルさ-各クラスは1つの状態のみです
  • スレッドセーフ-状態を変更できないため、同期は必要ありません
  • 不変のスタイルで書くと、より堅牢なコードにつながる可能性があります。文字列が不変ではなかったと想像してみてください。文字列を返したgetterメソッドでは、文字列が返される前に防御コピーを作成する必要があります。そうしないと、クライアントが誤ってまたは悪意を持ってオブジェクトの状態を壊す可能性があります。

一般に、結果として深刻なパフォーマンスの問題が発生しない限り、オブジェクトを不変にすることをお勧めします。このような状況では、可変ビルダーオブジェクトを使用して、StringBuilderなどの不変オブジェクトを構築できます。


1
各クラスは1つの状態にありますか、それとも各オブジェクトですか?
TusharThakur19年

@TusharThakur、各オブジェクト。
ジョーカー

17

ハッシュマップは典型的な例です。マップのキーは不変であることが不可欠です。キーが不変ではなく、hashCode()が新しい値になるようにキーの値を変更すると、マップが壊れます(キーがハッシュテーブルの間違った場所にあるようになりました)。


3
むしろ、キーを変更しないことが不可欠だと言いたいです。ただし、不変であるという公式の要件はありません。
ペーテルトロク2010

「公式」とはどういう意味かわかりません。
Kirk Woll 2010

1
彼が意味していると思うのは、唯一の本当の要件は、キーを変更してはならないということです...(不変性を介して)キーをまったく変更できないということではありません。もちろん、キーが変更されないようにする簡単な方法は、そもそもキーを不変にすることです。:-)
プラチナアズール

2
例:download.oracle.com/javase/6/docs/api/java/util/Map.html:「注:可変オブジェクトをマップキーとして使用する場合は、細心の注意を払う必要があります」。つまり、可変オブジェクトをキーとして使用できます。
ペーテルトロク2010

8

Javaは事実上すべての参照です。インスタンスが複数回参照される場合があります。このようなインスタンスを変更すると、そのすべての参照に反映されます。堅牢性とスレッドセーフを向上させるために、これが必要ない場合もあります。次に、不変のクラスが役立つので、新しいインスタンスを作成して現在の参照に再割り当てする必要があります。このようにして、他の参照の元のインスタンスは変更されません。

String変更可能である場合、Javaがどのように見えるか想像してみてください。


12
または、DateCalendarが変更可能だった場合。ああ、待って、彼らは、OH SH
gustafc 2010

1
@gustafc:日付の計算を行う仕事を考えると、カレンダーを変更可能にしても問題ありません(コピーを返すことで実行できますが、カレンダーの重量を考慮すると、この方法の方が適しています)。しかし、日付-はい、それは厄介です。
Michael Borgwardt 2010

一部のJRE実装でStringは、変更可能です。(ヒント:いくつかの古いJRockitバージョン)。string.trim()を呼び出すと、元の文字列がトリミングされました
Salandur 2010

6
@Salandur:それは「JREの実装」ではありません。これは、JREに似たものの実装ですが、そうではありません。
マークピーターズ

@Mark Peters:非常に真実の声明
Salandur 2010

6

不変のクラス自体は必要ありません、特に複数のスレッドが関係している場合は、プログラミングタスクを簡単にすることができます。不変オブジェクトにアクセスするためにロックを実行する必要はありません。そのようなオブジェクトについてすでに確立している事実は、今後も当てはまります。


6

極端な場合を考えてみましょう:整数定数。「x = x + 1」のようなステートメントを書く場合、プログラムの他の場所で何が起こっても、「1」という数字がどういうわけか2にならないことを100%確信したいと思います。

さて、整数定数はクラスではありませんが、概念は同じです。私が書いたとしましょう:

String customerId=getCustomerId();
String customerName=getCustomerName(customerId);
String customerBalance=getCustomerBalance(customerid);

簡単そうに見えます。ただし、文字列が不変でない場合は、getCustomerNameがcustomerIdを変更する可能性を考慮する必要があります。これにより、getCustomerBalanceを呼び出すと、別の顧客の残高が得られます。「なぜ、getCustomerName関数を作成すると、IDが変更されるのでしょうか?それは意味がありません」と言うかもしれません。しかし、それはまさにあなたが問題を起こす可能性がある場所です。上記のコードを書いている人は、関数がパラメーターを変更しないことは明らかだと思うかもしれません。次に、顧客が同じ名前で複数のアカウントを持っている場合を処理するために、その関数の別の使用法を変更する必要がある誰かがやって来ます。そして彼は、「ああ、これはすでに名前を検索しているこの便利なgetCustomername関数です。私は」と言います。

不変性とは、特定のクラスのオブジェクトが定数であることを意味し、それらを定数として扱うことができます。

(もちろん、ユーザーは別の「定数オブジェクト」を変数に割り当てることができます。誰かがString s = "hello"を記述し、後でs = "goodbye"を記述できます。変数をfinalにしない限り、確信が持てません。自分のコードブロック内で変更されていないこと。整数定数と同じように、「1」は常に同じ数ですが、「x = 1」を「x = 2」と書くことによって変更されることはありません。不変オブジェクトへのハンドルがある場合、それを渡す関数がそれを変更できないこと、またはオブジェクトのコピーを2つ作成した場合、1つのコピーを保持する変数を変更しても変更されないことを確信できます。その他。


5

不変性にはさまざまな理由があります。

  • スレッドセーフ:不変オブジェクトは変更できず、内部状態も変更できないため、同期する必要はありません。
  • また、(ネットワークを介して)送信するものはすべて、以前に送信したものと同じ状態になる必要があることも保証されます。これは、誰も(盗聴者)が来て、私の不変のセットにランダムなデータを追加できないことを意味します。
  • 開発も簡単です。オブジェクトが不変である場合、サブクラスが存在しないことを保証します。たとえば、Stringクラス。

したがって、ネットワークサービスを介してデータを送信する場合で、送信した結果とまったく同じ結果が得られるという保証が必要な場合は、不変として設定してください。


ネットワークを介した送信についての部分も、不変クラスを拡張できないとあなたが言う部分もわかりません。
aioobe 2010

@aioobe、例えばなどのWebサービス、RMI、ネットワークを介してデータを送信し、拡張のために、あなたは不変のStringクラスを拡張することができない、またはその他のImmutableSet、ImmutableList、
Buhake Sindi

String、ImmutableSet、ImmutableListなどを拡張できないことは、不変性と完全に直交する問題です。finalJavaでフラグが立てられたすべてのクラスが不変であるわけではなく、すべての不変クラスがフラグが立てられてfinalいるわけではありません。
ちょうど私の正しい意見2010

5

これを別の視点から攻撃します。不変のオブジェクトを使用すると、コードを読むときに作業が楽になります。

可変オブジェクトがある場合、それが私の直接の範囲外で使用された場合、その値が何であるかはわかりません。MyMutableObjectメソッドのローカル変数で作成し、値を入力してから、他の5つのメソッドに渡すとします。これらのメソッドのいずれかがオブジェクトの状態を変更する可能性があるため、次の2つのいずれかが発生する必要があります。

  1. コードのロジックについて考えながら、5つの追加メソッドの本体を追跡する必要があります。
  2. 正しい値が各メソッドに渡されるようにするには、オブジェクトの無駄な防御コピーを5つ作成する必要があります。

最初のものは私のコードについての推論を難しくします。2つ目は、コードのパフォーマンスを低下させます。基本的には、コピーオンライトセマンティクスを使用して不変オブジェクトを模倣していますが、呼び出されたメソッドが実際にオブジェクトの状態を変更するかどうかに関係なく、常に実行しています。

代わりにを使用する場合MyImmutableObject、設定するのは、メソッドの存続期間中の値であると確信できます。私の下からそれを変える「遠隔作用」はなく、他の5つのメソッドを呼び出す前にオブジェクトの防御コピーを作成する必要もありません。他のメソッドが目的のため物事を変更したい場合は、コピーを作成する必要がありますが、実際にコピーを作成する必要がある場合にのみこれを行います(すべての外部メソッド呼び出しの前に行うのとは対照的です)。現在のソースファイルに含まれていない可能性のあるメソッドを追跡するという精神的なリソースを節約し、万が一の場合に備えて、システムに不要な防御コピーを際限なく作成するオーバーヘッドを節約します。

(Javaの世界の外に出て、たとえばC ++の世界に入ると、さらにトリッキーになる可能性があります。オブジェクトを変更可能であるかのように見せることはできますが、舞台裏では、オブジェクトを透過的に複製します。ある種の状態変化、つまりコピーオンライトであり、誰も賢くはありません。)


それらをサポートし、それらへの永続的な参照を許可しない言語での可変値型の1つの利点は、説明する利点が得られることです(可変参照型がルーチンへの参照によって渡される場合、そのルーチンは実行中にそれを変更できます、ただし、不変オブジェクトがしばしば持つ不格好さなしに、戻った後に変更を発生させることはできません)。
スーパーキャット2012

5

将来の訪問者のための私の2セント:


不変オブジェクトが適切な選択である2つのシナリオは次のとおりです。

マルチスレッドの場合

マルチスレッド環境での同時実行の問題は同期によって非常によく解決できますが、同期にはコストがかかるため(ここでは「理由」については掘り下げません)、不変オブジェクトを使用している場合、同時実行の問題を解決するための同期はありません。不変オブジェクトは変更できません。状態を変更できない場合は、すべてのスレッドがオブジェクトにシームレスにアクセスできます。したがって、不変オブジェクトは、マルチスレッド環境の共有オブジェクトに最適です。


ハッシュベースのコレクションのキーとして

ハッシュベースのコレクションを操作するときに注意する最も重要なことの1つは、キーはhashCode()オブジェクトの存続期間中常に同じ値を返すようにする必要があるということです。その値が変更されると、ハッシュベースのコレクションに古いエントリが作成されるためです。そのオブジェクトを使用すると取得できないため、メモリリークが発生します。不変オブジェクトの状態は変更できないため、ハッシュベースのコレクションのキーとして最適です。したがって、ハッシュベースのコレクションのキーとして不変オブジェクトを使用している場合は、そのためにメモリリークが発生しないことを確認できます(もちろん、キーとして使用されているオブジェクトがどこからも参照されていない場合でも、メモリリークが発生する可能性があります)それ以外の場合、しかしそれはここでのポイントではありません)。


2

不変オブジェクトは、一度開始されると状態が変化しないインスタンスです。このようなオブジェクトの使用は、要件固有です。

不変クラスはキャッシュの目的に適しており、スレッドセーフです。


2

不変性のおかげで、基礎となる不変オブジェクトの動作/状態が変化しないことを確認でき、追加の操作を実行するという追加の利点が得られます。

  • 複数のコア/処理(並行/並列処理)を簡単に使用できます(操作の順序は重要ではなくなります)。

  • コストのかかる操作のキャッシュを実行できます(同じ
    結果が確実に得られるため)。

  • デバッグを簡単に行うことができます(実行の履歴は
    もう問題にならないため)


1

finalキーワードを使用しても、必ずしも不変になるとは限りません。

public class Scratchpad {
    public static void main(String[] args) throws Exception {
        SomeData sd = new SomeData("foo");
        System.out.println(sd.data); //prints "foo"
        voodoo(sd, "data", "bar");
        System.out.println(sd.data); //prints "bar"
    }

    private static void voodoo(Object obj, String fieldName, Object value) throws Exception {
        Field f = SomeData.class.getDeclaredField("data");
        f.setAccessible(true);
        Field modifiers = Field.class.getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(f, f.getModifiers() & ~Modifier.FINAL);
        f.set(obj, "bar");
    }
}

class SomeData {
    final String data;
    SomeData(String data) {
        this.data = data;
    }
}

「final」キーワードがプログラマーのエラーを防ぐためにあることを示すための単なる例であり、それ以上のものではありません。最後のキーワードがない値の再割り当ては偶然に簡単に発生する可能性がありますが、値を変更するためにこの長さにすることは意図的に行う必要があります。ドキュメントとプログラマーのエラーを防ぐためにあります。


これは最終的なものではなかったという点で、質問に直接答えるものではないことに注意してください。それは不変のクラスについてでした。適切なアクセス制限を使用して、finalキーワードなしで不変のクラスを持つことができます。
マークピーターズ

1

不変のデータ構造は、再帰的アルゴリズムをコーディングするときにも役立ちます。たとえば、3SAT問題を解決しようとしているとします。1つの方法は、次のことを行うことです。

  • 割り当てられていない変数を選択します。
  • それにTRUEの値を与えます。現在満たされている句を取り出してインスタンスを単純化し、繰り返してより単純なインスタンスを解決します。
  • TRUEの場合の再帰が失敗した場合は、代わりにその変数にFALSEを割り当てます。この新しいインスタンスを単純化し、繰り返して解決します。

問題を表す可変構造がある場合、TRUEブランチでインスタンスを単純化するときは、次のいずれかを行う必要があります。

  • 行ったすべての変更を追跡し、問題を解決できないことに気付いたら、すべて元に戻します。再帰がかなり深くなる可能性があり、コーディングが難しいため、これには大きなオーバーヘッドがあります。
  • インスタンスのコピーを作成してから、コピーを変更します。再帰が数十レベルの深さである場合、インスタンスのコピーを多数作成する必要があるため、これは遅くなります。

ただし、巧妙な方法でコーディングすると、不変の構造を持つことができます。この構造では、操作によって問題の更新された(ただし不変の)バージョンが返されます(同様にString.replace、文字列は置き換えられず、新しいものが提供されます)。 )。これを実装する素朴な方法は、「不変」構造をコピーして、変更を加えたときに新しい構造を作成し、変更可能な構造がある場合は2番目のソリューションに減らし、すべてのオーバーヘッドを伴うことですが、もっと多くのことでそれを行うことができます効率的な方法。


1

不変クラスが「必要」である理由の1つは、すべてを参照で渡すことと、オブジェクト(つまり、C ++ const)の読み取り専用ビューをサポートしていないことの組み合わせです。

オブザーバーパターンをサポートするクラスの単純なケースを考えてみましょう。

class Person {
    public string getName() { ... }
    public void registerForNameChange(NameChangedObserver o) { ... }
}

string不変でなければ、PersonクラスがregisterForNameChange()正しく実装することは不可能です。誰かが次のように記述して、通知をトリガーせずにその人の名前を効果的に変更できるからです。

void foo(Person p) {
    p.getName().prepend("Mr. ");
}

C ++では、getName()aconst std::string&を返すと、参照によって返され、ミューテーターへのアクセスが防止されます。つまり、そのコンテキストでは不変のクラスは必要ありません。



1

まだ呼び出されていない不変クラスの1つの機能:深く不変のクラスオブジェクトへの参照を格納することは、そこに含まれるすべての状態を格納する効率的な手段です。深く不変のオブジェクトを使用して50K相当の状態情報を保持する可変オブジェクトがあるとします。さらに、25回、元の(変更可能な)オブジェクトの「コピー」を作成したいとします(たとえば、「元に戻す」バッファの場合)。状態はコピー操作間で変化する可能性がありますが、通常は変化しません。可変オブジェクトの「コピー」を作成するには、参照をその不変状態にコピーするだけでよいため、20個のコピーは単純に20個の参照になります。対照的に、状態が50K相当の可変オブジェクトで保持されている場合、25回のコピー操作のそれぞれで50K相当のデータの独自のコピーを生成する必要があります。25部すべてを保持するには、1メガ分のほとんど複製されたデータを保持する必要があります。最初のコピー操作は決して変更されないデータのコピーを生成し、他の24の操作は理論的には単にそれを参照することができますが、ほとんどの実装では、2番目のオブジェクトがコピーを要求する方法はありません。不変のコピーがすでに存在することを知るための情報(*)。

(*)時々役立つパターンの1つは、可変オブジェクトがその状態を保持する2つのフィールドを持つことです。1つは可変形式で、もう1つは不変形式です。オブジェクトは、可変または不変としてコピーでき、いずれかの参照セットから始まります。オブジェクトが状態を変更しようとするとすぐに、不変の参照を可変の参照にコピーし(まだ行われていない場合)、不変の参照を無効にします。オブジェクトが不変としてコピーされるときに、その不変の参照が設定されていない場合、不変のコピーが作成され、不変の参照がそれを指します。このアプローチでは、「書き込み時の本格的なコピー」よりもいくつかのコピー操作が必要になります(たとえば、最後のコピー以降に変更されたオブジェクトのコピーを要求すると、コピー操作が必要になります。


1

なぜ不変クラスなのか?

オブジェクトがインスタンス化されると、その状態は存続期間中に変更できません。これにより、スレッドセーフにもなります。

例:

明らかに、文字列、整数、BigDecimalなど。これらの値が作成されると、存続期間中は変更できません。

ユースケース:データベース接続オブジェクトがその構成値で作成されると、不変クラスを使用できる状態を変更する必要がない場合があります


0

効果的なJavaから; 不変クラスは、インスタンスを変更できないクラスです。各インスタンスに含まれるすべての情報は、インスタンスが作成されたときに提供され、オブジェクトの存続期間中固定されます。Javaプラットフォームライブラリには、String、ボックス化されたプリミティブクラス、BigIntegerおよびBigDecimalなど、多くの不変クラスが含まれています。これには多くの理由があります。不変クラスは、可変クラスよりも設計、実装、および使用が簡単です。エラーが発生しにくく、安全性が高くなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.