【Java】Listの基本
List の宣言
List
はインターフェースのため、ArrayList
などの実装クラスを使用します。
他にもLinkedList
やVector
などの実装クラスはありますが、多くの場合はArrayList
を使用します。
この記事でもArrayList
を中心に説明していきます。
List
を使用する際は、ジェネリスク<>
を用いて要素のクラスを指定します。
int
など基本のデータ型を使用する際は、Integer
のようなラッパークラスを用います。
List<Integer> list = new ArrayList<Integer>();
右辺のジェネリクスは省略することができます。
List<Integer> list = new ArrayList<>();
また以下のようにして配列をList
に変換することができます。
String[] ary = {"AAA", "BBB", "CCC"};
List<String> list = Arrays.ofList(ary);
List の追加
add
List
に要素を追加するにはadd
を使用します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add(1, "CCC");
//結果
//["AAA", "CCC", "BBB"]
引数が 1 つの場合は、その要素を末尾に追加します。 引数が 2 つの場合は、指定したインデックスに要素を追加します。
addAll
add
は 1 つの要素を追加しますが、addAll
は複数の要素を追加します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
List<String> add1 = new ArrayList<>();
add1.add("CCC");
add1.add("DDD");
List<String> add2 = new ArrayList<>();
add2.add("EEE");
add2.add("FFF");
list.addAll(add1);
list.addAll(1, add2);
//結果
//["AAA", "EEE", "FFF", "BBB", "CCC", "DDD"]
add
と同じく引数 1 つの場合は末尾に追加し、2 つの場合は指定したインデックスから追加します。
List の参照
get
get
は引数に指定したインデックスの要素を取得します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.get(1); //"BBB"
要素数範囲外のインデックスを指定した場合はIndexOutOfBoundsException
が発生します。
size
size
はList
の要素数を返します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.size(); //3
isEmpty
isEmpty
は、List
の要素数が 0 であることを判定し、その結果を真偽値で返します。
List<String> list = new ArrayList<>();
list.isEmpty(); //true
list.add("AAA");
list.isEmpty(); //false
List の置換
set
set
は、指定したインデックスの要素を置き換えに使用します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.set(1, "DDD");
//結果
//["AAA", "DDD", "CCC"]
replaceAll
replaceAll
はList
のすべての要素に対し、何らかの変更を加える際に使用します。
例えばすべての要素を小文字に変更する場合は以下のようにします。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.replaceAll(String::toLowerCase);
//結果
//["aaa", "bbb", "ccc"]
詳細は割愛しますが、引数に指定するのは関数型インターフェースUnaryOperator<T>
になります。
要素に対し、変換がかけられない場合はUnsupportedOperationException
が発生します。
List の検索
indexOf, lastIndexOf
indexOf
、lastIndexOf
は引数の要素を検索し、最初に一致した要素のインデックスを返します。
対象の要素がない場合は-1
を返します。
要素の一致とはequals
がtrue
のものを指します。
indexOf
は先頭から検索し、lastIndexOf
は末尾から検索します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.add("BBB");
list.add("CCC");
list.add("AAA");
list.indexOf("BBB"); //1
list.lastIndexOf("BBB"); //3
contains, containsAll
contains
, containsAll
は引数の要素を検索し、一致する要素があるかを真偽値で返します。
contains
は引数の 1 つの要素を検索し、containsAll
は引数のすべての要素を検索し、すべての要素に対して一致する要素がある場合のみtrue
を返します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
List<String> listA = new ArrayList<>();
listA.add("BBB");
listA.add("CCC");
List<String> listB = new ArrayList<>();
listB.add("BBB");
listB.add("DDD");
list.contains("BBB"); //true
list.contains("DDD"); //false
list.containsAll(listA); //true
list.containsAll(listB); //false
List の削除
clear
clear
はList
の要素をすべて削除し、空の状態(要素数を 0)にします。
remove
remove
は指定した要素を削除するのですが、指定方法が 2 パターンあります。
1 つはインデックスを指定する方法です。そのままインデックスに該当する要素を削除します。
もう 1 つはインスタンスを指定する方法です。検索の場合と同じで、引数に指定したインスタンスと一致する最初の要素を削除します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.add("DDD");
list.add("EEE");
list.remove(1); //["AAA", "CCC", "DDD", "EEE"]
list.remove("DDD"); //["AAA", "CCC", "EEE"]
removeAll
removeAll
は引数のすべての要素に一致する要素を削除します。
List<String> list = new ArrayList<>();
list.add("AAA");
list.add("BBB");
list.add("CCC");
list.add("AAA");
List<String> listA = new ArrayList<>();
listA.add("AAA");
listA.add("CCC");
list.removeAll(listA); //["BBB"]
ArrayListについて
最後にArrayList
について補足しておきます。
内部構造
ArrayList
は内部ではObject[]
という配列に要素を格納されます。
配列のサイズは、コンストラクタの引数で指定でき、デフォルトでは10となっています。
List<Integer> list1 = new ArrayList<>(); //10
List<Integer> list2 = new ArrayList<>(10000); //10000
ここで配列のサイズイコールsize
メソッドで取得できる値ではない点に注意してください。
配列のサイズを直接調べるメソッドは用意されていません。
要素の追加
add
メソッドにより要素を追加しますが、配列のサイズとの関係によって少し挙動が異なります。
配列のサイズに空きがある場合、末尾に要素を追加するだけです。 しかし、配列のサイズに空きがない場合は配列のサイズを大きくする必要があります。 このために、新しいサイズの配列を作成し、元の配列の要素をコピーします。
計算量でみると、空きがある場合はO(1)
ですが、空きがない場合は要素分コピーを行うためO(n)
となります。
そのため、大量データを扱うことがわかっている場合は、初期時点での配列サイズは大きく指定した方がパフォーマンスはよくなる場合があります。
要素の参照
ArrayList
では前述の通り配列を使用しているため、インデックスを指定した要素へのアクセス(ランダムアクセス)はO(1)
と高速です。
つまり、get
やset
メソッドは高速に参照・置換が可能です。
しかし、indexOf
メソッドで要素自体を検索する場合は、順に要素を比較して対象のインデックスを検索するため、最大O(n)
の計算量となります。
要素の削除
要素を削除する場合、削除された要素以降の要素を左にシフトする処理が行われます。
そのため最大O(n)
の計算量となります。
しかし、これはあくまでインデックスを指定した場合の話であり、要素自体を指定する場合はindexOf
メソッド同様に順に検索する処理が必要となります。
メモリについて
要素の追加で記述した通り、配列サイズに空きがない場合は、新しいサイズの配列を作成します。 この新しいサイズは、元のサイズの約1.5倍となります。 つまり、だんだんと追加されるサイズも大きくなっていきます。
こうなったときに、無駄にメモリ領域が使用される可能性があります。 例えば、追加で10,000増やしたのに1要素しか追加しなかったので9,999分余ってしまう、といった感じです。
このような場合はtrimToSize
メソッドを使用します。
これにより、配列のサイズを要素数に合わせてくれます。
ArrayList<Integer> list = new ArrayList<>(10000);
list.add(1);
list.add(2);
list.trimToSize();
特に大量データを扱う場合には無駄になることが多いため、trimToSize
メソッドの使用をお勧めします。
ArrayListのまとめ
ArrayList
は内部的に配列を用いていることから、インデックスを用いた要素の参照については非常に高速です。
しかし、要素の追加や削除については時間がかかる場合があるためあまり向いていません。
追加・削除が多い場合はLinkedList
などを使用することを検討します。