S-JIS[2011-02-18/2011-04-17] 変更履歴

Scala Javaコレクション変換

ScalaコレクションJavaのコレクションの変換について。


概要

ScalaではJavaのコレクション(java.util.Listとか)をそのまま使用する事が出来るが、さらにScalaのコレクションとして扱う 為のラッパークラスも提供されている。

バージョン オブジェクト名 備考
Scala2.8.0 JavaConversions 暗黙変換メソッド及びラッパークラスを提供している。
Scala2.8.1 JavaConverters 変換メソッドを提供している。ラッパークラスはJavaConversionsの物を使用。

JavaConversions

Scala2.8.0では、scala.collection.JavaConversionsオブジェクトのメソッドをインポートしておくと、変換を自動的に行ってくれるようになる。
したがって、Javaコレクションに対しScalaコレクションのメソッドを適用すれば、JavaコレクションからScalaコレクション(のラッパークラス)に暗黙に変換される。

scala> import scala.collection.JavaConversions._
import scala.collection.JavaConversions._
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> jl += 123
res0: scala.collection.mutable.Buffer[Int] = Buffer(123)

scala> jl ++= List(4,5,6)
res1: scala.collection.mutable.Buffer[Int] = Buffer(123, 4, 5, 6)

scala> jl
res2: java.util.ArrayList[Int] = [123, 4, 5, 6]
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> val sl:scala.collection.mutable.Buffer[Int] = jl //Scalaのコレクションへ変換(ラッパーが作られる)
sl: scala.collection.mutable.Buffer[Int] = Buffer()

scala> sl += 123
res3: sl.type = Buffer(123)

scala> sl ++= List(4,5,6)
res4: sl.type = Buffer(123, 4, 5, 6)

scala> sl
res5: scala.collection.mutable.Buffer[Int] = Buffer(123, 4, 5, 6)

scala> jl
res6: java.util.ArrayList[Int] = [123, 4, 5, 6]

Scalaのコレクションに変換しても、あくまでJavaのコレクションのラッパーなので、
Scalaコレクションに対する更新はJavaコレクションにも影響を与える。


JavaConverters

Scala2.8.1では、scala.collection.JavaConvertersオブジェクトのメソッドをインポートしておくと、変換メソッド(asScalaやasJava)が呼び出せるようになる。
つまり、変換(ラッパークラスの作成)をプラグラマーが明示的に行うことになる。
ラッパークラス自体はJavaConversionsのものが使用される。(ラッパークラスに対する更新はJavaコレクションにも影響する)

scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> jl += 123	//JavaConvertersでは、Javaコレクションに対してScalaコレクションのメソッドを直接呼び出す事は出来ない
<console>:10: error: reassignment to val
       jl += 123
          ^
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> val sl = jl.asScala
sl: scala.collection.mutable.Buffer[Int] = Buffer()

scala> sl += 123
res0: sl.type = Buffer(123)

scala> sl ++= List(4,5,6)
res1: sl.type = Buffer(123, 4, 5, 6)

scala> sl
res2: scala.collection.mutable.Buffer[Int] = Buffer(123, 4, 5, 6)

scala> jl
res3: java.util.ArrayList[Int] = [123, 4, 5, 6]

明示的に変換する際にBufferをインポートしておく必要も無いし、
ラッパークラスも一度しか作られない(JavaConversionsの方法ではメソッドを適用する度に暗黙変換メソッド内でラッパーインスタンスが作られる事になる)し、
JavaConversionsよりもこちらの方が良い感じかも。

よく使われるjava.util.List・Map・Setの他に、java.util.Iterator・Enumeration・Iterable等の繰り返し用クラスも変換できる。[2011-04-17]

scala> new java.util.Scanner("a b c").asScala.toList	//ScannerはIteratorを実装している
res11: List[java.lang.String] = List(a, b, c)

scala> val seq = Seq(1,2,3)
seq: Seq[Int] = List(1, 2, 3)

scala> seq.asJava
res12: java.util.List[Int] = [1, 2, 3]

++メソッドの注意(cloneの問題)

xuweiさんのscalaでjavaのList使うときの話によると、java.util.Listに対するラッパークラスに++メソッドを適用すると、元のJavaのコレクションも変わってしまうらしい。
通常のBuffer(ArrayBufferやListBuffer)では++メソッドは元のコレクションには影響を与えない(自分自身に追加する場合は++=メソッドを使う)ので、これは非常に紛らわしい。バグと言ってもいいんじゃないかと思うくらい。
++:---も同様のようだ。(++:はBufferにおいては非推奨メソッドだが)

クラス 備考
ListBuffer
scala> val lb = scala.collection.mutable.ListBuffer(1,2)
lb: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2)

scala> lb ++ List(3,4)
res11: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2, 3, 4)

