いつもJavaConvertersでうまくいくとは限らない

ScalaとJavaのコレクションを相互に変換するには scala.collection.JavaConverters が便利です。例えばJavaのコレクションに .asScala をつければ、JavaのコレクションをラップしたScalaのコレクションが作られます。ただし、いつも .asScala で済むとは限りません。コレクションのインターフェイスではなくデータ構造が重要な場合は注意が必要です。

次のコード例は java.util.LinkedHashMap を .asScala に変換して、 +=, + の操作を行ったものです。 LinkedHashMap は要素が挿入された順番を保持するはずですが、 += を使った場合は順番が保持されるものの、 + を使った場合は順番が保持されていません。

val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

m2 += "field3" -> 3
m2 += "field4" -> 4
println(m2) // Map(field1 -> 1, field2 -> 2, field3 -> 3, field4 -> 4)
val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

val m3 = m2 + ("field3" -> 3) + ("field4" -> 4)
println(m3) // Map(field1 -> 1, field3 -> 3, field2 -> 2, field4 -> 4)

+= を使った場合、新しい要素は元のLinkedHashMap に挿入されます。一方、 + を使った場合は新たな Map のインスタンスが作られます。その新たな Map のインスタンスは JavaConverters で定義してあるラッパークラスが作るのですが、それが LinkedHashMap ではなく順番を保持しない HashMap なのです。

これは実際にplay-json 2.6.11に入り込んだバグです。play-jsonはJsonのフィールドを Map として保持していますが、その Map をScalaのものから java.util.LinkedHashMap のインスタンスとして生成して、 JavaConverters でラップするという実装に変えたところ、Jsonのフィールドの順序が狂うという現象が起きました。

https://github.com/playframework/play-json/issues/236

というわけで、気をつけましょう。