对任意的一个Object
实例,只要我们获取了它的Class
,就可以获取它的一切信息。
除了int
等基本类型外,其他类型基本都是class
,包括interface
1 | public final class Class { |
这个Class
实例是JVM内部创建的,如果我们查看JDK源码,可以发现Class
类的构造方法是private
,只有JVM能创建Class
实例,我们自己的Java程序是无法创建Class
实例的。
Class类
当JVM
加载String
类时,读取String.class
到内存,然后为String
创建一个Class
类型的实例与之关联。
1 | Class clz = new Class(String); |
一个Class
实例包含了该class
的所有完整信息:
Class Instance | String |
---|---|
name | “java.lang.String” |
package | “java.lang” |
super | “java.lang.Object” |
interface | CharSequence… |
field | value[],hash… |
method | indexof()… |
由于JVM为每个加载的class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。
这种通过Class
实例获取class
信息的方法称为反射(Reflection)。
获取Class实例的三种方式
通过class的静态变量获取
1 | Class clz = String.class; |
通过实例的getClass()方法获取
1 | String str = "bbdog"; |
通过Class.forName()方法获取
1 | Class clz = Class.forName("java.lang.String"); |
必须是类的完成类名。
因为每种class只会在JVM中存在唯一Class实例,所以以上三种方式获取的都是同一个Class实例。
1 | Class clz = String.class; |
instanceof
和==
的区别还在于,instanceof
体现了多态的思想,不仅可以匹配他的类型,还可以匹配它的子类型。
1 | String str = "bbdog"; |
通过Class创建对应实例
如果获取到了一个Class
实例,我们就可以通过该Class
实例来创建对应类型的实例:
1 | // 获取String的Class实例: |
上述代码相当于new String()
。通过Class.newInstance()
可以创建类实例,它的局限是:只能调用public
的无参数构造方法。带参数的构造方法,或者非public
的构造方法都无法通过Class.newInstance()
被调用。
动态加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。例如:
1 | // Main.java |
当执行Main.java
时,由于用到了Main
,因此,JVM首先会把Main.class
加载到内存。然而,并不会加载Person.class
,除非程序执行到create()
方法,JVM发现需要加载Person
类时,才会首次加载Person.class
。如果没有执行create()
方法,那么Person.class
根本就不会被加载。
这就是JVM动态加载class
的特性。
访问字段
访问字段
对任意的一个Object
实例,只要我们获取了它的Class
,就可以获取它的一切信息。
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getFields():获取所有public的field(包括父类)
- Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
1 | public class Hello{ |
获取字段的值
1 | Hello hello = new Hello("456"); |
如果字段权限是private
可以通过Field.setAccessible(true)
实现对私有字段的取值。这么做感觉有点破坏了封装的思想。
反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)
可能会失败。如果JVM运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java
和javax
开头的package
的类调用setAccessible(true)
,这样可以保证JVM核心库的安全。
设置字段
不仅仅可以获取到字段,还可以设置字段值。
1 | Hello hello = new Hello("456"); |
方法
获取方法
Class
类提供了以下几个方法来获取Method
:
Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
1 | public class Hello{ |
调用方法
调用public方法
1 | Hello hello = new Hello(); |
调用静态方法
1 | Hello hello = new Hello(); |
区别于调用public
方法,invoke
时不需要传对象的实例。所以实例对象的位置传null
,pringHello()
方法没有传入参数,不需要填写其他参数。
调用非public方法
1 | Hello hello = new Hello("bbdog"); |
与通过反射获取字段相同,使用Method.setAccessible(true)
可以调用非Public方法。同时如果JVM运行期间存在SecurityManager
,也还是会出错的。
多态
setAccessible(true)
的存在无视了封装的思想。那多态呢。
父类有getStr()
方法,子类重写了getStr()
方法,那么使用反射的方式调用getStr()
实际上调用的是哪个呢?
1 | public class Main { |
可见,还是保留了封装思想的。
构造方法
1 | Hello hello = Hello.class.newInstance(); |
这是反射获取对象实例的方法,但这种方法具有局限性,就是只能调用对象的非public
、无参构造函数
。
但是反射还给我们提供了一个Constructor对象,可以通过Constructor获得对象实例;
写法和获取普通方法类似,但是因为构造方法名只能和类名相同,所以它不需要传入方法名。
1 | public class Hello{ |
调用非public方法时,与method调用时相同,使用Constructor.setAccessible(true)
即可。
继承
获取父类
1 | Class hello = Hello.class; |
获取接口
1 | Class s = Integer.class; |
判断继承关系
判断继承关系我们常用instanceof
,
1 | Object n = Integer.valueOf(123); |
如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom():1
2
3
4
5
6
7
8// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理
动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface
的实例。
我们先定义了一个接口,但是我们并不去编写实现类,而是直接通过JDK提供的一个Proxy.newProxyInstance()
创建了一个接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理。
1 | public class Main { |
定义一个
InvocationHandler
实例,它负责实现接口的方法调用;通过
Proxy.newProxyInstance()
创建interface
实例,它需要3个参数:- 使用的
ClassLoader
,通常就是接口类的ClassLoader
; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler
实例。
- 使用的
将返回的
Object
强制转型为接口。
动态代理实际上是JDK在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
1 | public class HelloDynamicProxy implements Hello { |
其实就是JDK帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。