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の型のマッピング
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いずれにおいても細かな違いが存在します。例えば、スコープや出力されるバイトコードなどです。より詳しくは言語仕様などを当たってみてください。

results matching ""

    No results matching ""