Java 8に不変のコレクションが含まれないのはなぜですか?


130

Javaチームは、Java 8の関数型プログラミングの障壁を取り除くために多大な努力をしてきました。特に、java.utilコレクションの変更は、変換を非常に高速なストリーム操作に連鎖させるという素晴らしい仕事をしています。彼らがコレクションにファーストクラスの関数と機能メソッドを追加するのをどれだけうまくやったかを考えると、なぜ彼らは不変のコレクションや不変のコレクションのインターフェースを提供することに完全に失敗したのでしょうか?

既存のコードを変更せずに、Javaチームはいつでも変更可能なインターフェイスと同じ不変インターフェイスを追加し、「set」メソッドを削除して、既存のインターフェイスを次のように拡張できます。

                  ImmutableIterable
     ____________/       |
    /                    |
Iterable        ImmutableCollection
   |    _______/    /          \   \___________
   |   /           /            \              \
 Collection  ImmutableList  ImmutableSet  ImmutableMap  ...
    \  \  \_________|______________|__________   |
     \  \___________|____________  |          \  |
      \___________  |            \ |           \ |
                  List            Set           Map ...

確かに、List.add()やMap.put()などの操作は現在、指定されたキーのブール値または前の値を返し、操作が成功したか失敗したかを示します。不変のコレクションは、そのようなメソッドをファクトリーとして扱い、追加された要素を含む新しいコレクションを返す必要があります-これは現在の署名と互換性がありません。ただし、ImmutableList.append()または.addAt()およびImmutableMap.putEntry()などの別のメソッド名を使用することで回避できます。結果として得られる冗長性は、不変のコレクションを操作する利点よりも重要であり、型システムは間違ったメソッドを呼び出すエラーを防ぎます。時間が経つにつれて、古いメソッドは廃止される可能性があります。

不変コレクションの勝利:

  • シンプルさ-基礎となるデータが変更されない場合、コードについての推論は簡単です。
  • ドキュメンテーション-メソッドが不変のコレクションインターフェイスを取る場合、そのコレクションを変更しないことを知っています。メソッドが不変のコレクションを返す場合、変更できないことを知っています。
  • 並行性-不変コレクションはスレッド間で安全に共有できます。

不変性を前提とする言語を味わった人として、Wild延する突然変異のワイルドウエストに戻ることは非常に困難です。Clojureのコレクション(シーケンス抽象化)には、Java 8コレクションが提供するすべてのものと、不変性(ストリームの代わりに同期されたリンクリストのために余分なメモリと時間を使用する可能性があります)が既にあります。Scalaには、操作の完全なセットを備えた可変コレクションと不変コレクションの両方があり、それらの操作は熱心ですが、.iteratorを呼び出すと遅延ビューが得られます(また、それらを遅延評価する他の方法があります)。不変のコレクションがなければ、Javaがどのように競争し続けることができるかわかりません。

誰かがこれに関する歴史や議論を教えてくれますか?確かにどこかで公開されています。


9
これに関連して-Ayendeは最近、C#のコレクションと不変コレクションについてベンチマークでブログを作成しました。ayende.com/blog/tags/performance-tl ; dr-不変性は遅いです。
オデッド

20
あなたがあるとしてだけ持って、あなたの階層で私はあなたにImmutableListを与えることができ、その後、あなたはたくさんのことを破ることができることを期待していないときにそれを変更constコレクション
ラチェットフリーク

18
@Oded不変性は遅いですが、ロックも同様です。歴史を維持しています。多くの場合、シンプルさ/正確さはスピードの価値があります。小さなコレクションでは、速度は問題になりません。Ayendeの分析は、履歴、ロック、シンプルさを必要とせず、大規模なデータセットで作業しているという仮定に基づいています。時々それは本当ですが、それは常に良いものではありません。トレードオフがあります。
グレンペターソン

5
@GlenPetersonそれCollections.unmodifiable*()は防御的なコピーであり、その目的です。しかし、これらが不変の場合は不変として扱わないでください
ラチェットフリーク

13
え?あなたの関数がImmutableListその図に含まれている場合、人々は可変で渡すことができListますか?いいえ、それはLSPの非常に悪い違反です。
テラスティン

回答:


113

不変のコレクションを使用するには、共有が絶対に必要です。それ以外の場合、すべての単一の操作が他のリスト全体をヒープのどこかにドロップします。Haskellのように完全に不変の言語は、積極的な最適化と共有なしで驚くほどの量のゴミを生成します。<50の要素でのみ使用可能なコレクションを持つことは、標準ライブラリに入れる価値はありません。

