@xtccc
2016-08-11T16:45:08.000000Z
字数 8856
阅读 3107
Scala
参考链接:
ClassTag, Class and war stories…
需求1:希望写一个方法CloneInstance,能够复制输入的任何类型的对象,并构造一个新的实例输出。
假设,现在有Student和Teacher两个Java class,定义如下:
// Student.java文件
package cn.gridx.scala.example.generics;
/**
* Created by tao on 11/18/15.
*/
public class Student {
public String name;
public String city;
private String age;
public Student() {
name = null;
city = null;
age = null;
}
public Student(String name, String city, String age) {
this.name = name;
this.city = city;
this.age = age;
}
@Override
public String toString() {
return name + ", " + city + ", " + age;
}
}
// Teacher.java文件
package cn.gridx.scala.example.generics;
/**
* Created by tao on 11/18/15.
*/
public class Teacher {
public String name;
public String home;
public String gender;
public Teacher() {
name = null;
home = null;
gender = null;
}
public Teacher(String name, String home, String gender) {
this.name = name;
this.home = home;
this.gender = gender;
}
@Override
public String toString() {
return name + ", " + home + ", " + gender;
}
}
我们首先从直觉出发,编写一个如下的函数:
package cn.gridx.scala.example.generics
/**
* Created by tao on 11/18/15.
*
* 范型方法
*/
object GenericMethods {
def CloneInstance[T](obj:T):T = {
val instance = new T()
val fields = classOf[T].getDeclaredFields
for (f <- fields) {
f.setAccessible(true) // 防止不能访问private field
f.set(instance, f.get(obj))
}
instance
}
}
编译时会报错:
Error:(32, 28) class type required but T found
val instance = new T()
这说明编译器并不认为T是一个class type,既然用到了new T()
,那么T就必须是一个class type。
所以,我们需要借助于 ClassTag 这个关键字,如下实现:
package cn.gridx.scala.example.generics
import scala.reflect.ClassTag
/**
* Created by tao on 11/18/15.
*
* 范型方法
*/
object GenericMethods {
def main(args: Array[String]): Unit = {
val s = new Student("Jack", "Nanjing", "22")
val t = new Teacher("Tom", "Shanghai", "30")
// 必须用classOf[Student], 不能用s.getClass
println(CloneInstance(classOf[Student], s))
println(CloneInstance(classOf[Teacher], t))
}
def CloneInstance[T:ClassTag](clz:Class[T], obj:T): T = {
val instance = clz.newInstance()
val fields = clz.getDeclaredFields
for (f <- fields) {
f.setAccessible(true) // 防止不能访问private field
f.set(instance, f.get(obj))
}
instance
}
}
输出如下:
Jack, Nanjing, 22
Tom, Shanghai, 30
需求2:根据输入的class,创建相应的类的实例
例如:输入 classOf[Student] ,则输出一个 Student 实例;输入 classOf[Teacher] ,则输出一个 Teacher 实例。
从直觉上,我们可以借助于 match ... case 来实现,并写出如下代码:
def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
val obj = clz match {
case classOf[Student] => new Student("n", "c", "a")
case classOf[Teacher] => new Teacher("N", "H", "G")
case _ =>
}
obj
}
但是编译器会报错:
Error:(53, 18) not found: type classOf
case classOf[Student] => new Student("n", "c", "a")
^
分析:
换一种写法:
def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
val studentClzName = classOf[Student].getCanonicalName
val teacherClzName = classOf[Teacher].getCanonicalName
val obj = clz.getCanonicalName match {
case studentClzName => new Student("n", "c", "a")
case teacherClzName => new Teacher("N", "H", "G")
case _ =>
}
obj
}
编译器会给出警告:
Warning:(47, 18) patterns after a variable pattern cannot match (SLS 8.1.1)
case studentClzName => new Student("n", "c", "a")
^Warning:(48, 36) unreachable code due to variable pattern 'studentClzName' on line 47
case teacherClzName => new Teacher("N", "H", "G")
^Warning:(49, 26) unreachable code due to variable pattern 'studentClzName' on line 47
case _ =>
^Warning:(48, 36) unreachable code
case teacherClzName => new Teacher("N", "H", "G")
^
这意味着,下面两行代码是unreachable code:
case teacherClzName => new Teacher("N", "H", "G")
case _ =>
并且,不论CreateInstanceByClass的输入类型是什么,这个match ... case
总是会匹配到第一个case。我们可以验证这一点。
package cn.gridx.scala.example.generics
import scala.reflect.ClassTag
/**
* Created by tao on 11/18/15.
*
* 范型方法
*/
object GenericMethods {
def main(args: Array[String]): Unit = {
println(CreateInstanceByClass(classOf[Student]))
println("+"*10)
println(CreateInstanceByClass(classOf[Teacher]))
println("+"*10)
println(CreateInstanceByClass(classOf[String]))
}
def CreateInstanceByClass[T: ClassTag](clz:Class[T]): Any = {
val studentClzName = classOf[Student].getCanonicalName
val teacherClzName = classOf[Teacher].getCanonicalName
val obj = clz.getCanonicalName match {
case studentClzName => new Student("n", "c", "a")
case teacherClzName => new Teacher("N", "H", "G")
case _ =>
}
obj
}
}
输出为
n, c, a
++++++++++
n, c, a
++++++++++
n, c, a
分析:
这是由
match ... case
variable pattern 引起的问题,参考这里。
解决方法:
// 下面有2种解决方法
def CreateInstanceByClass_1[T: ClassTag](clz:Class[T]): Any = {
val studentClzName = classOf[Student].getCanonicalName
val teacherClzName = classOf[Teacher].getCanonicalName
val obj = clz.getCanonicalName match {
case `studentClzName` => new Student("n", "c", "a")
case `teacherClzName` => new Teacher("N", "H", "G")
case _ =>
}
obj
}
def CreateInstanceByClass_2[T: ClassTag](clz:Class[T]): Any = {
val studentClz = classOf[Student]
val teacherClz = classOf[Teacher]
val obj = clz match {
case `studentClz` => new Student("n", "c", "a")
case `teacherClz` => new Teacher("N", "H", "G")
case _ =>
}
obj
}
参考
Generics允许参数化类型(parameterised types),例如,Vector
的一个实例可以被声明为Vector<String>
。为了避免对运行时环境带来大的改动,范型是通过一种被称为type erasure的技术来实现的,即:擦除关于类型的信息,在必要的时候进行类型转换。
但是,数组是一个例外:可以对数组应用paramaterized types,但是数组不会进行类型擦除。
String[] vector = new String[3];
vector[0] = "0"; vector[1] = "1"; vector[2] = "2";
Object[] objects = vector;
/**
* objects[0] = new Object(); 编译正常,但是运行异常
* Exception in thread "main" java.lang.ArrayStoreException: java.lang.Object
*/
objects[0] = new Object();
Vector<String> vector = new Vector<String>();
Vector<Object> objects = (Vector<Object>)vector; // 无法转换: inconvertible types
public <T> T createGenericArray() {
return new T[10]; // Error: generic array creation
}
上面的方法无法通过编译,会报错:generic array creation。因为,数组不支持type erasure,但是参数化的类型T
在运行时是不存在的,编译器无法把一个运行时才有的类型赋给数组。
解决方法
通过java.lang.reflect.Array.newInstance
来创建数组。
public class JavaExamples {
public static void main(String[] args) {
GenericArray a = new GenericArray("Hello");
String[] A = (String[])a.createGenericArray();
for (int i=0; i<A.length; i++)
A[i] = String.valueOf(i);
for (int i=0; i<A.length; i++)
System.out.println(A[i]);
}
public static class GenericArray<T> {
private Class<T> tClz;
public GenericArray(T t) {
tClz = (Class<T>)t.getClass();
}
public T[] createGenericArray() {
// `java.lang.reflect.Array.newInstance`返回的类型是`Object[]`
return (T[])java.lang.reflect.Array.newInstance(tClz, 10);
}
}
}
参考:
作为JVM上的语言,Scala也是具有 type erasure at run-time 这个特性的。例如,如果我们希望在运行时对参数的类型进行比较,如下:
def meth1[T](xs: List[T]): Unit = {
xs match {
case _: List[String] => println("It is List[String]")
case _: List[Int] => println("It is List[Int]")
}
}
那么,meth1(List(1,2,3))
和 meth1("1","2","3")
都是会打印出 "It is List[String]"这句话的,因为在运行时,List[String]
中的String 和 List[Int]
中的Int都会被擦除,因为无法区分。
在2.10版本之后,Scala引入了一个新的reflection library,使得Scala同时具备了compile-time reflection和run-time reflection的能力。
什么是runtime reflection?在运行时,对于给定的一个type或者instance,通过reflection可以:
1. 获取该instance/type的类型(包括generic type)
2. 实例化一个新的object
3. 访问或者调用该object的成员
对于meth1
遇到的类型擦除问题,可以通过scala.reflect.runtime.universe.TypeTag
和scala.reflect.runtime.universe.typeOf
来解决:
def meth2[T: TypeTag](xs: List[T]) = {
typeOf[T] match {
case t if t =:= typeOf[String] => println("It is List[String]")
case t if t =:= typeOf[Int] => println("It is List[Int]")
}
}
这里,操作符=:=
用于比较两个type是否相等。
可以把TypeTag
s 看操作是这样的对象:它包含了关于type的所有信息(包括compile-time和run-time)。TypeTag总是是由编译器生成的(只要用到了要求TypeTage的implicit parameter或者context bound,就会触发对TypeTage的生成)。这意味着:要想获得一个TypeTag,必须使用implicit parameter或者context bound。
下面是一个使用context bound的例子。
scala> import scala.reflect.runtime.universe
import scala.reflect.runtime.universe
scala> val L = List(1,2,3)
L: List[Int] = List(1, 2, 3)
scala> def getTypeTag[T: universe.TypeTag](obj: T) = universe.typeTag[T]
getTypeTag: [T](obj: T)(implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T]
scala> val theType = getTypeTag(L).tpe
theType: reflect.runtime.universe.Type = List[Int]
这里,在定义getTypeTag
方法时,类型参数T
是有context bound的(从REPL可以看出,这相当于定义了一个隐式参数evidence
,因而导致编译器为类型T
生成了一个TypeTag)。而TypeTag的方法tpe
返回的是TypeTag包含的类型。可以看到,对于generic type,通过TypeTag也能获取到它的真实类型。
通过ClassTag,我们可以创建范型数组(generic array),如下:
def createGenericArray[T: scala.reflect.ClassTag](seq: T*): Array[T] = Array[T](seq: _*)
val A = createGenericArray(1,2,3,4)
val B = createGenericArray("X", "Y", "Z")
println(A.getClass + " -> " + A.mkString("[", ",", "]"))
println(B.getClass + " -> " + B.mkString("[", ",", "]"))
运行结果为
class [I -> [1,2,3,4]
class [Ljava.lang.String; -> [X,Y,Z]
如果对上述代码中的类型T
不用scala.reflect.ClassTag
来声明的话,是不能创建范型数组的。因为parameterized type在运行时遭遇到了type erasure,而创建数组时要求明确提供数组元素的类型。ClassTag的作用正是运行时提供创建类型所需的有关信息。
参考:
abstract class Fruit {
def name: String
}
class Orange extends Fruit {
def name: String = "orange"
}
class Apple extends Fruit {
def name: String = "apple"
}
class Box[F <: Fruit](f: Fruit) {
def getFruit = f
def contains(aFruit: Fruit): Boolean = f.name == aFruit.name
}
Box[Fruit], Box[Apple], Box[Orange]是三个class,这三个class之间实际上是没有任何关系的。虽然Apple是Fruit的子类,但是Box[Apple]并不是Box[Fruit]的子类,因此下面的语句是无法通过编译的:
val box: Box[Fruit] = new Box[Apple](new Apple)
如果没有声明variance annotation,那么parameterized type就是invariant,例如Box[Friut]与Box[Apple]之间并不存在任何继承关系。
通过variance annotation可以将parameterized type声明为covariant或者contravariant。
covariant
通过符号 +,可以让Box[Apple]成为Box[Fruit]的子类,如下声明Box即可:
class Box[+F <: Fruit](f: Fruit) {
... ...
}
contravariant
通过符号 -, 可以让Box[Apple]成为Box[Fruit]的父类,如下声明Box即可:
class Box[-F <: Fruit](f: Fruit) {
... ...
}