Javaとの相互運用
ScalaとJava
ScalaはJVM(Java Virtual Machine)の上で動作するため、JavaのライブラリのほとんどをそのままScalaから呼びだすことができます。また、現状では、Scalaの標準ライブラリだけでは、どうしても必要な機能が足りず、Javaの機能を利用せざるを得ないことがあります。ただし、Javaの機能と言っても、Scalaのそれとほとんど同じように利用することができます。
import
Javaのライブラリをimportするためには、Scalaでほとんど同様のことを記述すればOKです。
import java.util.*;
import java.util.ArrayList;
ワイルドカードインポートはScala 2では_
を、Scala 3では*
を使います。
import java.util._
import java.util.ArrayList
インスタンスの生成
インスタンスの生成もJavaと同様にできます。Javaでの
ArrayList<String> list = new ArrayList<>();
というコードはScalaでは
val list = new ArrayList[String]()
// list: ArrayList[String] = [Hello, World]
と記述することができます。
練習問題
java.util.HashSet
クラスのインスタンスをnew
を使って生成してみましょう。
インスタンスメソッドの呼び出し
インスタンスメソッドの呼び出しも同様です。
list.add("Hello");
list.add("World");
は
list.add("Hello")
// res0: Boolean = true
list.add("World")
// res1: Boolean = true
と同じです。
練習問題
java.lang.System
クラスのフィールド out
のインスタンスメソッド println
を引数 "Hello, World!"
として呼びだしてみましょう。
staticメソッドの呼び出し
staticメソッドの呼び出しもJavaの場合とほとんど同様にできますが、1つ注意点があります。それは、Scalaではstaticメソッドは継承されない(というよりstaticメソッドという概念がない)ということです。これは、クラスAがstaticメソッドfooを持っていたとして、Aを継承したBに対してB.foo()とすることはできず、A.foo()としなければならないという事を意味します。それ以外の点についてはJavaの場合とほぼ同じです。
現在時刻をミリ秒単位で取得するSystem.currentTimeMillis()
をScalaから呼び出してみましょう。
scala> System.currentTimeMillis()
res0: Long = 1416357548906
表示される値はみなさんのマシンにおける時刻に合わせて変わりますが、問題なく呼び出せているはずです。
練習問題
java.lang.System
クラスのstaticメソッドexit()
を引数 0
として呼びだしてみましょう。どのような結果になるでしょうか。
staticフィールドの参照
staticフィールドの参照もJavaの場合と基本的に同じですが、staticメソッドの場合と同じ注意点が当てはまります。つまり、staticフィールドは継承されない、ということです。たとえば、Javaでは JFrame.EXIT_ON_CLOSE
が継承されることを利用して、
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame() {
setDefaultCloseOperation(EXIT_ON_CLOSE); //JFrameを継承しているので、EXIT_ON_CLOSEだけでOK
}
}
のようなコードを書くことができますが、Scalaでは同じように書くことができず、
scala> import javax.swing.JFrame
class MyFrame extends JFrame {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) //JFrame.を明示しなければならない
}
のように書く必要があります。
現実のプログラミングでは、Scalaの標準ライブラリだけでは必要なライブラリが不足している場面に多々遭遇しますが、そういう場合は既にあるサードパーティのScalaライブラリかJavaライブラリを直接呼びだすのが基本になります。
練習問題
ScalaでJavaのstaticフィールドを参照しなければならない局面を1つ以上挙げてみましょう。
Scalaの型とJavaの型のマッピング
Javaの型は適切にScalaにマッピングされます。たとえば、System.currentTimeMillis()
が返す型はlong型ですが、Scalaの標準の型であるscala.Long
にマッピングされます。Scalaの型とJavaの型のマッピングは次のようになります。
Javaの型 | Scalaの型 |
void (厳密にはJavaでvoidは型ではなくただのキーワードとして扱われていますが、ここでは便宜上型としています) | scala.Unit |
boolean | scala.Boolean |
byte | scala.Byte |
short | scala.Short |
int | scala.Int |
long | scala.Long |
char | scala.Char |
float | scala.Float |
double | scala.Double |
java.lang.Object(プリミティブ型ではありませんが特別な型なので載せました) | scala.AnyRef |
java.lang.String | java.lang.String |
Javaのすべてのプリミティブ型に対応するScalaの型が用意されていることがわかりますね! また、java.lang
パッケージにあるクラスは全てScalaからimport無しに使えます。
また、参照型についてもJava同様にクラス階層の中に組み込まれています。たとえば、Javaで言うint[]
はArray[Int]
と書きますが、これはAnyRef
のサブクラスです。ということは、Scala
でAnyRef
と書くことでArray[Int]
をAnyRef
型の変数に代入可能です。ユーザが定義したクラスも同様で、基本的にAnyRef
を継承していることになっています。(ただし、value classというものがあり、それを使った場合は少し事情が異なりますがここでは詳細には触れません)
nullとOption
Scalaの世界ではnullを使うことはなく、代わりにOption型を使います。一方で、Javaのメソッドを呼び出したりすると、返り値としてnullが返ってくることがあります。Scalaの世界ではできるだけnullを取り扱いたくないのでこれは少し困ったことです。幸いにも、ScalaではOption(value)
とすることで、value
がnullのときはNone
が、nullでないときはSome(value)
を返すようにできます。
java.util.Mapを使って確かめてみましょう。
val map = new java.util.HashMap[String, Int]()
// map: HashMap[String, Int] = {A=1, B=2, C=3}
map.put("A", 1)
// res2: Int = 0
map.put("B", 2)
// res3: Int = 0
map.put("C", 3)
// res4: Int = 0
Option(map.get("A"))
// res5: Option[Int] = Some(value = 1)
Option(map.get("B"))
// res6: Option[Int] = Some(value = 2)
Option(map.get("C"))
// res7: Option[Int] = Some(value = 3)
Option(map.get("D"))
// res8: Option[Int] = None
ちゃんとnullがOptionにラップされていることがわかります。Scalaの世界からJavaのメソッドを呼びだすときは、返り値をできるだけ Option()でくるむように意識しましょう。
scala.jdk.CollectionConverters
JavaのコレクションとScalaのコレクションはインタフェースに互換性がありません。これでは、ScalaのコレクションをJavaのコレクションに渡したり、逆に返ってきたJavaのコレクションをScalaのコレクションに変換したい場合に不便です。そのような場合に便利なのがscala.jdk.CollectionConverters
です。
Scala 2.12以前は同様の機能はscala.collection.JavaConverters
で提供されていましたが、Scala 2.13以降はそれが非推奨になりました。使い方はいたって簡単で、
import scala.jdk.CollectionConverters._
とするだけです。これで、JavaとScalaのコレクションのそれぞれにasJava()やasScala()といったメソッドが追加されるのでそのメソッドを以下のように呼び出せば良いです。
import scala.jdk.CollectionConverters._
import java.util.ArrayList
val list = new ArrayList[String]()
// list: ArrayList[String] = [A, B]
list.add("A")
// res9: Boolean = true
list.add("B")
// res10: Boolean = true
val scalaList = list.asScala
// scalaList: collection.mutable.Buffer[String] = Buffer("A", "B")
BufferはScalaの変更可能なリストのスーパークラスですが、ともあれ、asScalaメソッドによってJavaのコレクションをScalaのそれに変換することができていることがわかります。そのほかのコレクションについても同様に変換できますが、詳しくはAPIドキュメントを参照してください。
また、scala.jdk
パッケージには、コレクションの変換以外の機能も提供されています。
練習問題
scala.collection.mutable.ArrayBuffer
型の値を生成してから、scala.jdk.CollectionConverters
を使ってjava.util.List型に変換してみましょう。なお、ArrayBuffer
には1つ以上の要素を入れておくこととします。
ワイルドカードと存在型
Javaでは、
import java.util.List;
import java.util.ArrayList;
List<? extends Object> objects = new ArrayList<String>();
のようにして、クラス宣言時には不変であった型パラメータを共変にしたり、
import java.util.Comparator;
Comparator<? super String> cmp = new Comparator<Object>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
のようにして反変にすることができます。ここで、? extends Object
の部分を共変ワイルドカード、
? super String
の部分を反変ワイルドカードと呼びます。より一般的には、このような機能を、利用側で変位指定するという意味でuse-site varianceと呼びます。
この機能に対応するものとして、Scalaには存在型があります。上記のJavaコードは、Scalaでは次のコードで表現することができます。
import java.util.{List => JList, ArrayList => JArrayList}
val objects: JList[? <: Object] = new JArrayList[String]()
// objects: List[?$1] = []
import java.util.{Comparator => JComparator}
val cmp: JComparator[? >: String] = new JComparator[Any] {
override def compare(o1: Any, o2: Any): Int = {
o1.hashCode() - o2.hashCode()
}
}
// cmp: Comparator[?$2] = repl.MdocSession$MdocApp$$anon$1@6466699
より一般的には、G<? extends T>
は G[? <: T]
に、G<? super T>
は G[? >: T]
に置き換えることができます。Scalaのプログラム開発において、Javaのワイルドカードを含んだ型を扱いたい場合は、この機能を使いましょう。一方で、Scalaプログラムでは定義側の変位指定、つまりdeclaration-site varianceを使うべきであって、Javaと関係ない部分においてこの機能を使うのはプログラムをわかりにくくするため、避けるべきです。
SAM変換
Scala 2.12ではSAM(Single Abstract Method)変換が導入され1、Java 8のラムダ式を想定したライブラリを簡単に利用できるようになりました。 Java 8におけるラムダ式とは、関数型インタフェースと呼ばれる、メソッドが1つしかないようなインタフェースに対して無名クラスを簡単に記述できる構文です2。例えば、10の階乗を例にすると以下のように簡潔に書くことができます。
import java.util.stream.IntStream;
int factorial10 = IntStream.rangeClosed(1, 10).reduce(1, (i1, i2) -> i1 * i2);
ちなみに、これをラムダ式を使わずに書くと、以下のようにとても大変です。
import java.util.stream.IntStream;
import java.util.function.IntBinaryOperator;
int factorial10 = IntStream.rangeClosed(1, 10).reduce(1,
new IntBinaryOperator() {
@Override public int applyAsInt(int left, int right) {
return left * right;
}
});
関数の章で説明したように、元々Scalaにもラムダ式に相当する無名関数という構文があります。しかし、以前のScalaではFunctionN
型が期待される箇所に限定されており、Javaにおいてラムダ式が期待される箇所の大半において使用することができませんでした。例えば、10の階乗の例はIntBinaryOperator
型が期待されているので以下のように無名クラスを使う必要がありました。
import java.util.stream.IntStream;
import java.util.function.IntBinaryOperator;
val factorial10 = IntStream.rangeClosed(1, 10).reduce(1,
new IntBinaryOperator {
def applyAsInt(left: Int, right: Int) = left * right;
});
// factorial10: Int = 3628800
SAM変換を利用すると以下のようにここにも無名関数を利用できるようになります。
import java.util.stream.IntStream;
val factorial10 = IntStream.rangeClosed(1, 10).reduce(1, _ * _);
// factorial10: Int = 3628800
1. 正確には-Xexperimetal
オプションにより、Scala 2.11でもSAM変換を有効にすることができます。 ↩
2. 厳密に言うと、無名クラスを用いたコードとラムダ式もしくは無名関数を用いたコードの間には、JavaとScalaいずれにおいても細かな違いが存在します。例えば、スコープや出力されるバイトコードなどです。より詳しくは言語仕様などを当たってみてください。 ↩