さらに、不変のコレクションは、多くの場合、可変のコレクションとは根本的に異なる実装を持っています。たとえばArrayList、効率的な不変ArrayListは配列にはなりません!Clojureは32のIIRCを使用し、大きな分岐係数を持つバランスの取れたツリーで実装する必要があります。機能更新を追加するだけで可変コレクションを「不変」にすることは、メモリリークと同様にパフォーマンスのバグです。

さらに、共有はJavaでは実行できません。Javaは、可変性と参照の平等に対して無制限のフックを多数提供しているため、共有を「最適化」するだけです。リスト内の要素を変更できれば、リストの他の20のバージョンの要素を変更しただけであることに気づいたら、おそらく少し気に入らないでしょう。

これはまた、効率的な不変性、共有、ストリーム融合のための非常に重要な最適化の巨大なクラスを排除します。(FPエバンジェリストにとって良いスローガンになるでしょう)


21
私の例では、不変のインターフェイスについて説明しました。Javaは、必要なトレードオフを行うこれらのインターフェイスの可変および不変の両方の実装の完全なスイートを提供できます。必要に応じて可変または不変を選択するのはプログラマ次第です。プログラマは、リストとセットをいつ使用するかを知る必要があります。通常、パフォーマンスの問題が発生するまで可変バージョンは必要ありません。その後、ビルダーとしてのみ必要になる場合があります。いずれにせよ、不変のインターフェースを持つことは、それ自体が勝つでしょう。
グレンペターソン

4
私はあなたの答えをもう一度読みましたが、Javaには可変性の基本的な仮定(たとえば、Java Bean)があり、コレクションは氷山の一角にすぎず、そのひっくり返しは根本的な問題を解決しないと言っていると思います。有効なポイント。この答えを受け入れて、Scalaの採用をスピードアップするかもしれません!:
グレンペターソン

8
不変のコレクションが有用であるために共通部分を共有する能力を必要とするかどうかはわかりません。Javaで最も一般的な不変の型であり、不変の文字のコレクションであり、共有を許可するために使用されますが、現在は使用できません。便利なのは、データをからにすばやくコピーStringしてStringBuffer操作し、データを新しい不変にコピーできることですString。わずかに変更されたインスタンスの生産を促進するように設計されている不変の種類を使用して同じくらい良いかもしれませんが、それでも良いかもしれセットとリストと、そのようなパターンを使用して...
supercat

3
共有を使用してJavaで不変のコレクションを作成することは完全に可能です。コレクションに格納されているアイテムは参照であり、その参照対象は変更される可能性があります。このような動作により、HashMapやTreeSetなどの既存のコレクションは既に壊れていますが、これらはJavaで実装されています。また、複数のコレクションに同じオブジェクトへの参照が含まれる場合、オブジェクトを変更すると、すべてのコレクションから表示されたときに変更が表示されることが完全に予想されます。
ソロモノフの秘密

4
jozefg、JVMに効率的な不変コレクションを実装することは完全に可能です。ScalaとClojureには標準ライブラリの一部としてそれらがあり、どちらの実装もPhil BagwellのHAMT(Hash Array Mapped Trie)に基づいています。BALANCEDツリーで不変のデータ構造を実装するClojureに関するあなたの声明は、完全に間違っています。
sesm

78

可変コレクションは、不変コレクションのサブタイプではありません。代わりに、可変および不変のコレクションは、読み取り可能なコレクションの兄弟の子孫です。残念ながら、「読み取り可能」、「読み取り専用」、および「不変」の概念は、3つの異なることを意味しているにもかかわらず、一緒にぼやけているように見えます。

  • 読み取り可能なコレクションの基本クラスまたはインターフェイスタイプは、アイテムを読み取れることを約束し、コレクションを変更する直接的な手段を提供しませんが、参照を受け取るコードが変更を許可するような方法でそれをキャストまたは操作できないことを保証しません。

  • 読み取り専用のコレクションインターフェイスには新しいメンバーは含まれませんが、コレクションを変更したり、何かへの参照を受け取ったりするような方法で参照を操作する方法がないことを約束するクラスによってのみ実装する必要があります。そうすることができます。ただし、内部への参照を持つ他の何かによってコレクションが変更されることはありません。読み取り専用のコレクションインターフェイスは、可変クラスによる実装を防止できない可能性がありますが、任意の実装または実装から派生したクラスを指定して、突然変異を許可する「不正な」実装または実装の派生物と見なされることに注意してください。

  • 不変コレクションは、参照が存在する限り常に同じデータを保持するコレクションです。特定のリクエストに対して常に同じデータを返すとは限らない不変のインターフェースの実装はすべて壊れています。

