@xtccc
2016-08-11T08:45:08.000000Z
字数 8856
阅读 3389
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;}@Overridepublic 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;}@Overridepublic 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].getDeclaredFieldsfor (f <- fields) {f.setAccessible(true) // 防止不能访问private fieldf.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.genericsimport 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.getClassprintln(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.getDeclaredFieldsfor (f <- fields) {f.setAccessible(true) // 防止不能访问private fieldf.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].getCanonicalNameval teacherClzName = classOf[Teacher].getCanonicalNameval 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.genericsimport 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].getCanonicalNameval teacherClzName = classOf[Teacher].getCanonicalNameval 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 ... casevariable pattern 引起的问题,参考这里。
解决方法:
// 下面有2种解决方法def CreateInstanceByClass_1[T: ClassTag](clz:Class[T]): Any = {val studentClzName = classOf[Student].getCanonicalNameval teacherClzName = classOf[Teacher].getCanonicalNameval 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是否相等。
可以把TypeTags 看操作是这样的对象:它包含了关于type的所有信息(包括compile-time和run-time)。TypeTag总是是由编译器生成的(只要用到了要求TypeTage的implicit parameter或者context bound,就会触发对TypeTage的生成)。这意味着:要想获得一个TypeTag,必须使用implicit parameter或者context bound。
下面是一个使用context bound的例子。
scala> import scala.reflect.runtime.universeimport scala.reflect.runtime.universescala> 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).tpetheType: 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 = fdef 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) {... ...}