コレクションフレームワーク

コレクションフレームワークは、オブジェクトの集合を扱うためのインタフェースと実装クラスの集合です。可変長配列、連想配列、キューといった枠組みが用意されています。

Python・Rubyなどでは 配列は可変長で各種メソッドも持っているため、データ集合を扱う場合は 配列や連想配列が良く使われますが、Javaでは通常の配列は固定長で 配列自体にはメソッドが用意されていなく、更には連想配列もないため、データ集合を扱う場合は 利便性の高いコレクションが使われることが多いです。

J2SE 5.0で総称型が導入され コレクションを型安全に利用できるようになりました。また、Java SE 8でストリームが追加され コレクションの要素をストリームとして扱うことができるようになりました。

 

コレクションフレームワークの主なクラス

コレクションフレームワークの主なクラスやインタフェースを次に挙げます。

 

コレクションフレームワークの主なクラスやインタフェース
インタフェース 概要 主な実装クラス 概要
List 順序付けられたコレクション ArrayList 一般的な可変長のListです。
LinkedList リンク構造のListです。要素の挿入、削除を行う際の処理がArrayListに比べて少ないため、頻繁に要素の挿入、削除を行う場合はLinkedListを使う方が性能が上がります。その代わり、インデックスを指定して要素にアクセスする場合は リンク構造を辿らないといけないため ArrayListに比べて性能が劣ります。
Set 重複要素のないコレクション HashSet 重複要素のない可変長のSetです。要素の順番は不定です。
LinkedHashSet 順序を保持したSetです。順序はSetに追加した順番になります。
TreeSet 自然順序付け等でソートされたSetです。
Map キーと値のコレクション(連想配列に該当) HashMap 連想配列のようなキー・値のセットの集合です。要素の順番は不定です。
LinkedHashMap キーの順序を保持したMapです。順序はMapに追加した順番になります。
TreeMap キーを自然順序付け等でソートされたMapです。
Queue 片端から追加、片端から取り出しができるキュー ArrayDeque ArrayDequeはQueueとDequeの両方のインタフェースを実装していて、Queueとして扱う場合はFIFOのキューとして利用できます。
Deque 両端から追加、両端から取り出しができるキュー ArrayDeque Dequeとして扱う場合は、FIFOのキューとしても、LILOのスタックとしても利用することができます。

 

List

Listインタフェース

Listは順序付けられたコレクションで、Collectionインタフェースのサブインタフェースになります。

