我们知道,scala编译器会将scala代码编译成JVM字节码,编译过程中会擦除scala特有的一些类型信息,在scala-2.10以前,只能在scala中利用java的反射机制,但是通过java反射机制得到的是只是擦除后的类型信息,并不包括scala的一些特定类型信息。从scala-2.10起,scala实现了自己的反射机制,我们可以通过scala的反射机制得到scala的类型信息。scala反射包括运行时反射和编译时反射,本文主要阐述运行时反射的一些用法,方便scala开发人员参考,具体原理细节请查看官方文档。本文涉及到的代码示例是基于scala-2.10.4,如有不同请勿对号入座。

给定类型或者对象实例,通过scala运行时反射,可以做到:1)获取运行时类型信息;2)通过类型信息实例化新对象;3)访问或调用对象的方法和属性等。下面分别举例阐述运行时反射的功能。

获取运行时类型信息

scala运行时类型信息是保存在TypeTag对象中,编译器在编译过程中将类型信息保存到TypeTag中,并将其携带到运行期。我们可以通过typeTag方法获取TypeTag类型信息。

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._ scala> typeTag[List[Int]]
res0: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]] scala> res0.tpe
res1: reflect.runtime.universe.Type = scala.List[Int]

如上述scala REPL显示,通过typeTag方法获取List[Int]类型的TypeTag对象,该对象包含了List[Int]的详细类型信息,通过TypeTag对象的tpe方法得到由Type对象封装具体的类型信息,可以看到该Type对象的类型信息精确到了类型参数Int。如果仅仅是获取类型信息,还有一个更简便的方法,那就是通过typeOf方法。

scala> import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe._ scala> typeOf[List[Int]]
res0: reflect.runtime.universe.Type = scala.List[Int]

这时有人就会问,typeTag方法需要传一个具体的类型,事先知道类型还要TypeTag有啥用啊。我们不妨写个方法,获取任意对象的类型信息。

scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@6dae22e7 scala> def getTypeTag[T: ru.TypeTag](obj: T) = ru.typeTag[T]
getTypeTag: [T](obj: T)(implicit evidence$1: ru.TypeTag[T])ru.TypeTag[T] scala> val list = List(1, 2, 3)
list: List[Int] = List(1, 2, 3) scala> val theType = getTypeTag(list).tpe
theType: ru.Type = List[Int]

如上scala REPL显示,方法getTypeTag可以获取任意对象的类型信息,注意方法中的上下文界定T: ru.TypeTag ,它表示存在一个从TTypeTag[T]的隐式转换,前面已经讲到,TypeTag对象是在编译期间由编译器生成的,如果不加这个上下文界定,编译器就不会为T生成TypeTag对象。当然也可以通过隐式参数替代上下文界定,就如同REPL中显示的implicit evidence$1: ru.TypeTag[T]那样。一旦我们获取到了类型信息(Type instance),我们就可以通过该Type对象查询更详尽的类型信息。

scala> val decls = theType.declarations.take(10)
decls: Iterable[ru.Symbol] = List(constructor List, method companion, method isEmpty, method head, method tail, method ::, method :::, method reverse_:::, method mapConserve, method ++)

到这里我们知道TypeTag对象封装了Type对象,通过Type对象可以获取详尽的类型信息,包括方法和属性等,通过typeTag方法可以得到TypeTag对象,通过typeOf方法可以得到Type对象,它包含没有被编译器擦除的含完整的scala类型信息,与之对应地,如果想获取擦除后的类型信息,传统的方法可以通过java的反射机制来实现,但是scala也提供了该功能,通过classTag方法可以获取ClassTag对象,ClassTag封装了擦除后的类型信息,通过classOf方法可以获取Class对象,这与java反射中的Class对象一致。

scala> import scala.reflect._
import scala.reflect._ scala> val clsTag = classTag[List[Int]]
clsTag: scala.reflect.ClassTag[List[Int]] = scala.collection.immutable.List scala> clsTag.runtimeClass
res0: Class[_] = class scala.collection.immutable.List scala> val cls = classOf[List[Int]]
cls: Class[List[Int]] = class scala.collection.immutable.List scala> cls.[tab键补全]
asInstanceOf asSubclass cast
desiredAssertionStatus getAnnotation
getAnnotations getCanonicalName ...

从上述scala REPL中可以看到,ClassTag对象包含了Class对象,通过Class对象仅仅可以获取擦除后的类型信息,通过在scala REPL中用tab补全可以看到通过Class对象可以获取的信息。

运行时类型实例化

我们已经知道通过Type对象可以获取未擦除的详尽的类型信息,下面我们通过Type对象中的信息找到构造方法并实例化类型的一个对象。

scala> case class Person(id: Int, name: String)
defined class Person scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@3e9ed70d scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: ru.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@a57fc5f ... scala> val classPerson = ru.typeOf[Person].typeSymbol.asClass
classPerson: ru.ClassSymbol = class Person scala> val cm = m.reflectClass(classPerson)
cm: ru.ClassMirror = class mirror for Person (bound to null) scala> val ctor = ru.typeOf[Person].declaration(ru.nme.CONSTRUCTOR).asMethod
ctor: ru.MethodSymbol = constructor Person scala> val ctorm = cm.reflectConstructor(ctor)
ctorm: ru.MethodMirror = constructor mirror for Person.<init>(id: scala.Int, name: String): Person (bound to null) scala> val p = ctorm(1, "Mike")
p: Any = Person(1,Mike)

