コレクションAPI:Listインタフェース

なんか「コレクションフレームワーク」でググれば全て解決するような気がしてきたが、まあちまちまやっていきます。基本的にここに書くことはレベルを抑えているというか、本気を出して書いて的外れなことを書くのが怖いのです。


このコレクションフレームワークを特に対象とした1.5からの新機能が多いので、今回はListだけにとどめます。よくあることですが、冗長になるし面倒なのでAPIを全部コピったりはしません。常にAPIドキュメントを参照してください。ArrayListはよくある可変長配列の要求を満たすもので、これを知っているだけでいろいろと便利になります。

public interface List<E>

boolean add(E e);
void add(int index, E element);
void clear();
E get(int index);
E remove(int index);
boolean remove(Object o);
E set(int index, E element);
int size();

Listの主なメソッドを挙げました。addは最後に要素を追加します。挿入する位置を指定するものもあります。clearは全消去します。getはindex番目の要素を取得します。removeはindex番目を削除するものと、オブジェクトを渡して一致するもの(equalsメソッドで判定)を削除するものがあります(まあ後者を頻繁に使うならSetの使用を考えるべきだが)。setはindex番目を書き換えます。sizeは要素数を返します。まあadd(E)とget(int)とsize()あたりで要求が満たされることがほとんどかも?
よく使われるListの実装はArrayListとLinkedListです。


ごちゃごちゃ言わずにArrayListを使ってみましょう。

List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(3);
list.add(5);
list.add(7);
list.add(9);
System.out.println(list.size());
for (int i = 0; i < list.size(); i++) {
  System.out.println(list.get(i));
}
for (Integer v : list) {
  System.out.println(v);
}

1 3 5 7 9が2回出てきます。上の方法は配列としての特性を生かしたもので、下の方法は拡張for文でイテレータ(要素を巡るもの)を利用したものです。
ファイルからデータを読み込むが、要素数がわからない。しかし、全部メモリ上に読み込んでから全体を処理したい、などという要求はありがちだと思います。そんなあなたにArrayListですね。


ジェネリクスについて
Listというようにという変なものがついています。ここには型(といってもプリミティブ型はだめ、Objectを継承していること。)を指定します。すると例えば今回の場合Integerを指定したのでadd(E e)がadd(Integer e)というように置き変わります。1.4以前はこの機能がなく、すべてObjectでした。

boolean add(Object e);
Object get(int index);
List list = new ArrayList();
// Integerの配列のつもりなのにStringが追加できてしまう
list.add("abc");
// 取り出すときにObjectで返ってくるのでキャストが必要 面倒
// Stringが入っていたりするとClassCastException(実行時例外)
Integer v = (Integer)list.get(0);

まず追加時に引数がObjectであるため、オブジェクトなら何を入れてもコンパイルが通ってしまいます。また、取得時にはObject型で返ってくるためキャストが必要で面倒です。また、変な型のオブジェクトを追加していた場合、addではなくgetのここで(コンパイル時ではなく)実行時例外が発生します。追加と取得のコードが離れていることも多々あるため、非常に質の悪いバグの原因になります。ジェネリクスはこの問題を解決し、特定の型のオブジェクトしかaddできない(コンパイル時エラーで発見できる)ので取得時に例外も起こらないという、論理的整合性を持たせました。文法的にキャストも不要になりました。この機能はコンパイラが自動的に(ちょうど1.4までと同じような)キャストのコードを生成することで実現されています。こうやって互換性を維持したんですね。


オートボクシング・アンボクシングについて
プリミティブ型はオブジェクトではないのでコレクションに追加できません。というわけでIntegerなどのラッパークラスを用いて使うことになるのですが、ジェネリクスでキャストが不要になったとはいえ少々コードが冗長になります。というわけで例えば「Integerが必要とされるところにintを渡すとintが自動的にIntegerに変換される」(オートボクシング)とその逆(オートアンボクシング)という機能が1.5で追加されました。

// add(Integer e)が要求されているのにintを渡している
// →コンパイルエラーではなくIntegerに変換してから渡される
list.add(1);

ラッパークラスなどこういうパフォーマンスに来そうなところは特によく考えられているでしょうが、それでも無駄な処理は避けるべきです。例えば「Integer+IntegerをIntegerに代入」のようなコードは「Integer2つをintにしてint+intを計算した結果のintをIntegerにして代入」となることは理解しておいた方がいいでしょう。こういう目に見えないコードの自動生成は危ない面も多い。


拡張for文について
前にも書きましたがインタフェースIterableを実装したクラスのインスタンスは拡張for文の対象にできます。意味はソースから推測すること。

public interface Iterable<T> {
  Iterator<T> iterator();
}
public interface Iterator<E> {
  boolean hasNext();
  E next();
  void remove();
}

普通に使うならこうです。

Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
  // ※アンボクシング
  int v = it.next();
  // ...
}

これを簡単に書けるようにしたのがforeach文というわけです。Iteratorインタフェースが「要素を巡る方法」を抽象化していることに気がつけばナイス。…まだLinkedListやってないのに……。