S-JIS[2011-01-04/2013-06-08] 変更履歴
Javaで言うジェネリクス(総称型)は、Scalaでは「パラメータ化された型(parameterized types)」と呼ぶらしい。
(でもなんか長ったらしいので、自分はひとまずジェネリクスと呼ぶことにする^^;)
Scalaのジェネリクスは、型パラメーター(型引数)や型名を角括弧「[]
」で囲む。(Javaは「<>
」で囲む)
Scalaでは、型名を囲む必要がある場合は全て角括弧に統一されているようだ。(classOf[]
やisInstanceOf[]
等)
|
|
|
クラス・トレイトのジェネリクスは、クラス名(トレイト名)の後ろに角括弧で型パラメーターを記述する。
class クラス名[型パラメーター, …] 〜 trait トレイト名[型パラメーター, …] 〜
Scalaの例 | Java相当 | 備考 |
---|---|---|
class Example[T] { protected var t: T = _ def set(t: T): Unit = { this.t = t } def get(): T = { t } } |
public class Example<T> { protected T t; public void set(T t) { this.t = t; } public T get() { return t; } } |
型パラメーターが1つのクラスを定義した例。 |
val s = new Example[String] s.set("abc") val v = s.get() |
final Example<String> s = new Example<String>(); s.set("abc"); final String v = s.get(); |
使う際に型パラメーターを指定する例。 |
def f(s:Example[_]) = println(s) |
public void f(Example<?> s) { System.out.println(s); } |
使う際に型パラメーターが何でもいい場合は、「_」を指定する。 |
class Example[T] extends Parent[T] { } |
public class Example<T> extends Parent<T> { } |
親クラスの型パラメーターに自分の型パラメーターを指定する例。 |
class Example[T, U] { def method1(t: T): Unit = { println(t) } def method2(u: U): Unit = { println(u) } } |
public class Example<T, U> { public void method1(T t) { System.out.println(t); } public void method2(U u) { System.out.println(u); } } |
型パラメーターが2つのクラスを定義した例。 |
val s = new Example[String, Int] val s = new Example[String, Int]() |
final Example<String, Integer> s = new Example<Stirng, Integer>(); |
型パラメーターが2つのクラスのインスタンスを生成する例。 |
val s = new (String Example Int) val s = new (String Example Int)() |
型パラメーターが2つのクラスをnewでインスタンス生成する場合、クラス名を中央に置く書き方(中置記法)が出来る。 (まるで演算子のようになる。) 参考: okomokさんのOr.scala new (A op B) は、AやBのインスタンスを生成するのではなく、opのインスタンスを生成している。 |
|
scala> class op[L, R] defined class op scala> class A; class B; class C defined class A defined class B defined class C scala> new (A op B) res1: op[A,B] = op@138d2fc scala> new (A op B op C) res2: op[op[A,B],C] = op@d76d51 |
メソッドのみに適用するジェネリクスは、メソッド名の後ろに角括弧で型パラメーターを記述する。
def メソッド名[型パラメーター, …](引数, …) 〜
Scalaの例 | Java相当 | 備考 |
---|---|---|
class Example { def method[T](t: T): T = { t } } |
public class Example { public <T> T method(T t) { return t; } } |
型パラメーターが1つのクラスを定義した例。 |
val obj = new Example val s = obj.method("abc") |
final Example obj = new Example(); final String s = obj.method("abc"); |
メソッドのジェネリクスは、JavaでもScalaでも型推論してくれる。 |
val s = obj.method[String](null) |
final String s = obj.<String>method(null); |
型パラメーターを明示して呼び出す例。 |
型パラメーターの上限境界(upper bound、Javaのextends)・下限境界(lower
bound、Javaのsuper?)について。
型パラメーターは、指定できるクラスの上限・下限(親クラス・子クラス)の範囲を指定することが出来る。
例として、A1←A2←A3という継承関係のクラスがあるとする。
class A1 class A2 extends A1 class A3 extends A2
Scalaの例 | Java相当 | 備考 |
---|---|---|
class Example[T] { } ○ new Example[Any] ○ new Example[A1] ○ new Example[A2] ○ new Example[A3] ○ new Example[Nothing] |
class Example<T> { } ○ new Example<Object>(); ○ new Example<A1>(); ○ new Example<A2>(); ○ new Example<A3>(); |
境界を何も指定しない例。 この場合、上限境界にAny、下限境界にNothingを指定したのと同じ。 |
class Example[T <: A2] { } × new Example[Any] × new Example[A1] ○ new Example[A2] ○ new Example[A3] ○ new Example[Nothing] |
class Example<T extends A2> { } × new Example<Object>(); × new Example<A1>(); ○ new Example<A2>(); ○ new Example<A3>(); |
上限境界を指定した例。 上限境界で指定したクラス自身とその派生クラスだけ受け付ける。 |
class Example[T >: A2] { } ○ new Example[Any] ○ new Example[A1] ○ new Example[A2] × new Example[A3] × new Example[Nothing] |
下限境界を指定した例。 下限境界で指定したクラス自身とその親クラスだけ受け付ける。 (Javaではextendsと同じようなsuperの指定は出来ない) |
|
class Example[T >: A3 <: A1] { } × new Example[Any] ○ new Example[A1] ○ new Example[A2] ○ new Example[A3] × new Example[Nothing] |
下限境界と上限境界を同時に指定した例。 この場合、順序を変えて「 T <: A1 >: A3 」と書くとエラーになる。 |
|
class Example[T <% scala.runtime.RichInt] { } × new Example[Any]
○ new Example[scala.runtime.RichInt]
○ new Example[Int]
○ new Example[Nothing]
|
可視境界(view bound)の例。 FからCへの暗黙変換(例:Int→RichInt)が定義されている場合、 「 T <% C 」と書けば、Fも型パラメーターに指定できるようになる。→implicitへの書き換え |
|
//上限境界では、暗黙変換は無関係 class Example[T <: scala.runtime.RichInt] { } × new Example[Any]
○ new Example[scala.runtime.RichInt]
× new Example[Int]
○ new Example[Nothing]
|
||
class Example[T] def fu(s: Example[_ <: A2]) = println(s) × fu(new Example[Any]) × fu(new Example[A1]) ○ fu(new Example[A2]) ○ fu(new Example[A3]) ○ fu(new Example[Nothing]) def fl(s: Example[_ >: A2]) = println(s) ○ fl(new Example[Any]) ○ fl(new Example[A1]) ○ fl(new Example[A2]) × fl(new Example[A3]) × fl(new Example[Nothing]) def fb(s: Example[_ >: A3 <: A1]) = println(s) × fb(new Example[Any]) ○ fb(new Example[A1]) ○ fb(new Example[A2]) ○ fb(new Example[A3]) × fb(new Example[Nothing]) |
class Example<T> {} public void fu(Example<? extends A2> s) { System.out.println(s); } public void fl(Example<? super A2> s) { System.out.println(s); } |
メソッドの引数などで使用する際に境界を指定する事も出来る。 (Javaでは、extendsとsuperを同時に指定する事は出来ない) この使い方の場合、「<%」を指定する事は出来ないようだ。 |
import java.io._ class Example[T <: Serializable with Closeable] { } class SC extends Serializable with Closeable { def close() = {} } new Example[SC] |
import java.io.*; class Example<T extends Serializable & Closeable> { } class SC implements Serializable, Closeable { @Override public void close() throws IOException {} } new Example<SC>(); |
Javaでは、複数のインターフェースを継承していることを示したい場合は「&」でつなぐ。 Scalaでは、素直に“withでつないだ新しいクラス(トレイト)”を上限境界に指定すれば良さそう。 |
class Example[T <: { def close():Unit }] { } |
型パラメーターには、クラス名そのものだけではなく、クラス定義を直接指定することが出来る。 ただし、これとwithを同時に使うことは出来ないようだ。 |
|
class Example[T : Hoge] { } |
Scala2.8で導入されたcontext bound。 |
→type(抽象タイプ)での上限境界(<:
)・下限境界(>:
)
型パラメーターが特定のクラスのときだけ呼び出せるようなメソッドを作る(そういう制限をする)ことが出来る。[2011-07-24]
これをGeneralized Type Constraintsと言う。(日本語訳では何と呼ぶのか定まっていないようなので、造語として「型パラメーター制約」と呼ぶことにする)
どういうときに使うかと言うと、ListのtoMapメソッドの例が分かり易い。
Listの定義は(単純化して言うと)「class List[A]」なので、Listの要素にはどんなクラスでも指定することが出来る。
ところがtoMapメソッドは、要素がTuple2のときだけしか使えない。
“使えない”と言うのは、実行時に例外が発生するという意味ではなく、Tuple2以外の型だったらコンパイルエラーになるということ。
これを型パラメーターの制約で実現している。
class List[A] { def toMap[K,V](implicit ev: A <:< (K,V)): Map[K,V] = 〜 }
「<:<
」が制約を表している。
「A <:< (K,V)
」は、「Aが(K,V)
(タプル)の子クラスである(タプルを継承している)必要がある」という制約を意味する。
そういう条件を満たすような暗黙の値を探し、見つかればOK(コンパイルが通る)、見つからなければNG(コンパイルエラー)になるという仕組み。
ちなみに引数の変数名の「ev」はevidence(証拠)の事らしい。実行時には何らかのインスタンスが渡ってくるので、それが渡ってくるのは「コンパイルが通った証拠」というようなニュアンスなんだろうか。
記号 | 説明 |
---|---|
A =:= B |
AとBが等しい。 |
A <:< B |
AはBの子クラス(サブクラス)である。 |
A <%< B |
Aは暗黙変換によってBになれる。 |
なお、これらの記号の実態は、Predefに定義されたクラス。(Predefに定義されているので、自動的に使える状態になっている)
(型パラメーターが2個のクラスでは中置記法が使える。つまり「C[A,B]
」は「A C B
」と書ける。Cが記号なら「=:=[A,B]
」「A
=:= B
」ということになる)
これらに適合させる為の暗黙変換関数もPredef内に定義されている。
Javaでは配列は共変なので、String配列(String[]
)をObject配列の変数(Object[]
)に代入することが出来る。
また、Javaのジェネリクスは共変でないので、例えばStringのList(List<String>
)はObjectのListの変数(List<Object>
)に代入することは出来ない。
(StringはObjectを継承しているが、StringのコレクションはObjectのコレクションに代入できるか?という問題。なお、Javaでは配列が共変なのは危険だとされている。
Scalaの配列はJavaと異なり、非変である)
Scalaでは、変位指定(変位指定アノテーション、variance annotations)によって非変(nonvariant)・共変(covariant)・反変(contravariant)を指定することが出来る。
例として、A1←A2←A3という継承関係のクラスがあるとする。
class A1 class A2 extends A1 class A3 extends A2
Scalaの例 | Java相当 | 備考 |
---|---|---|
class MyList[T] { } var list:MyList[A2] = _ × list = new MyList[A1] ○ list = new MyList[A2] × list = new MyList[A3] |
class MyList<T> { } MyList<A2> list = null; × list = new MyList<A1>(); ○ list = new MyList<A2>(); × list = new MyList<A3>(); |
型パラメーターに特に何も指定しないと、非変(nonvariant)になる。 この場合、変数に指定されている型にしか代入できない。 (ちなみに、ScalaのListは(不変オブジェクトなので)共変) |
List<String> list = new ArrayList<String>(); |
||
class MyArray[+T] { } var array:MyArray[A2] = _ × array = new MyArray[A1] ○ array = new MyArray[A2] ○ array = new MyArray[A3] |
A2[] array = null; × array = new A1[] {}; ○ array = new A2[] {}; ○ array = new A3[] {}; |
型パラメーターの前に「+」(共変アノテーション)を付けると共変(covariant)になる。 子クラスのコレクションを親クラスのコレクションに代入できる。 関連?→共変戻り値型 |
Object[] array = new String[] {}; |
||
class MyList[-T] { } var list:MyList[A2] = _ ○ list = new MyList[A1] ○ list = new MyList[A2] × list = new MyList[A3] |
型パラメーターの前に「-」(反変アノテーション)を付けると反変(contravariant)になる。 親クラスのコレクションを子クラスのコレクションに代入できる。 |
参考: Naotsuguさんの型パラメータの変位指定 …特に最後のまとめが使い分けの参考になる
Javaのジェネリクスでは出来ない事がある。Scalaではどうか?[/2013-06-08]
Scala相当 | Javaの例 | 備考 | |
---|---|---|---|
クラス取得 |
//×出来ない(コンパイルエラー) class Example[T] { def printClass(): Unit = { val c = classOf[T] println(c) } } |
//×出来ない(コンパイルエラー) public class Example<T> { public void printClass() { final Class c = T.class; System.out.println(c); } } |
型パラメーターからjava.lang.Classを取得することは出来ない。 |
class Example[T](implicit m: ClassTag[T]) { def printClass(): Unit = { val c = m.runtimeClass println(c) } } val s = new Example[String] s.printClass() |
ScalaではClassTagを使えば型パラメーターを受け取ることが出来るので、 そこからjava.lang.Class(型消去されたもの)を取得できる。 |
||
new |
//×出来ない(コンパイルエラー) class Example[T] { def create(): T = { new T() } } |
//×出来ない(コンパイルエラー) public class Example<T> { public T create() { return new T(); } } |
型パラメーターからnewでインスタンスを生成する事は出来ない。 |
class Example[T](implicit m: ClassTag[T]) { def create(): T = { m.runtimeClass.newInstance().asInstanceOf[T] } } val s = new Example[String] val v = s.create() |
ClassTagを使えばjava.lang.Classが取得できるので、 後はリフレクション(newInstance())を用いればインスタンスを生成できる。 ただし、型パラメーターに指定するクラスに引数無しのpublicコンストラクターが必要。 (例えばIntは生成できない) |
||
class Example[T](implicit m: ClassTag[T]) { def create(size:Int): Array[T] = { m.newArray(size) } } val s = new Example[Int] val a = s.create(10) |
//×出来ない(コンパイルエラー) public class Example<T> { public T[] create(int size) { return new T[size]; } } |
配列の生成なら、ClassTagにそれ専用のメソッドがある。 | |
クラス判断 |
//×コンパイルは通るが、正しく動作しない class Example[T] { def check(o:AnyRef): Boolean = { o.isInstanceOf[T] } } |
//×出来ない(コンパイルエラー) public class Example<T> { public boolean check(Object o) { return (o instanceof T); } } |
型パラメーターを使って、インスタンスがその型かどうかを判断する事は出来ない。 |
def check(o:AnyRef): Boolean = o match { case _:T => true case _ => false } |
instanceOfでの判断と同様、期待した動作にならない。[2011-06-25] | ||
class Example[T](implicit m: ClassTag[T]) { def check(o:AnyRef): Boolean = { (o ne null) && m.runtimeClass.isInstance(o) } } val d = new Example[java.util.Date] assert( d.check(new java.util.Date()) ) assert( d.check(new java.sql.Date(0)) ) assert(!d.check("abc") ) assert(!d.check(null) ) val q = new Example[java.sql.Date] assert( q.check(new java.sql.Date(0)) ) assert(!q.check(new java.util.Date()) ) |
ClassTagを使えばjava.lang.Classが取得できるので、 リフレクション(isInstance())を用いればクラスを判断できる。 |
||
class Example[T](implicit m: ClassTag[T]) { def check[S](o:S)(implicit s: ClassTag[S]): Boolean = { m.runtimeClass.isAssignableFrom(s.runtimeClass) } } |
双方のClassTagを取得してみた例。 | ||
static |
//×出来ない(コンパイルエラー) public class Example<T> { private static T value; }
|
Javaではstaticなフィールドやメソッドにはクラスの型パラメーターを適用できない。 が、Scalaにはstaticなメンバーという概念は無いので、無関係。 |