Listの要素が可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。equals()が返す結果が変わってしまうと、contains(Object)、indexOf(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

 

ArrayListクラス

ArrayListはListインタフェースの実装の1つで 可変長のリストです。内部的には要素を配列で保持しているので インデックスによるアクセスは高速ですが、リストの途中に要素の挿入・削除をする場合の性能は 次に紹介するLinkedListに比べて劣ります。

 

生成・初期化

インスタンスはダイヤモンド演算子を使って生成します。

コンストラクタの引数で 初期サイズを設定することができます。デフォルト値は10です。Listのサイズは可変長で 必要に応じて増やすことはできますが、動的なサイズ拡張はコストの高い処理となりますので 予め必要なサイズが分かっている場合は 生成の際にサイズを明示しておいた方が良いです。

インスタンスイニシャライザで add()メソッドを使って要素を追加することにより 初期化することもできます。

 

 

出力

System.out.println()にListを渡すとListの要素を [] で囲んで出力してくれます。

尚、配列の場合は Arrays.toString()を使わないと 配列の要素ではなく配列のハッシュコードなどが出力されてしまいます。

 

走査

Listの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査
  • Iteratorによる走査
  • forEach()メソッドによる走査

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は Iteratorを使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

 

主なメソッド

ArrayListの主なメソッドを次にまとめます。

AddListの主なメソッド
分類 メソッド 概要
追加・削除・更新 add リストの最後に要素を追加します。
addAll リストの最後に別のリストの要素を全て追加します。
remove(int) 指定したインデックスの要素を削除します。
remove(Object) 指定した要素を削除します。
set 指定したインデックスの要素を置き換えます。
clear リストから全ての要素を削除します。
取得・検索 get 指定したインデックスの要素を返します。
size リストの要素の数を返します。
isEmpty リストが空かどうか判定します。
contains 指定した要素が含まれるかどうか判定します。
indexOf 指定した要素のインデックスを返します。存在しない場合は-1を返しますので、要素の包含チェックに使うこともできます。
部分リスト作成 subList 指定した範囲の部分リストを作成して返します。

 

ソート

並べ替えを行うにはCollections.sort()を使います。自然順序付け(ComparableインタフェースのcompareTo())に基づいて並べ替えます。並べ替えたリストを作成して返すのではなく、自身の要素を並べ替えることに注意してください。ソートの順序付けを独自に指定したい場合は、Collections.sort()にComparatorを渡します。

 

LinkedListクラス

LinkedListは Listインタフェースの実装クラスの1つで 双方向(自分の前の要素と後ろの要素の参照を持つ)のリストを持ちます。そのため、リストの途中に要素の挿入・削除をする場合は リンク先の付け替えだけで済むので ArrayListに比べて高速に行うことができます。一方でインデックスを指定して要素にアクセスするためには リンクを辿らないといけないため、ArrayListに比べて遅くなります。

用途に応じてArrayListとLinkedListを使い分けると良いです。

 

Set

Setインタフェース

Setは重複要素のないコレクションで、Collectionインタフェースのサブインタフェースになります。

Setの要素が可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。HashSetクラスやLinkedHashSetクラスの場合は、オブジェクトの状態が変更になっても hashCode()が返す結果が変わらないようにする必要があります。また、TreeSetクラスの場合は、オブジェクトの状態が変更になっても compareTo()が返す結果が変わらないようにする必要があります。

equals()等が返す結果が変わってしまうと、contains(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

HashSetクラス

HashSetは Setインタフェースの実装の1つで 重複しない要素の集合です。要素の順番は保持しません。

 

生成・初期化

インスタンスは ダイヤモンド演算子を使って生成します。

HashSetもArrayListと同様 必要に応じて容量が動的に拡張します。コンストラクタで初期容量と負荷係数を指定することができますが、デフォルト値はそれぞれ16と0.75です。内部のハッシュテーブルのバケットが16割り当てられ、0.75にあたる12が埋まると拡張されます。動的な容量拡張の処理はコストの高い処理となるため、ArrayList同様 全体の要素数が予め分かっていれば、初期容量を適切な値で明示すると良いです。ただ、ArrayListの場合とは異なり、必要な要素数=初期容量にはならず、正確に見積もることはできません。また、ハッシュテーブルの構造上、容量を大きくし過ぎると Iterator等による走査で全要素にアクセスする際にパフォーマンスの低下を招いてしまいます。小さ過ぎても 大き過ぎても性能劣化につながります。おおよその目安として 必要な要素数の4/3程度を初期容量とするのが良いと考えられます。

インスタンスイニシャライザで add()メソッドを使って要素を追加することにより 初期化することもできます。

 

HashMapの注意点」(プログラマはサイコロを振らない)

出力

System.out.println()にSetを渡すと Setの要素を [] で囲んで出力してくれます(Listと同様)。

走査

Setの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査
  • Iteratorによる走査
  • forEach()による走査

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は Iterator を使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

主なメソッド

HashSetの主なメソッドを次にまとめます。

HashSetの主なメソッド
分類 メソッド 概要
追加・削除・更新 add セットに要素を追加します。
remove 指定した要素を削除します。
clear セットから全ての要素を削除します。
取得・検索 size セットの要素の数を返します。
isEmpty セットが空かどうか判定します。
contains 指定した要素が含まれるかどうか判定します。

 

ハッシュコード

HashSetでは 要素のhashCode()メソッドによりハッシュ値を導出しています。「Object」の章の「hashCode()メソッド」で説明した通り、hashCode()メソッドの一般契約として、equals()がtrueを返すインスタンス同士は hashCode()の戻り値が同じである必要があります。そうしないと、contains()やremove()といった 指定された要素の特定を行うメソッドが正しく機能しなくなってしまいます。contains()やremove()では、同じハッシュバケットの要素に対してequals()を呼び出して 指定された要素かどうかの検査を行います。そのため、異なるハッシュバケットに入れられてしまった要素は 検査対象にすらならないためです。

独自クラスをHashSetの要素にする場合は、「Object」の章で説明した eqauls()とhashCode() の一般契約に従う必要があります。

 

LinkedHashSetクラス

LinkedHashSetは Setインタフェースの実装の1つで 要素の順番を保持します。要素の順番は Setに追加した順番となります。自然順序付けや 独自の順序付けで並べたい場合は、次のに紹介するTreeSetクラスを使います。

TreeSetクラス

TreeSetは Setインタフェースの実装の1つで 要素の順番を保持します。引数なしのコンストラクタで生成すると 要素の順序は 自然順序付けに従って並べられます

自然順序付けではなく 独自の順序付けを行いたい場合は、TreeSetのコンストラクタにComparatorを渡します。ラムダ式を用いると簡潔に記述することができます。

Map

Mapインタフェース

Mapは キーと値のペアのコレクションで、Collectionインタフェースのサブインタフェースになります。他の言語の連想配列に該当します。

Mapのキーは、基本的には不変オブジェクトにします。Mapのキーが可変オブジェクトの場合、オブジェクトの状態が変更になっても equals()が返す結果が変わらないようにする必要があります。HashMapクラスやLinkedHashMapクラスの場合は、オブジェクトの状態が変更になっても hashCode()が返す結果が変わらないようにする必要があります。また、TreeMapクラスの場合は、オブジェクトの状態が変更になっても compareTo()が返す結果が変わらないようにする必要があります。

equals()等が返す結果が変わってしまうと、get(Object)、containsKey(Object)、remove(Object)といった 対象要素を検索するメソッドにおいて、対象要素を特定できなくなってしまうからです

HashMapクラス

HashMapは Mapインタフェースの実装の1つで、キーと値のペアの集合で 要素の順番は保持しません。

 

生成・初期化

インスタンスは ダイヤモンド演算子を使って行います。キーと値の両方の型を指定します。

HashMapの初期容量と拡張の仕組みは HashSetと同じになります。

 

インスタンスイニシャライザで put()メソッドを使って要素を追加することにより 初期化することもできます。

 

 

出力

System.out.println()にMapを渡すと Mapの要素を {} で囲んで出力してくれます。

 

 

走査

Mapの要素を走査するには、いくつかの方法があります。

  • for文や拡張for文による走査(キー/値/キーと値のペア)
  • Iteratorによる走査(キー/値/キーと値のペア)
  • forEach()による走査(キーと値のペア)

ラムダ式やメソッド参照の導入で コレクション走査の処理がすっきりと記述できるようになりました。

走査中に要素の削除を行う場合は、Iteratorを使います。拡張for文で削除を行うとConcurrentModificationExceptionが発生することがあります。

 

 

主なメソッド

HashMapの主なメソッドを次にまとめます。

HashMapの主なメソッド
分類 メソッド 概要
追加・削除・更新 put キーと値のペアを追加します。
remove キーを指定してキーと値のペアを削除します。
clear マップの全ての要素を削除します。
取得・検索 get 指定したキーの値を返します。
size マップの要素の数を返します。
isEmpty マップが空かどうか判定します。
containsKey 指定したキーが含まれるかどうか判定します。
containsValue 指定した値が含まれるかどうか判定します。
keySet 全てのキーを返します。
values 全ての値を返します。
entrySet 全てのキー・値のペアを返します。

 

ハッシュコード

HashMapも HashSetと同様、「Object」の章で説明した equals()とhashCode()の一般契約に従う必要があります。

 

LinkedHashMapクラス

LinkedHashMapは Mapインタフェースの実装の1つで 要素の順番を保持します。要素の順番は Mapに追加した順番となります。自然順序付けや独自の順序付けで並べたい場合は、次に紹介するTreeMapを使います。

 

 

TreeMapクラス

TreeMapは Mapインタフェースの実装の1つで キーの順番を保持します。引数なしのコンストラクタで生成すると キーの順序は自然順序付けに従って並べられます

自然順序付けではなく 独自の順序付けを行いたい場合は、TreeMapのコンストラクタにComparatorを渡します。ラムダ式を用いると簡潔に記述することができます。

 

 

Queue、Deque(J2SE 5.0~)

Queueインタフェース

J2SE 5.0以前では、FIFOのキューを実現するには Listを利用した実装が良く見られました。また、LILOのスタックを実現するには、java.util.Stackクラスが用意されていました。J2SE 5.0から QueueDequeインタフェースが提供され、キューとスタックの枠組みが導入されました。元からあるStackクラスは QueueもDequeインタフェースも実装しておらず、Java APIドキュメントでも StackよりQueue、Dequeを使うことが推奨されています

Queueインタフェースは 片端からの追加と 片端からの取り出しを定義しています。あくまでも片端からの追加と片端からの取り出しを定義しているだけであって、取り出し順番は実装クラスに任せられています(FIFOでもLIFOでも優先度付きでも構いません)。

Queueインタフェースでは 主に次のメソッドを定義しています。それぞれの操作に対して 操作が失敗したときに例外を投げるメソッドと、特殊な値(操作に応じてnullまたはfalse)を返すメソッド両方が用意されています。nullを特殊な値として扱うため、Queueに対してnull要素の追加はするべきではないとされています。

Queueインタフェースの主なメソッド
操作 操作失敗時に例外を投げる 操作失敗時に特殊な値を返す
追加 add offer
削除 remove poll
調査(キューから要素は削除しない) element peek

 

Dequeインタフェース

Dequeは dequeue(キューから取り出す)を表しているのではなく、Double Ended Queue両端キュー)を表しています。Queueでは片端に要素を追加して 片端から要素を取り出しますが、追加する端と取り出す端は固定です(FIFOの場合、追加する端は末尾 取り出す端は先頭になります)。これに対して、Dequeは どちらの端に追加することもでき どちらの端から取り出すこともできます使い方次第で FIFOのキューとしても、LIFOのスタックとしても利用することができます

Dequeインタフェースでは Queueインタフェース同様、それぞれの操作に対して 操作が失敗したときに例外を投げるメソッド特殊な値を返すメソッド両方が用意されています

Dequeインタフェースの主なメソッド
操作対象が先頭 操作対象が末尾
操作 操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加 addFirst offerFirst addLast offerLast
削除 removeFirst pollFirst removeLast pollLast
調査(キューから要素は削除しない) getFirst peekFirst getLast peekLast

Dequeは Queueのサブインタフェースなので、Queueとして扱うこともできます。その場合は FIFOの形でマッピングされます。具体的には、Queueのメソッドは 次のようにDequeのメソッドにマッピングされます。

DequeをQueueとして扱うの場合のマッピング
Queue Deque
操作 操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加 add offer addLast offerLast
削除 remove poll removeFirst pollFirst
調査(キューから要素は削除しない) element peek getFirst peekFirst

また、Dequeには スタックとして利用するためのメソッドも定義されていて、内部的には次のようにマッピングされます。

Dequeをスタックとして扱う場合のマッピング
スタック用メソッド Deque
操作 操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
操作失敗時に
例外を投げる
操作失敗時に
特殊な値を返す
追加 push addLast
削除 pop removeFirst
調査(キューから要素は削除しない) peek peekFirst

 

ArrayDequeクラス

ArrayDequeは Dequeインタフェースの実装クラスの1つです。Dequeインタフェースの説明の通り、FIFOのキューとして利用することもできますし、LIFOのスタックとして利用することもできます。ArrayDequeはスレッドセーフではありません。

FIFOのキューとしての利用例を次に挙げます。

LIFOのスタックとしての利用例を次に挙げます。

 

相互変換

コレクションと配列の相互変換や 各コレクション間での相互変換をいくつか取り上げます。

 

Listと配列の相互変換

Listから配列に変換するには、ListのtoArray()メソッドを使用します。toArray()は引数ありのメソッドを使い、引数には実際の型の配列(例えば String[])を生成して渡します。その際に渡す配列のサイズは List.size()ではなく 0を指定します。(List.size()よりも 0を指定した方がパフォーマンスが良くなります。Effective Javaの中で その検証を行ったサイトが紹介されています。)

引数なしのtoArray()もありますが、そちらはObject[] が返ってきてしまい、実際の型の配列(例えば String[])へキャストすると ClassCastExceptionが発生してしまい使い勝手が悪いです。

 

配列からListに変換するには、ArraysのクラスメソッドasList()を使います。asList()で返されるのはjava.util.Arrays.$ArrayListで これはArraysクラスの内部クラスであり、java.util.ArrayListとは異なります。したがって 要素の追加や削除を行うことはできず、要素の追加や削除を行うとUnsupportedOperationExceptionが発生します。

 

ListとSetの相互変換

ListからSetに変換するには、Setの実装クラスのコンストラクタにListのインスタンスを渡します。渡したListに重複要素がある場合は 重複要素が取り除かれます。また、HashSetのように順不同のSetの場合は 当然ながらListの順番は保持されません。

Listの順番を保持したい場合は LinkedHashSetのように順番を保持するSetの実装を使います。

 

逆に SetからListに変換する場合は、Listの実装クラスのコンストラクタにSetのインスタンスを渡します。HashSetのように順不同のSetを渡した場合は Listの順番は順不同となります。

ListとMapの変換

ListからMapへの変換の前提として Listの要素からキーと値を生成できることとします。

ListからMapへの変換には Streamのcollect()メソッドを使うと 簡潔に処理を記述することができます。

 

逆に MapのキーをListに変換する場合は keySet()メソッドを、値をListに変換する場合はvalues()メソッドを呼び出して Listの実装クラスのコンストラクタに渡します。

スレッドセーフなコレクション

HashMap、HashSetなどはスレッドセーフではなく、マルチスレッド環境で利用する場合は 自前で排他制御を行う必要があります。マルチスレッド環境で 並列性を維持しつつ スレッドセーフに操作したい場合は、ConcurrentHashMapやConcurrentHashSetなどの java.util.concurrentパッケージのコレクション実装を使います。

以前は Collections.synchronizedMap()等のメソッドを利用する方法がありましたが、Iterator等を用いて要素の走査を行う場合にスレッドセーフでなく、自前で排他処理を実装する必要がありました。

これを改善すべく J2SE 5.0からConcurrentHashMapやConcurrentHashSet等の実装クラスが追加されました。詳しくは「マルチスレッド」の章の「ロックフリーでスレッドセーフな操作を提供するクラスを利用する」を参照してください。

 

タイトルとURLをコピーしました