強く関連する可変および不変コレクション実装したり、同じ「読める」タイプから派生し、読みやすいタイプを持つように、両方のタイプなどが持っていると便利な場合がありAsImmutableAsMutableおよびAsNewMutable方法を。このような設計により、コレクション内のデータを永続化するコードを呼び出すことができますAsImmutable。このメソッドは、コレクションが変更可能な場合は防御的なコピーを作成しますが、既に不変の場合はコピーをスキップします。


1
素晴らしい答え。不変のコレクションは、スレッドの安全性と、時間の経過とともにそれらについて推論する方法に関連する非常に強力な保証を提供できます。読み取り可能/読み取り専用のコレクションはそうではありません。実際、liskovのサブスティションの原則を尊重するために、Read-OnlyおよびImmutableはおそらくfinalメソッドとprivateメンバーを持つ抽象基本型にして、派生クラスが型によって与えられた保証を破壊できないようにする必要があります。または、コレクションをラップする(読み取り専用)、または常に防御的なコピーを取る(完全に変更可能な)完全に具体的なタイプである必要があります。これは、guava ImmutableListが行う方法です。
ローランブルゴーロイ

1
@ LaurentBourgault-Roy:シール型と継承型の不変型の両方に利点があります。不正な派生クラスが不変式を壊すことを許可したくない場合、継承型クラスは何も提供しない一方で、シール型はそれに対する保護を提供できます。一方、保持しているデータについて何かを知っているコードは、それについて何も知らない型よりもはるかにコンパクトに格納できる可能性があります。たとえば、intメソッドgetLength()とを使用して、のシーケンスをカプセル化するReadableIndexedIntSequenceタイプを考えgetItemAt(int)ます。
supercat

1
@ LaurentBourgault-Roy:が与えられたReadableIndexedIntSequence場合、すべてのアイテムを配列にコピーすることにより、配列に裏付けされた不変型のインスタンスを生成できますが、特定の実装が単に長さと((long)index*index)>>24各アイテムに対して16777216を返したとします。これは正当な不変の整数シーケンスですが、配列にコピーすることは時間とメモリの大きな浪費になります。
supercat

1
私は完全に同意します。私のソリューションは(ある程度まで)正確さを提供しますが、大規模なデータセットでパフォーマンスを得るには、永続的な構造と設計を最初から不変にする必要があります。ただし、小さなコレクションの場合は、不変のコピーを時々取得することで問題を回避できます。Scalaがさまざまなプログラムの分析を行い、インスタンス化されたリストの90%が10項目以下の長さであることがわかりました。
ローランブルゴーロイ

1
@ LaurentBourgault-Roy:基本的な問題は、壊れた実装や派生クラスを生成しないと人々を信頼するかどうかです。もしそうなら、インターフェース/基底クラスがasMutable / asImmutableメソッドを提供するなら、パフォーマンスを何桁も改善できるかもしれません(例えばasImmutable、上記で定義されたシーケンスのインスタンスを呼び出すコストと構築するコストを比較してください)不変の配列バックアップコピー]。そのような目的のために定義されたインターフェースを持つことは、アドホックなアプローチを使用しようとするよりもおそらく優れていると思います。IMHO、...最大の理由
supercat

15

Java Collections Frameworkは、java.util.Collectionsクラスの 6つの静的メソッドを使用して、コレクションの読み取り専用バージョンを作成する機能を提供します。

元の質問へのコメントで誰かが指摘したように、返されたコレクションは不変とは見なされない可能性があります。コレクションは変更できませんが(コレクションにメンバーを追加または削除できません)、コレクションによって参照される実際のオブジェクトオブジェクトタイプで許可されている場合は変更できます。

ただし、コードが単一のオブジェクトを返すか、オブジェクトの変更不可能なコレクションを返すかに関係なく、この問題は残ります。タイプがそのオブジェクトの変更を許可する場合、その決定はタイプの設計で行われ、JCFへの変更がそれをどのように変更できるかわかりません。不変性が重要な場合、コレクションのメンバーは不変型である必要があります。


4
ラッパーにラップされているものがすでに不変であるかどうかの指示が含まれていてimmutableList、渡されたコピーの周りに読み取り専用ラッパーを返すファクトリメソッドなどがある場合、変更不可能なコレクションの設計は大幅に強化されていました。リスト渡されたリストにない限りはすでに不変でした。そのようなユーザー定義型を作成するのは簡単ですが、1つの問題があります:joesCollections.immutableListによって返されるオブジェクトをコピーする必要がないことをメソッドが認識する方法がありませんfredsCollections.immutableList
supercat

8

これは非常に良い質問です。私は、Javaで記述され、世界中の何百万台ものコンピューターで毎日実行されているすべてのコードの中で、合計クロックサイクルの約半分が、コレクションの安全コピーを作成するだけで無駄にならなければならないというアイデアを楽しんでいます関数によって返されます。(これらのコレクションを作成してから数ミリ秒後にガベージコレクションを行います。)

Javaプログラマーの一部unmodifiableCollection()は、Collectionsクラスのメソッドファミリの存在を認識していますが、その中でも、多くの人は気にしません。

そして、私はそれらを責めることはできません:読み取り/書き込みのふりをしますがUnsupportedOperationException、その「書き込み」メソッドのいずれかの呼び出しを間違えた場合にスローするインターフェースは、非常に悪いことです!

今、風のインターフェイスCollection不足していることになるadd()remove()clear()の方法は、「ImmutableCollection」インターフェースではありません。「UnmodifiableCollection」インターフェースになります。実際のところ、不変は実装の性質であり、インターフェイスの特性ではないため、「ImmutableCollection」インターフェイスが存在することはありません。私は知っている、それは非常に明確ではありません。説明させてください。

誰かがそのような読み取り専用のコレクションインターフェイスを渡したとします。別のスレッドに渡すのは安全ですか?本当に不変のコレクションであることを確信している場合、答えは「はい」になります。残念ながら、それはインターフェースであるため、どのように実装されるのかわからないので、答えは「いいえ」でなければなりません。 (あなたが取得するものとCollections.unmodifiableCollection()同様)、別のスレッドが変更しているときに読み取りを試みると、破損したデータが読み取られます

したがって、基本的に説明したのは、「Immutable」ではなく「Unmodifiable」コレクションインターフェイスのセットです。「変更不可」とは、単に、そのようなインターフェースへの参照を持っている人が基礎となるコレクションを変更できないことを意味することを理解することが重要です。基礎となるコレクションは非常に変更可能です。あなたはそれを知らず、それを制御することもできません。

不変のコレクションを持つためには、それらはインターフェースではなくクラスでなければなりません!

これらの不変のコレクションクラスは最終的なものでなければならないので、そのようなコレクションへの参照が与えられたとき、あなたやそれへの参照を持つ他の人が何であれ不変のコレクションとして振る舞うことは確実です。それでやります。

したがって、java(またはその他の宣言的命令言語)でコレクションの完全なセットを使用するには、次のものが必要です。

  1. 変更不可能なコレクションインターフェイスのセット。

  2. 変更できないコレクションインターフェイスを拡張する、変更可能なコレクションインターフェイスのセット。

  3. 可変インターフェースを実装する可変コレクションクラスのセット。さらに、変更不可能なインターフェースも拡張します。

  4. 変更不可能なインターフェイスを実装する不変のコレクションクラスのセットですが、不変性を保証するために、ほとんどの場合クラスとして渡されます。

上記のすべてを楽しみのために実装しており、プロジェクトでそれらを使用していますが、それらは魅力のように機能します。

それらがjavaランタイムの一部ではない理由は、おそらくこれが多すぎる/複雑すぎる/理解しにくいと考えられていたためです。

個人的には、上記で説明したことでも十分ではないと思います。必要と思われるもう1つのことは、構造的な不変性のための一連の可変インターフェースとクラスです。(接頭辞「StructurallyImmutable」が長すぎるため、単に「Rigid」と呼ばれる場合があります。)


良い点。2つの詳細:1.不変コレクションには特定のメソッドシグネチャが必要です。具体的には(例としてListを使用):List<T> add(T t)-すべての「ミューテーター」メソッドは、変更を反映する新しいコレクションを返す必要があります。2.良くも悪くも、インターフェイスは多くの場合、署名に加えて契約を表します。Serializableはそのようなインターフェイスの1つです。同様に、同等のは、あなたが正しく実装する必要がcompareTo()正常に動作し、理想的と互換性を持つようにする方法をequals()してhashCode()
グレンペターソン

ああ、mutate-by-copyの不変性も考慮していませんでした。上で書いたことは、本当にメソッドのような単純な不変コレクションを指しadd()ます。しかし、ミューテーターメソッドを不変クラスに追加する場合、不変クラス返す必要があると思います。したがって、そこに潜んでいる問題がある場合、私はそれを見ません。
マイクナキス

実装は公開されていますか?私は今月前に尋ねるべきだった。とにかく、私のものは:github.com/GlenKPeterson/UncleJim
GlenPeterson

4
Suppose someone hands you such a read-only collection interface; is it safe to pass it to another thread?誰かが可変コレクションインターフェイスのインスタンスを渡したとします。メソッドを呼び出すのは安全ですか?実装が永久にループしたり、例外をスローしたり、インターフェースのコントラクトを完全に無視したりしないことはわかりません。不変コレクション専用の二重標準があるのはなぜですか?
ドーバル

1
可変インターフェイスに対するあなたの推論は間違っています。不変のインターフェイスの可変実装を作成すると、壊れてしまいます。承知しました。しかし、それはあなたが契約に違反しているのであなたのせいです。それをやめるだけです。SortedSet適合しない実装でセットをサブクラス化することでを壊すことと違いはありません。または、矛盾するパスを渡すことによってComparable。必要に応じて、ほとんど何でも壊すことができます。それが、@ Dovalが「二重標準」によって意味したものだと思います。
-maaartinus

2

不変のコレクションは、お互いに比べて深く再帰的であり、オブジェクトの同等性がsecureHashによるものである場合、不合理に非効率的ではありません。これは、マークルフォレストと呼ばれます。コレクションごと、またはソートされたマップの(自己バランスバイナリ)AVLツリーのように、それらの部分内にあることができます。

これらのコレクション内のすべてのJavaオブジェクトに一意のIDまたはハッシュするビット文字列がない限り、コレクションには独自に名前を付けるためにハッシュするものは何もありません。

例:4x1.6ghzラップトップでは、1つのハッシュサイクル(最大55バイト)に収まる最小サイズの1秒あたり20万のsha256を実行できます。1秒あたり200K / log(collectionSize)の新しいコレクションは、データの整合性と匿名のグローバルスケーラビリティが重要な場合に十分に高速です。


-3

パフォーマンス。本質的にコレクションは非常に大きくなる可能性があります。単一の要素を挿入するのではなく、1000個の要素を1001個の要素を持つ新しい構造にコピーするのは恐ろしいことです。

並行性。複数のスレッドを実行している場合、スレッドが開始されてから12時間前に渡されたバージョンではなく、コレクションの現在のバージョンを取得する場合があります。

ストレージ。マルチスレッド環境の不変オブジェクトを使用すると、ライフサイクルのさまざまな時点で「同じ」オブジェクトのコピーを数十個作成できます。CalendarまたはDateオブジェクトには関係ありませんが、10,000個のウィジェットのコレクションの場合、これはあなたを殺します。


12
不変コレクションは、Javaのような広範囲にわたる可変性のために共有できない場合にのみコピーを必要とします。不変のコレクションは、ロックを必要としないため、一般的に同時実行は簡単です。また、可視性のために、不変のコレクション(OCamlで共通)への可変参照を常に保持できます。共有により、アップデートは基本的に無料で行えます。可変構造の場合よりも対数的に割り当てを行うことができますが、更新時に、期限切れになった多くのサブオブジェクトをすぐに解放または再利用できるため、必ずしもメモリオーバーヘッドが高くなるとは限りません。
ジョンパーディ

4
カップルの問題。ClojureとScalaのコレクションはどちらも不変ですが、軽量のコピーをサポートしています。要素1001を追加すると、33個未満の要素をコピーし、さらにいくつかの新しいポインターを作成することになります。スレッド間で可変コレクションを共有する場合、変更時にあらゆる種類の同期の問題が発生します。「remove()」のような操作は悪夢です。また、不変のコレクションを変更可能に構築してから、スレッド間で安全に共有できる不変のバージョンに一度コピーすることもできます。
グレンペターソン

4
不変性に対する引数として並行性を使用することは珍しいです。複製も同様です。
トムホーティン-タックライン

4
ここでの反対票については少し不満がありました。OPは、なぜ不変のコレクションを実装していないのかを尋ね、私はその質問に対する考慮された答えを提供しました。おそらく、ファッションに敏感な人たちの間で受け入れられる唯一の答えは、「彼らは間違いを犯したから」です。私は実際に、これ以外の場合は優れたBigDecimalクラスを使用して大きな倍数のコードをリファクタリングする必要がある経験があります。
ジェームズアンダーソン

3
@JamesAnderson:あなたの答えに関する私の問題: "パフォーマンス"-実際の不変のコレクションは、あなたが説明する問題を避けるために、常に何らかの形の共有と再利用を実装していると言えます。「並行性」-引数は「可変性が必要な場合、不変オブジェクトは機能しません」に要約されます。「同じものの最新バージョン」という概念がある場合、何か自体、またはものを所有するもののいずれかを突然変異させる必要があります。また、「ストレージ」では、可変性は望ましくない場合があると言っているようです。
ジョミナル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.