scalaでjavaのList使うときの話

scalajavaのcollectionを使う場合は、scala.collection.JavaConversions以下をimportしておけば、便利なimplicitのmethodが定義されているので、自動でscalaのcollectionに変換されます。
それの実装がどうなっているかのメモ。(versionは2.8.1)
たとえば、java.util.Listを変換する場合どうなるかというと、

  implicit def asScalaBuffer[A](l : ju.List[A]): mutable.Buffer[A] = l match {
    case MutableBufferWrapper(wrapped) => wrapped
    case _ =>new JListWrapper(l)
  }

というasScalaBufferというmethodが定義されていて、暗黙的に、mutable.Buffer型のインスタンスに変換されます。変換されるといっても、MutableBufferWrapper*1でない場合は、JListWrapperというクラスのコンストラクタに、java.util.Listのインスタンスを渡してます。
名前から想像つくように、これは結局ラップしているだけなので、要素をコピーしたりしているわけではありません。
JListWrapperがどういうものかというと以下のようなクラスです

  case class JListWrapper[A](val underlying : ju.List[A]) extends mutable.Buffer[A] {
    def length = underlying.size
    override def isEmpty = underlying.isEmpty
    override def iterator : Iterator[A] = underlying.iterator
    def apply(i : Int) = underlying.get(i)
    def update(i : Int, elem : A) = underlying.set(i, elem)
    def +=:(elem : A) = { underlying.subList(0, 0).add(elem) ; this } 
    def +=(elem : A): this.type = { underlying.add(elem); this }
    def insertAll(i : Int, elems : Traversable[A]) = { val ins = underlying.subList(0, i) ;  elems.foreach(ins.add(_)) }
    def remove(i : Int) = underlying.remove(i)
    def clear = underlying.clear
    def result = this
  }

要素をコピーしているわけではないので、変換するときにはあまりコストはかかりません。
しかし、当たり前といえば当たり前なんですが、要素を削除したり、追加したりした場合には、もとのjava.util.Listのほうにも、削除や追加が反映されます。
以下REPLでの実行結果

scala> import scala.collection.JavaConversions._  //自動で変換するためにimport
import scala.collection.JavaConversions._  

scala> val jlist = new java.util.ArrayList[String]()  //javaのList作成
jlist: java.util.ArrayList[String] = []

scala> jlist.add("a")  // 要素追加
res0: Boolean = true

scala> jlist //中身確かめる
res1: java.util.ArrayList[String] = [a] // "a" という要素が1つだけ入ってる

scala> val scalaBuffer:collection.mutable.Buffer[String] = jlist  //scalaのBufferに変換(というかラップ)
scalaBuffer: scala.collection.mutable.Buffer[String] = Buffer(a)

scala> scalaBuffer += "b"  //新しく作ったscalaのBufferのほうに、要素追加
res2: scalaBuffer.type = Buffer(a, b) //もちろん要素2つになってる

scala> jlist
res3: java.util.ArrayList[String] = [a, b]  //こっちも要素2つになってる

ここまで一般的な説明で、以下は、この話題に関係あるような、あまり関係ないような、実際に起きた出来事のメモ・・・

昨日引っかかったのが、javaのあるライブラリを使っていて、java.util.Listを返すメソッドがあったのですが、以下のように使っていました。

//hogeもfugaもjava.util.Listを返すメソッド

( obj.hoge() ++ obj.fuga() ) map {
 
  //何らかの処理
}

上記のようなコードだと、obj.hoge()で返されたjavaのListに要素が追加されます。一見何も問題なさそうなんですが、なんと、obj.hoge()の戻り値がメモ化(キャッシュか?)されていて、hogeのメソッドで返したListへの参照をインスタンスの内部に持っていたという・・・(´・ω・`)

//そのライブラリの中身をすごく単純にして一部抜き出すとこんな感じ・・・

private List list; //インスタンス変数に保持してる

public List hoge() {
  
  if( list == null ){ //存在してなかったら
    
    // Listつくってかえしてる

  }else{
    return list; //存在してたらそれをそのまま返す
  }
}

で、hogeで返したListが変更されてしまっているので、もういちどhogeを呼び出したときにおかしなListが返ってきて、それの原因がわからず数時間悩んでました(´・ω・`)

ていうか、このライブラリの設計が悪いのか・・・自分がちゃんと使い方調べなかった悪いのか・・・でもソースコードみないと、メモ化されてるなんてわかりそうになかったし・・・普通こんな設計するものなのか・・・?

*1:すでに、scalaのcollectionに変換されてるやつ?