Java 类型信息详解和反射机制
本文部分摘自 On Java 8
RTTI
RTTI(RunTime Type Information)运行时类型信息,能够在程序运行时发现和使用类型信息,把我们从只能在编译期知晓类型信息并操作的局限中解脱出来
传统的多态机制正是 RTTI 的基本使用:假设有一个基类 Shape 和它的三个子类 Circle、Square、Triangle,现在要把 Circle、Square、Triangle 对象放入 List<Shape> 中,在运行时,先把放入其中的所有对象都当作 Object 对象来处理,再自动将类型转换为 Shape。所有类型转换的正确性检查都是在运行时进行的,这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型
但这样的类型转换并不彻底,Object 只是被转型为 Shape,而不是更具体的 Circle、Square、Triangle,如果我们希望得到更具体的类型呢?比如说我们现在需要旋转所有图形,但是想跳过圆形(圆形旋转没有意义),这时可以使用 RTTI 查询某个 Shape 引用所指向对象的确切类型,然后选择进行合适的处理
Class 对象
众所周知,每当我们编写并编译了一个新类,就会产生一个 Class 对象,它包含了与类有关的信息。我们可以使用 Class 对象来实现 RTTI,一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象
Class 对象都属于 Class 类型,既然它也是对象,那我们就可以获取和操控它的引用。forName() 是 Class 类的一个静态方法,我们可以使用 forName() 根据目标类的全限定名(包含包名)得到该类的 Class 对象。使用 forName() 会有一个副作用,那就是如果这个类没有被加载就会加载它,而在加载的过程中,Gum 类的 static 初始块会被执行。当 Class.forName() 找不到要加载的类,就会抛出异常 ClassNotFoundException
Class gumClass = Class.forName("Gum");
使用 Class.forName() 你不需要先持有这个类型的对象,但如果你已经拥有了目标类的对象,那就可以通过调用 getClass() 方法来获取 Class 引用,这个方法来自根类 Object,它将返回表示该对象实际类型的 Class 对象的引用
Gum gum = new Gum();
Class gumClass = gum.getClass();
另外,你还可以调用 getSuperclass() 方法来得到父类的 class 对象,再用父类的 Class 对象调用该方法,重复多次,你就可以得到一个完整的类继承结构
Class 对象的 newInstance() 方法可以让你在不知道一个的确切类型的时候创建这个类的对象,使用 newInstance() 来创建的类,必须带有无参数的构造器
Object obj = gumClass.newInstance();
当然,由于得到的是 Object 的引用,目前你只能给它发送 Object 对象能接受的调用。如果你想请求具体对象才有的调用,你就得先获取该对象的更多类型信息,并执行转型
Java 还提供了另一种生成类对象的引用:类字面常量,这样做不仅更简单,而且更安全,因为它在编译时就会收到检查(不用放在 try 语句块中),而且根除了对 forName() 方法的调用,效率更高
Class gumClass = Gum.class;
类字面常量不仅可以用于普通类,也可以用于接口、数组以及基本数据类型。对于基本数据类型的包装类,还有一个标准字段 Type,Type 字段是一个引用,指向对应基本数据类型的 Class 对象,例如 int.class 就等价于 Integer.TYPE。还有一点值得注意的是:使用 .class 语法来获得对类对象的引用不会触发初始化
到这里我们都知道了,Class 引用总是指向某个 Class 对象,而 Class 对象可以用于产生类的实例。不过自从 Java 引入泛型以后,我们就可以使用泛型对 Class 引用所指向的 Class 对象的类型进行限定,让它的类型变得更具体些
Class intClass = int.class;
Class<Integer> genericIntClass = int.class;
intClass = genericIntClass; // 同一个东西
// genericIntClass = double.class 非法
好了,既然拿到了 Class 对象,那我们就可以这个类的类型信息,常用的方法如下:
方法 | 用途 |
---|---|
asSubclass(Class clazz) | 把传递的类的对象转换成代表其子类的对象 |
Cast | 把对象转换成代表类或是接口的对象 |
getClassLoader() | 获得类的加载器 |
getClasses() | 返回一个数组,数组中包含该类中所有公共类和接口类的对象 |
getDeclaredClasses() | 返回一个数组,数组中包含该类中所有类和接口类的对象 |
forName(String className) | 根据类名返回类的对象 |
getName() | 获得类的完整路径名字 |
newInstance() | 创建类的实例 |
getPackage() | 获得类的包 |
getSimpleName() | 获得类的名字 |
getSuperclass() | 获得当前类继承的父类的名字 |
getInterfaces() | 获得当前类实现的类或是接口 |
类型转换检测
到目前为止,我们已知的 RTTI 类型包括:
- 传统的类型转换,如多态
- 代表对象类型的 Class 对象
RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof,它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它
if(x instanceof Dog) {
((Dog)x).bark();
}
Java 还提供了 Class.isInstance() 方法动态检测对象类型,例如
0 instance of String // 编译报错
String.class.isInstance(0) // 可以通过编译
反射
如果你不知道对象的确切类型,RTTI 会告诉你,但是有一个限制:必须在编译时知道类型,才能使用 RTTI 检测它。换句话说,编译器必须知道你使用的所有类
看上去这并不是什么特别大的限制,但假设你引用了一个不在程序空间中的对象,比如你从磁盘文件或网络连接中获得大量的字节,并被告知这些字节代表一个类,那该怎么办呢?
类 Class 支持反射的概念,java.lang.reflect 库中支持类 Field、Method、Constructor(每一个都实现了 Member 接口),这些类型的对象由 JVM 运行时创建,以表示未知类中的对应成员。通常我们不会直接使用反射,但反射可以用来支持其他 Java 特性,例如对象序列化等
Field 代表类的成员变量(成员变量也称为类的属性),Class 类中定义了如下方法用来获取 Field 对象
方法 | 用途 |
---|---|
getField(String name) | 获得某个公有的属性对象 |
getFields() | 获得所有公有的属性对象 |
getDeclaredField(String name) | 获得某个属性对象 |
getDeclaredFields() | 获得所有属性对象 |
Field 类定义了如下方法设置成员变量的信息
方法 | 用途 |
---|---|
equals(Object obj) | 属性与 obj 相等则返回 true |
get(Object obj) | 获得 obj 中对应的属性值 |
set(Object obj, Object value) | 设置 obj 中对应属性值 |
Method 代表类的方法,Class 类中定义了如下方法用来获取 Method 对象
方法 | 用途 |
---|---|
getMethod(String name, Class...<?> parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class...<?> parameterTypes) | 获得该类某个方法 |
getDeclaredMethods() | 获得该类所有方法 |
Method 类定义了如下方法对方法进行调用
方法 | 用途 |
---|---|
invoke(Object obj, Object... args) | 传递 object 对象及参数调用该对象对应的方法 |
Constructor 代表类的构造器,Class 类中定义了如下方法用来获取 Constructor 对象
方法 | 用途 |
---|---|
getConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获得该类的所有公有构造方法 |
getDeclaredConstructor(Class...<?> parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获得该类所有构造方法 |
Constructor 代表类的构造方法
方法 | 用途 |
---|---|
newInstance(Object... initargs) | 根据传递的参数创建类的对象 |
除了成员变量、方法和构造器以外,反射还能获取其他更多的信息,例如注解等,具体可查阅 Java API
反射的强大威力大家已经看到了,通过反射我们甚至可以获取到一些“本不应该获取”的信息,例如程序员为了降低耦合,往往会使用接口来隔离组件,但反射却可以轻易破解
public interface A {
void f();
}
class B implements A {
public void f() {}
public void g() {}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // 编译错误
if (a instanceof B) {
B b = (B) a;
b.g();
}
}
}
通过使用 RTTI,我们发现 a 是用 B 实现的,只要将其转型为 B,我们就可以调用不在 A 中的方法。如果你不希望客户端开发者这样做,那该如何解决呢?一种解决方案是直接声明为实际类型,另一种则是让实现类只具有包访问权限,这样包外部的客户端就看不到实现类了
除了这个以外,通过反射可以获得所有成员信息,包括 private 的,通常这种违反访问权限的操作并不是十恶不赦的,也许还可以帮助你解决某些特定类型的问题
动态代理
代理是基本的设计模式之一,一个对象封装真实对象,代替真实对象提供其他不同的操作,这些操作通常涉及到与真实对象的通信,因此代理通常充当中间对象。下面是一个简单的静态代理的示例:
interface Interface {
void doSomething();
}
class RealObject implements Interface {
@Override
public void doSomething() {
System.out.println("doSomething");
}
}
class SimpleProxy implements Interface {
private Interface proxied;
SimpleProxy(Interface proxied) {
this.proxied = proxied;
}
@Override
public void doSomething() {
System.out.println("SimpleProxy doSomething");
proxied.doSomething();
}
}
class SimpleProxyDemo {
public static void consumer(Interface iface) {
iface.doSomething();
}
public static void main(String[] args) {
consumer(new RealObject());
consumer(new SimpleProxy(new RealObject()));
}
}
当你希望将额外的操作与真实对象做分离时,代理可能会有所帮助,而 Java 的动态代理更进一步,不仅动态创建代理对象,而且可以动态地处理对代理方法的调用。在动态代理上进行的所有调用都会重定向到一个调用处理程序,该程序负责发现调用的内容并决定如何处理,下面是一个简单示例:
class DynamicProxyHandler implements InvocationHandler {
private Object proxied;
DynamicProxyHandler(Object proxied) {
this.proxied = proxied;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(proxied, args);
}
}
class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething();
}
public static void main(String[] args) {
RealObject real = new RealObject();
Interface proxy = (Interface) Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{Interface.class},
new DynamicProxyHandler(real));
consumer(proxy);
}
}
通过调用静态方法 Proxy.newProxyInstance() 来创建动态代理,该方法需要三个参数:类加载器、希望代理实现的接口列表、以及接口 InvocationHandler 的一个实现。InvocationHandler 正是我们所说的调用处理程序,动态代理的所有调用会被重定向到调用处理程序,因此通常为调用处理程序的构造函数提供一个真实对象的引用,以便执行中间操作后可以转发请求
Java 类型信息详解和反射机制的更多相关文章
- Java基础 -- 深入理解Java类型信息(Class对象)与反射机制
一 RTTI概念 认识Claa对象之前,先来了解一个概念,RTTI(Run-Time Type Identification)运行时类型识别,对于这个词一直是 C++ 中的概念,至于Java中出现RT ...
- Java类型信息(RTTI和反射)
要想在IT领域站得住脚,必须得不断地学习来强化自己,但是学过的技术不实践很容易便被遗忘,所以一直都打算开个博客,来记录自己学的知识,另外也可以分享给有需要的人! 最近在学习反射,为了更好地理解反射,就 ...
- JAVA类型信息——反射机制
JAVA类型信息——反射机制 一.反射机制概述 1.反射机制:就是java语言在运行时拥有的一项自我观察的能力,java通过这种能力彻底了解程序自身的情况,并为下一步的动作做准备. 2.反射机制的功能 ...
- 最强Java并发编程详解:知识点梳理,BAT面试题等
本文原创更多内容可以参考: Java 全栈知识体系.如需转载请说明原处. 知识体系系统性梳理 Java 并发之基础 A. Java进阶 - Java 并发之基础:首先全局的了解并发的知识体系,同时了解 ...
- Java 序列化Serializable详解
Java 序列化Serializable详解(附详细例子) Java 序列化Serializable详解(附详细例子) 1.什么是序列化和反序列化Serialization(序列化)是一种将对象以一连 ...
- JAVA类型信息——Class对象
JAVA类型信息——Class对象 一.RTTI概要 1.类型信息RTTI :即对象和类的信息,例如类的名字.继承的基类.实现的接口等. 2.类型信息的作用:程序员可以在程序运行时发现和使用类型信息. ...
- JAVA命令参数详解
JAVA命令参数详解 JAVA命令详解 结构 说明 Java 和 OldJava JIT 选项 另请参阅 结构 java [ options ] class [ argument ... ] java ...
- Java 序列化Serializable详解(附详细例子)
Java 序列化Serializable详解(附详细例子) 1.什么是序列化和反序列化 Serialization(序列化)是一种将对象以一连串的字节描述的过程:反序列化deserialization ...
- Java类加载器详解
title: Java类加载器详解date: 2015-10-20 18:16:52tags: JVM--- ## JVM三种类型的类加载器- 我们首先看一下JVM预定义的三种类型类加载器,当一个 J ...
随机推荐
- Linux最常用的命令大全
Linux最常用的命令大全 按功能索引 目录处理命令 ls mkdir pwd cd rmdir cp mv rm 文件处理命令 touch cat tac more less head tail l ...
- 爬虫之Selenium
简介 selenium最初是一个自动化测试工具,而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题 selenium本质是通过驱动浏览器,完全模拟浏览器的操作,比如 ...
- lambda函数小结
C++中的lambda函数 lambda函数是函数式编程中的概念,由C++11引入,成为现代C++中重要的特性. 所谓lambda函数就是匿名函数,语法结构: [capture list] (para ...
- Linux命令行history
概述 当执行命令后,系统默认会在内存记录执行过的命令 当用户正常退出时,会将内存的命令历史存放对应历史文件中,默认是~/.bash_history 登录shell时,会读取命令历史文件中记录下的命令加 ...
- linux硬盘分区及挂载
今天买的一台服务器发现其硬盘容量与购买界面的描述不符,于是我去问了客服才知道有一块硬盘需要自己挂载,所以记录自己硬盘分区以及挂载操作得此文. 测试环境 由于时间限制,本人仅在centos 8下测试 ...
- ELK6环境搭建
(一)什么是ELK Stack ELK 到底是什么呢? "ELK"是三个开源项目的首字母缩写,这三个项目分别是:Elasticsearch.Logstash 和 Kibana. E ...
- poj1654 -- Area (任意多边形面积)
Area Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 20444 Accepted: 5567 Description ...
- 联赛模拟测试22 B. 分组配对 倍增+二分
题目描述 分析 首先,容易发现一个小组内的最优配对方式(能得到最大综合实力的方式) 一定是实力值最大的男生和最大的女生配对,次大的和次大的配对,以此类推. 但是每次新插入一个值时,需要用 \(nlog ...
- IOS8 对flex兼容性问题
问题: IOS8.2 dispaly:flex:不生效: 注意一下兼容写法的顺序问题, display: -webkit-box; /* Chrome 4+, Safari 3.1, iOS Saf ...
- IC晶圆缺货涨价浪潮持续上涨 无线路由芯片WiFi模块受波及严重
正是多事之秋,继受美国贸易战影响后.由于晶圆供不应求,市场各大行业IC纷纷出现了断货,缺货,涨价的现象.这给了本来低迷的经济市场又一重创.WiFi路由芯片的无线路由模块必不可免的受到了波及. 晶圆代工 ...