S-JIS[2011-02-18/2011-04-17] 変更履歴
ScalaのコレクションとJavaのコレクションの変換について。
|
|
ScalaではJavaのコレクション(java.util.Listとか)をそのまま使用する事が出来るが、さらにScalaのコレクションとして扱う 為のラッパークラスも提供されている。
バージョン | オブジェクト名 | 備考 |
---|---|---|
Scala2.8.0 | JavaConversions | 暗黙変換メソッド及びラッパークラスを提供している。 |
Scala2.8.1 | JavaConverters | 変換メソッドを提供している。ラッパークラスは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コレクションにも影響を与える。
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]
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()が自分と異なるクラスを返すのもおかしい気がするし。