kagamihogeの日記

kagamihogeの日記です。

JEP 269: Convenience Factory Methods for Collectionsをテキトーに訳した

new Collections APIs for Java 9って何すかって感じだったんで該当のJEP 269を読んでみることにした。

http://openjdk.java.net/jeps/269

JEP 269: Convenience Factory Methods for Collections

Owner    Stuart Marks
Created 2014/06/26 23:05
Updated 2015/11/05 22:58
Type    Feature
Status  Proposed to Target
Component   core-libs/java.util:collections
Scope   SE
Discussion  core dash libs dash dev at openjdk dot java dot net
Effort  S
Duration    S
Priority    3
Reviewed by Brian Goetz, Chris Hegarty, Paul Sandoz
Endorsed by Brian Goetz
Release 9
Issue   8048330

Summary

少数の要素によるcollectionとmapインスタンスの生成の手間を軽減するライブラリAPIを定義します。目的はJavaプログラミング言語にコレクションリテラルが無いことによる不便さを緩和することです。

Goals

コレクションのインタフェースにstaticファクトリーメソッドを設け、このインタフェースはコンパクト(compact)で更新不能な(unmodifiable)コレクションインスタンスを生成します。頻繁に使われるコレクションクラスの実装を生成するstaticファクトリーメソッドを提供します。このAPIは意図的に小さく保たれるようにします。

Non-Goals

ただし、完全に汎用的なコレクションビルダー("collection builder")機能の提供は本JEPの目的ではありません。たとえば、コレクション実装をユーザが制御したり、ミュータブル・指定サイズ・負荷率(loading factor)・コンカレンシーレベル(concurrency level)などの機能、がそれに当たります。

高パフォーマンスや、任意の要素数によるスケーラブルなコレクションのサポートも対象外です。本JEPは小規模なコレクションを対象とします。

更新不能コレクション型(unmodifiable collection types)の提供は対象外です。本JEPでは型システムにおける更新不能の機能を、たとえ実装が実際に更新不能だったとしても、公開しません。

本JEPでは"immutable persistent"や"functional" collectionsの提供は行いません。

Motivation

Javaはその冗長さのために批判に晒されることがあります。少数要素で更新不能なコレクションを生成するには、コレクションを生成し、ローカル変数に格納し、複数回add()を実行し、それからラップします。

Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");
set = Collections.unmodifiableSet(set);

これはマジで冗長で、その理由は単一の式で表現不可能だからです。また、staticなコレクションは、より扱いやすいフィールドイニシャライザではなく、staticイニシャライザでの処理が必須です。別の方法として、他のコレクションを受け取るコピーコンストラクタを使用するコレクション処理のやり方があります。

Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

幾分マシではありますがコードの明確さが薄れており、たとえばSet生成前にListを生成している点などです。更に別の方法として、いわゆる"double brace"テクニックを使うやり方があります。

Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
    add("a"); add("b"); add("c");
}});

これは無名内部クラスのインスタンスイニシャライザを使用しており、ややマシです。しかし、これはちょっと分かりにくく、使用時に余分なクラスというコストが必要です。また、エンクロージングインスタンスへの隠された参照と、任意のオブジェクトへの参照を保持します*1。これはメモリリークシリアライズ時に問題を引き起こす可能性があります。そうした理由のため、このテクニックは基本的には避けるべきです。

Java 8 Stream APIのファクトリメソッドとコレクタを組み合わせることで、小規模なコレクション生成が可能です。

Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet()));

(streamのコレクタは、コレクタが返すコレクションのミュータブルについては保証しません。Java 8では、返されるコレクションはArrayList, HashSet, HashMapなどのミュータブルで順序付きのコレクションです。ただし、将来のJDKリリースで変更される可能性があります。)

上記のコードは若干回りくどく、分かりにくいわけではないものの、明確なコードとはとても言えません。これもまた不必要なオブジェクト生成がいくらか含まれています。現在のところ上記の方法はMapには使えません。StreamではMap生成に上記のやり方は使用出来ません。ただし、値がキーから算出可能であるか、streamの要素がキーと値の双方を含む場合には可能です。

これまでにJavaプログラミング言語でコレクションリテラルをサポートするための変更案が何度か浮上してきました。しかし、言語機能の場合と同様、最初に思い描いたものほどクリーンでシンプルな機能にはならず、よってコレクションリテラルJavaの次期バージョンに現れることはありませんでした。

小規模コレクションインスタンス生成用のリテラルAPIを提供することでコレクションリテラルの利点の多くを実現可能で、言語変更に比べればリスクとコストは極めて低いです。例えば、以下のようにして小規模なSetインスタンスを生成するコードを考えます。

Set<String> set = Set.of("a", "b", "c");

Collectionsクラスには既存のファクトリメソッドとして空のList, Set, Mapを生成するものがあります。また、単一のkey-valueペア、ないし、単一要素のみから成るシングルトンのList, Set, Mapを生成するファクトリがあります。EnumSetには固定または可変の引数を取るof(...)オーバーロードメソッドがあり、指定要素でEnumSetを生成する際に使います。しかし、任意の型のオブジェクトから成るList, Set, Mapを生成するための汎用目的ではありません。

Collectionsクラスには更新不能なList, Set, Mapを生成するcombinatorメソッドがあります。これらのメソッドは本質的には更新不能なコレクションを生成しません。代わりに、コレクションを受け取ると更新リクエストを拒否するクラスでラップし、元となるコレクションの更新不能なビュー(view)を生成します。元となるコレクションへの参照を所持していれば更新操作は可能です。各ラッパーはオブジェクトを新規作成するので、間接演算が必要となり、元となるコレクションにプラスのメモリ消費が発生します。また、決して更新が発生しない場合であっても、ラップされたコレクションはmutationのためのコストを負い続けることになります。

Description

更新不能なコレクションインスタンスを生成するためにList, Set, Mapインタフェースにstaticファクトリメソッドを設けます。(クラスのstaticメソッドとは異なり、インタフェースのstaticメソッドは継承されない点に注意してください。よって、実装クラス経由では呼び出せないし、インタフェース型の実装経由でも同様です。)

ListSetのファクトリーメソッドは以下のようになります。

List.of(a, b, c);
Set.of(d, e, f, g);

可変長引数のオーバーロードもあるため、コレクションサイズに制限はありません。ただし、生成されるコレクションインスタンスのサイズが小さければチューニングが可能な場合があります。少数要素用の特殊ケースAPI(固定引数のオーバーロード)を提供します。

Mapでは固定引数のメソッドをいくつか提供します。

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)
Map.of(k1, v1, k2, v2, k3, v3)
...

我々の想定では8個までのkey-valueペアによるmap生成で大抵は事足りるであろうと考えています。それ以上の場合、任意の数のkey-valueペアを指定してMapインスタンスを生成するAPIを提供します。

Map.fromEntries(Map.Entry<K,V>...)

このアプローチはListSet用の可変長引数APIと似てはいますが、mapはあいにくとkey-valueの組を必要とします*2。key-value用のメソッドにはstatic importが適しており、これでより簡潔になります。

Map.Entry<K,V> entry(K k, V v)

上記のメソッドを使用して、任意の数の要素からなるmapを生成できます。

Map.fromEntries(
    entry(k1, v1),
    entry(k2, v2),
    entry(k3, v3),
    // ...
    entry(kn, vn));

(将来のJDKバージョンではvalue typeを使用することでboxing*3のコストを軽減出来る可能性があります。entry()簡易メソッドの戻り値型はMap.Entryを実装する新規追加の型の予定で、この措置は将来的にvalue typeへの移行を円滑にするためのものです。)

小規模コレクションを生成するAPIを提供し、また、更新不能コレクションでユースケースの大半を満たすことにより、仕様と実装はシンプルに保てます。更新不能コレクションは防御的コピーをしなくても良いため、パラレル処理に適しています。

小規模コレクションが占有する実行時の空間もまた重要な考慮事項です。2つの要素から成る更新不能HashSetの生成という単純なケースでラッパーAPIを使用すると6個のオブジェクトを構成します。ラッパー・HasthSetHasthSet内のHashMap・バケット(配列)のテーブル・要素ごとに1つのNodeインスタンス。格納するデータ量に比べて凄まじい量のオーバーヘッドが上乗せされており、データへのアクセスにポインタ参照とメソッド呼び出しが複数回必要となる事を避けられません。小規模・固定サイズコレクション用に設計する実装はこれらのオーバーヘッドの大半を回避可能で、それにはコンパクトなフィールドベースあるいは配列ベースのレイアウトを使用します。mutationをサポートする必要が無いこと(と、生成時にコレクションサイズが既知であること)も空間の節約に寄与しています。

ファクトリが返す実際のクラスはpublic APIを公開しません。実行時の型や戻されるコレクションの同一性(identity)についての保証はありません。これにより、互換性を壊すことなく後になってからの実装変更が可能になります。呼び出し元が依存すべきことは、戻される参照がインタフェース型の実装である、ということだけです。

返されるオブジェクトはserializableです。A serialization proxy object will be used as the common serialized form for the implementation classes. これにより、シリアライズ形式に実装の情報が漏れ出てしまうことを防止します。このような将来のメンテナンス向けの柔軟性を設けることにより、シリアライズの互換性に影響を与えることなく複数のリリースをまたいで実装に変更を入れることが可能となります。

Nullの要素・キー・値は許容しません。(直近で導入されたコレクションにnullをサポートするものはありません。)また、null拒否により、さらにコンパクトな内部表現・高速アクセス・少数の特殊ケース向け特化実装、の可能性が生まれます。

List実装はインデックスによる高速要素アクセスが期待されるため、RandomAccessマーカーインターフェースを実装します。

コレクションに格納する要素は通常のコレクションの契約に従うことが必須であり、その契約には適切なhashCode()equals()の実装があります。もしhashCode()equals()に影響を与えるような変更をSetの要素あるいはMapのキーに加えられる場合、コレクションの振る舞いは未定義です。

コレクションを生成して安全にパブリッシュされて以降は、そのインスタンス複数スレッドでコンカレントアクセスを安全に実行できます。

頻繁に使われるコレクションクラス用のファクトリーメソッドを追加します。少なくとも、以下のようなメソッドを追加します。

ArrayList.of(...)
HashSet.of(...)
HashMap.of(...)
HashMap.fromEntries(...)

上記のファクトリーメソッドは特定のコレクションのインスタンスを簡単に作成するものです。固定引数と可変長引数のメソッドがあり、インタフェース型に追加するメソッドと似たような感じです。

JDKで新APIを使用可能な場所を調査中です。時間とスケジュールが許せば新APIを使用するような変更を行います。

Alternatives

過去に言語に対する変更が何度か議論されたものの、却下されています。

  1. Project Coin Proposal, 29 March 2009
  2. Project Coin Proposal, 30 March 2009
  3. JEP 186 discussion on lambda-dev, January-March 2014

このメッセージに要約されている通り、言語ベースの変更提案ではなくライブラリベースの提案が採用となりました。

Google Guavaライブラリには、イミュータブルコレクション生成用・ビルダーパターン・様々なミュータブルなコレクション用、のユーティリティがあります。Guavaライブラリは大変使いやすく汎用的ですが、Java SEプラットフォームに含めるにはちょっとばかりやり過ぎ(overkill)なシロモノです。本JEPの提案はStephen Colebourneのlambda-dev, 19 Feb 2014の提案と良く似ており、また、Guavaのイミュータブルコレクションのファクトリーメソッドのアイデアを取り入れています。

Map.fromEntries()という任意数の要素でMapを初期化するやり方は理想的では無いですが、代替案の中では最もマシなものかと思われます。これの利点はタイプセーフで、構文上keyとvalueをワンセットに書けて*4、要素数コンパイル時に決まり、フィールドイニシャライザとしての使い方にマッチします。ただし、boxingな点は冗長です。複数の代替案が検討されましたが、それらは本JEPの提案よりも良くないトレードオフを発生させるものでした。

Testing

JDKレグレッションテストスイートおよびpublic API用のJDKテストにユニットテストが存在します。シリアライズ形式はJCKでカバー可能と思われます。

サイズおよびパフォーマンステストを作成します。baseline measurementsとの比較という一般的な目的とは対照的に、新規のコレクション実装と既存のものとを比較するテストとなります。固定オーバーヘッドベースと要素別ベースそれぞれにおいて、新規コレクションがヒープ空間の消費を少なくすることを検証します。既存コレクションと比較すると異なる内部表現となるので、場合によっては、新規コレクションは遅くなる可能性があります。そうしたスローダウンは許容範囲内に収まるべきです。パフォーマンス目標は明確に指定していませんが、10倍遅くなることは許容しません。また、新規コレクションは要素数の増加に応じた一貫性のあるパフォーマンスを維持します。最後に、このテストの測定結果はパフォーマンス数値のベースラインを定めることになり、将来の変更時との比較対象となります。

*1:and to any captured objects.が原文。

*2:it unfortunately requires that each key-value pair be boxed.が原文

*3:プリミティブのボクシンでは無いと思われ。Listやsetと比べると、Mapは受け取る要素が単一ではなくkey-valueペアになる。で、fromEntriesではkey-valueのペアを受け取るが、そのペアをentry()で囲うことをboxと言ってるのかな?と推測している。

*4:it has keys and values adjacent in the syntaxが原文。文法上key-valueを隣接させて書ける利点がある、と言っていると思うのでちょっと意訳した