如上scala REPL代码,要想通过Type对象获取相关信息,必须借助MirrorMirror是按层级划分的,有ClassLoaderMirrorClassMirrorInstanceMirrorModuleMirrorMethodMirrorFieldMirror。通过ClassLoaderMirror可以创建ClassMirrorInstanceMirrorModuleMirrorMethodMirrorFieldMirror。通过ClassMirrorInstanceMirror可以创建MethodMirrorFieldMirrorModuleMirror用于处理单例对象,通常是由object定义的。从上述代码中可以发现,首先获取一个ClassLoaderMirror,然后通过该Mirror创建一个ClassMirror,继续创建MethodMirror,通过该MethodMirror调用构造函数。从一个Mirror创建另一个Mirror,需要指定一个SymbolSymbol其实就是绑定名字和一个实体,有ClassSymbol、 MethodSymbol、 FieldSymbol等,Symbol的获取是通过Type对象方法去查询,例如上述代码中通过declaration方法查询构造函数的Symbol

运行时类成员访问

下面举例阐述访问运行时类成员,同理,我们只需逐步创建FieldMirror来访问类成员。

scala> case class Person(id: Int, name: String)
defined class Person scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@3e9ed70d scala> val m = ru.runtimeMirror(getClass.getClassLoader)
m: ru.Mirror = JavaMirror with scala.tools.nsc.interpreter.IMain$TranslatingClassLoader@a57fc5f ... scala> val p = Person(1, "Mike")
p: Person = Person(1,Mike) scala> val nameTermSymb = ru.typeOf[Person].declaration(ru.newTermName("name")).asTerm
nameTermSymb: ru.TermSymbol = value name scala> val im = m.reflect(p)
im: ru.InstanceMirror = instance mirror for Person(1,Mike) scala> val nameFieldMirror = im.reflectField(nameTermSymb)
nameFieldMirror: ru.FieldMirror = field mirror for Person.name (bound to Person(1,Mike)) scala> nameFieldMirror.get
res0: Any = Mike scala> nameFieldMirror.set("Jim") scala> p.name
res2: String = Jim

如上代码所示,通过层级ClassLoaderMirror->InstanceMirror->FieldMirror得到FieldMirror,通过Type对象调用方法declaration(ru.newTermName("name"))获取name字段的Symbol,通过FieldMirrorgetset方法去访问和修改成员变量。

说了这么多,貌似利用java的反射机制也可以实现上述功能,还不用这么的费劲。关键还是在于你要访问编译器擦除后的类型信息还是擦除前的类型信息,如果是访问擦除后的类型信息,使用java和scala的反射都可以,但是访问擦除前的类型信息,那就必须要使用scala的反射,因为java的反射并不知道擦除前的信息。

举个栗子,一步步剖析,首先定义一个基类A,它包含一个抽象类型成员T,然后分别派生出两个子类BC

scala> class A {
| type T
| val x: Option[T] = None
| }
defined class A scala> class B extends A
defined class B scala> class C extends B
defined class C

现在分别实例化BC的一个对象,并将抽象类型T具体化为String类型。

scala> val b = new B { type T = String }
b: B{type T = String} = $anon$1@446344a8 scala> val c = new C { type T = String }
c: C{type T = String} = $anon$1@195bc0a4

现在通过java的反射机制判断对象bc的运行时类型是否是父子关系。

scala> b.getClass.isAssignableFrom(c.getClass)
res3: Boolean = false

可以看到通过java的反射判断对象c的运行时类型并不是对象b的运行时类型的子类。然而从我们的定义来看,对象c的类型本应该是对象b的类型的子类,到这里我们就会想到编译器,在实例化对象bc时实际上是通过匿名类来实例化的,一开始定义类型信息在编译的时候被擦除了,转为匿名类了。下面通过scala的反射机制判断对象bc的运行时类型是否是父子关系。

scala> val ru = scala.reflect.runtime.universe
ru: scala.reflect.api.JavaUniverse = scala.reflect.runtime.JavaUniverse@3e9ed70d scala> def isSubClass[T: ru.TypeTag, S: ru.TypeTag](x: T, y: S): Boolean = {
| val leftTag = ru.typeTag[T]
| val rightTag = ru.typeTag[S]
| leftTag.tpe <:< rightTag.tpe
| }
isSubClass: [T, S](x: T, y: S)(implicit evidence$1: ru.TypeTag[T], implicit evidence$2: ru.TypeTag[S])Boolean scala> isSubClass(c, b)
res5: Boolean = true

从上述代码中可以看到,通过scala的反射得到的类型信息符合我们一开始定义。所以在scala中最好是使用scala的反射而不要使用java的反射,因为很有可能编译后通过java的反射得到的结果并不是想象的那样。

另参考:了解Scala反射(一)

Scala反射(二)的更多相关文章

  1. 了解Scala反射

    本篇文章主要让大家理解什么是Scala的反射, 以及反射的分类, 反射的一些术语概念和一些简单的反射例子. 什么是反射 我们知道, Scala是基于JVM的语言, Scala编译器会将Scala代码编 ...

  2. 初识Scala反射

    我们知道,scala编译器会将scala代码编译成JVM字节码,编译过程中会擦除scala特有的一些类型信息,在scala-2.10以前,只能在scala中利用java的反射机制,但是通过java反射 ...

  3. 一篇入门 -- Scala 反射

    本篇文章主要让大家理解什么是Scala的反射, 以及反射的分类, 反射的一些术语概念和一些简单的反射例子. 什么是反射 我们知道, Scala是基于JVM的语言, Scala编译器会将Scala代码编 ...

  4. joda time, jackson 与 scala 反射

    1. scala 反射,获得所有 field name 可以直接从 case class 获得 field 而不必创建实例 (get fields of a class without an inst ...

  5. Java 8 vs. Scala(二):Stream vs. Collection

    [编者按]在之前文章中,我们介绍了 Java 8和Scala的Lambda表达式对比.在本文,将进行 Hussachai Puripunpinyo Java 和 Scala 对比三部曲的第二部分,主要 ...

  6. Scala学习(二)--- 控制结构和函数

    控制结构和函数 摘要: 本篇主要学习在Scala中使用条件表达式.循环和函数,你会看到Scala和其他编程语言之间一个根本性的差异.在Java或C++中,我们把表达式(比如3+4)和语句(比如if语句 ...

  7. Scala学习二十二——定界延续

    一.本章要点 延续让你可以回到程序执行当中之前的某个点; 可以在shift块中捕获延续 延续函数一直延展到包含它的reset块的尾部 延续所谓的”余下的运算“,从包含shift的表达式开始,到包含它的 ...

  8. Scala学习二十一——隐式转换和隐式参数

    一.本章要点 隐式转换用于类型之间的转换 必须引入隐式转换,并确保它们可以以单个标识符的形式出现在当前作用域 隐式参数列表会要求指定类型的对象.它们可以从当前作用域中以单个标识符定义的隐式对象的获取, ...

  9. Scala学习二十——Actor

    一.本章要点 每个actor都要扩展Actor类并提供act方法 要往actor发送消息,可以用actor!message 消息发送是异步的:”发完就忘“ 要接受消息,actor可以调用receive ...

随机推荐

  1. git add无效,git status(modified content, untracked content)

    问题一:git status 时文件目录后提示(modified content, untracked content) git add后也添加不上,文件不能提交上去   例如下图:   原因: 该文 ...

  2. svg图片拖动与缩放

    引入jquery.js文件,svg-pan-zoom.min.js文件 和 hammer.min.js 文件 这三个文件可以在网上搜一下下载 //svg拖动和缩放 initPanZoom() { th ...

  3. Asp.net MVC企业级开发(01)---Autofac

    1.1 控制反转 在面向对象设计的软件系统中,它的底层都是由N个对象构成的,各个对象之间通过相互合作,最终实现系统的业务逻辑.同时,对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础.但是 ...

  4. mysql锁机制总结,以及优化建议

    一.锁概述和分类 二.表锁 偏向MyISAM存储引擎,开销小,加锁快:无死锁:锁定粒度大,发生锁冲突的概率最高,并发度最低. [手动增加表锁] lock table 表名字1 read(write), ...

  5. 02篇ELK日志系统——升级版集群之kibana和logstash的搭建整合

    [ 前言:01篇LK日志系统已经把es集群搭建好了,接下来02篇搭建kibana和logstash,并整合完成整个ELK日志系统的初步搭建. ] 1.安装kibana 3台服务器: 192.168.2 ...

  6. RMAN笔记

    Rman常用命令 Preview选项 1)    显示用于还原system表空间数据文件的备份文件 RMAN> restore datafile 2 preview; 2)    显示用于还原特 ...

  7. C# 读取Excel文件数据

    1.首先需要在管理NuGet程序包中添加外部包:ExcelDataReader,添加好后,不要忘记在命名空间那里引用. 2.定义文件流,将文件流传入IExcelDataReader类型的对象excel ...

  8. CentOS 8 安装

    截止目前为止CentOS的最新版本为CentOS 8版本,接下来就介绍CentOS Linux 8.0.1905的安装过程 1. 安装CentOS 8 成功引导系统会显示如上图的界面: # 界面说明 ...

  9. 内核中dump_stack的实现原理(3) —— 内核函数printk的实现

      参考内核文档: Documentation/printk-formats.txt   在内核中使用dump_stack的时候可以看到如下用法: static inline void print_i ...

  10. 八、collection系列-----计数器、有序字典、默认字典、可命名元组、双向队列、单向队列一.计数器(对字典的扩展)

    一.计数器(对字典的扩展) 有如下一个字典: dic = {'k1':123,'k2':123,'k3':12} 统计12出现的次数,123出现的次数   1.统计出现次数 >>> ...