List.append() - MIDP実装間で異なる動作

 携帯電話メーカーや通信オペレーターの独自拡張のためにJava MEの互換性が損なわれてしまった、という話を耳にすることがある。実際には、共通基盤たるべきMIDPの実装においても、仕様書に明確に記述されていない部分について細かい差異が生じている。Expense Reportで経験したListクラスを例に挙げる。
 Expense Reportの費用リスト表示用に、Listクラスを継承したDataBoundListというクラスを定義している。Listクラスは表示する各アイテムごとに文字列とアイコンを持っているが、DataBoundListはさらに任意のオブジェクトをアイテムに関連づけることができる。Expense Reportでは費用を表現するオブジェクトを各アイテムに関連づけている。
 アイテムを追加するためのメソッドにObject引数を加えて、

public int append(String stringPart, Image imagePart, Object dataPart) {
  super.append(stringPart, imagePart);

  // dataPartの処理
    :
}

public void insert(int elementNum, String stringPart, Image imagePart, Object dataPart) {
  super.insert(elementNum, stringPart, imagePart);

  // dataPartの処理
    :
}

とし、もともとListクラスにあったシグネチャーのappend()とinsert()は、呼び出し禁止の意味でIllegalStateExceptionを送出するようにした。*1

public int append(String stringPart, Image imagePart) {
  throw new IllegalStateException();
}

public void insert(int elementNum, String stringPart, Image imagePart) {
  throw new IllegalStateException();
}

 このバージョンをKDDIオープンアプリプレイヤーで動かしたところ、IllegalStateExceptionでアプリケーションが終了してしまった。ノキアソニー・エリクソンの実機では例外は発生しないし、コードからはDataBoundList.append(String, Image, Object)しか呼び出していない。しかし、IllegalStateExceptionが発生する原因は、DataBoundListクラスの、Objectパラメータを取らないappend()かinsert()が呼ばれている以外に考えられない。いくつかパターンを変えてテストしてみて、結局、DataBoundList.append(String, Image, Object)から、List.append(String, Image)を経由してDataBoundList.insert(int, String, Image)が呼ばれていると結論した。つまり、オープンアプリプレイヤーMIDP実装ではList.append(String, Image)の内部でinsert(int, String, Image)が呼ばれ、最終的にDataBoundListでオーバーライドした(Objectパラメータなしの)バージョンを呼び出しているということだ。
 Choice.insert(int, String, Image)の最初の引数に、そのときChoiceが保持しているアイテム数と同じ値が指定されたときは、append(String, Image)と同じ効果を持つ、とMIDPの仕様書に書かれている*2。この部分を読むと、オープンアプリプレイヤーの動きは正しいように見える。とはいえ、アプリケーションのプログラマーがListクラスを継承した実装を用意したときに、意図して呼び出したスーパークラスのappend()からサブクラスのinsert()が呼ばれることを想定するのだろうか。List.insert()がfinalなメソッドでない以上、そのように想定すべきなのかもしれない。少なくとも、オープンアプリプレイヤーの実装ポリシーとしてはそうなっている、ということのようだ。
 DataBoundList.append()からList.append()を呼び出すことを避けるため、以下のように改良してこの問題を回避した。あくまでも趣味のプログラミングである。プラットフォーム間のフラグメンテーションをアプリケーションで吸収するのも、一つの楽しみ方だ。*3

public int append(String stringPart, Image imagePart, Object dataPart) {
  int index = size();
  insertImpl(index, stringPart, imagePart);
  return index;
}

public void insert(int elementNum, String stringPart, Image imagePart, Object dataPart) {
  insertImpl(elementNum, stringPart, imagePart);
}

private void insertImpl(int elementNum, String stringPart, Image imagePart, Object dataPart) {
  super.insert(elementNum, stringPart, imagePart);

  // dataPartの処理
    :
}

*1:set()メソッドも同様だが、今回のテーマとは関係が薄いので省略する。

*2:おもしろいことに、Choiceインターフェイスの実装クラスであるListやChoiceGroupのinsert()メソッドにはそのような定義は書かれていない。

*3:自分のアプリケーションを、各メーカーごとに異なるソフトキー・ポリシーに自動的に合わせてみたいと考えている。我ながら馬鹿げた考えだと思う。