scala> lb
res12: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2)
通常のListBufferでは、++を呼んでも元のコレクションは変化しない。
ArrayBuffer
scala> val ab = scala.collection.mutable.ArrayBuffer(1,2)
ab: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2)

scala> ab ++ List(3,4)
res13: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2, 3, 4)

scala> ab
res14: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2)
通常のArrayBufferでも、元のコレクションは変化しない。
ラッパー
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> jl.add(1); jl.add(2)

scala> jl
res15: java.util.ArrayList[Int] = [1, 2]

scala> val sl = jl.asScala
sl: scala.collection.mutable.Buffer[Int] = Buffer(1, 2)

scala> sl ++ List(3,4)
res16: scala.collection.mutable.Buffer[Int] = Buffer(1, 2, 3, 4)

scala> sl
res17: scala.collection.mutable.Buffer[Int] = Buffer(1, 2, 3, 4)

scala> jl
res18: java.util.ArrayList[Int] = [1, 2, 3, 4]
ラッパークラスでは++を呼ぶと元のコレクションも変わってしまう。

ここではJavaConvertersのasScalaを用いてラッパーを作っているが、
JavaConversionsを使う場合でも同じ。
(ラッパークラス自体は同じだから)

java.uitl.Listのラッパークラスは以下のようになっている。

object JavaConversions {
  import java.{ lang => jl, util => ju }
〜
  case class JListWrapper[A](val underlying : ju.List[A]) extends mutable.Buffer[A] {
    〜
  }
〜
}

++メソッド自体はBufferLikeトレイト(Bufferがミックスインしている)に定義されている。

trait BufferLike[A, +This <: BufferLike[A, This] with Buffer[A]] 〜 with Cloneable[This] 〜

  @migration(2, 8,
    "As of 2.8, ++ always creates a new collection, even on Buffers.\n"+
    "Use ++= instead if you intend to add by side effect to an existing collection.\n"
  )
  def ++(xs: TraversableOnce[A]): This = clone() ++= xs
〜
}

つまり、自分自身のクローンを作成し、それに対して++=メソッドで要素を追加している。
少なくとも考え方としては、自分自身に影響しないようになっているわけだ。

ではclone()はどこで定義されているかというと、Cloneableトレイトにある。

@cloneable
trait Cloneable[+A <: AnyRef] {
  override def clone: A = super.clone().asInstanceOf[A]
}

結局super.clone()の呼び出しなので、JavaのObjectクラスのclone()の呼び出しとなる。

最初に戻って、JListWrapperクラスケースクラスなので、最初に渡されたjava.util.Listを内部で保持している。
クローンを作成すると、内部で保持しているフィールド(の参照)はそのまま新しいインスタンスにコピーされる。
したがって、ラッパークラス自体が別インスタンスになっても、参照しているjava.util.Listは同じものになる。
なので、「clone() ++= xs」で新しいインスタンスに対して追加しているように見えても、元のjava.util.Listの中身が書き換わってしまう。


試しにJListWrapperと同じ内容のクラスを定義し、clone()を実装してみた。

case class MyListWrapper[A](val underlying : java.util.List[A]) extends scala.collection.mutable.Buffer[A] {
  //clone()以外は基本的にJListWrapperと同じ
  def length = underlying.size
  override def isEmpty = underlying.isEmpty
  override def iterator: Iterator[A] = new scala.collection.JavaConversions.JIteratorWrapper(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

   override def clone() = new MyListWrapper(new java.util.ArrayList(underlying))
}
scala> val jl = new java.util.ArrayList[Int]
jl: java.util.ArrayList[Int] = []

scala> jl.add(1); jl.add(2)

scala> jl
res31: java.util.ArrayList[Int] = [1, 2]

scala> val ml = MyListWrapper(jl)
ml: MyListWrapper[Int] = Buffer(1, 2)

scala> ml ++ List(3,4)
res32: scala.collection.mutable.Buffer[Int] = Buffer(1, 2, 3, 4)

scala> ml
res33: MyListWrapper[Int] = Buffer(1, 2)

scala> jl
res34: java.util.ArrayList[Int] = [1, 2]

うん、ちゃんと元のListへの影響が無くなった!

ちなみにMyListWrapperのclone()メソッドでは、Listの新しいインスタンスを作るのに「new ArrayList(underlying)」としている。
本来なら「underlying.clone()」と出来ればいいんだけど、Listはclone()を持ってないんだよね…。
だからJListWrapperも独自のclone()を実装するのを諦めたんだろうか?

そもそも++メソッド(や++:---等、影響のありそうなメソッド全部)をオーバーライドして、元のListの要素をコピーした新しいBufferを返すようにすべきなのかも?
clone()でListの要素をコピーしたListBufferでも作って返せば多数のメソッドをオーバーライドする必要は無いけど、clone()が自分と異なるクラスを返すのもおかしい気がするし。


コレクションへ戻る / Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま