Java基础复习(六、反射)
六、反射
Java的反射机制是在运行过程中,对于任何一个类,都能够知道这个类的所有属性和方法;对于任何一个对象,都能够调用他的任何一个方法和属性(这种说法不正确,我之前调用private方法时就报错。必须要在前面设置一下权限才能使用)。这种运行时动态获取信息以及动态调用对象的功能称为java语言的反射机制。
反射的主要用途
反射最重要的用途就是开发框架。在我们日常开发工作中,难免会用到很多框架,比如说Spring、Mybatis,这些框架除了用注解之外,还需要我们使用 XML 文件对 Bean 等进行配置。这实际上就是对反射的使用,在 XML 文件中描述好一个 Bean 之后,项目启动时会有拦截器拦截并动态的去创建这些 Bean,这个过程中就需要 XML 中描述好的类信息,然后通过反射区创建甚至是调用一些方法。
其次,我们日常开发中使用IDE,打出类名会自动跳出他的属性和方法,这个过程其实也是对反射的一种使用。
其他时候,我们虽然不怎么用反射,但是反射作为一种思想,用在了很多框架的 IOC 模块,因此,想要深入理解这些框架,反射是必须要了解的。同时,了解反射的知识也可以丰富自己的编程思想,很有必要。
反射的基本使用
反射可以通过获取一个类的 Class 来实现这个类的实例化、属性调用、方法调用,与反射有关的类基本上都在 java.lang.relfect 的包里。这边我们来讲一下反射的基本使用:
1、获得 Class 对象
我们使用反射时,通常都需要先获得一个类的 Class 对象,然后根据 Class 对象来操作,可以进行实例化,可以调用静态方法和属性,也可以实例化后调用普通成员方法和属性。获得 Class 对象的方法有三种:
(1) 使用 Class 类的forName
方法:
使用方法如下代码:
// 被加载的类
public class MyClass {
public static String str1 = "str1";
static {
System.out.println("static code block...");
System.out.println(str1);
}
public MyClass() {
System.out.println("construct method...");
}
}
// 获取 Class 对象
public class Test{
public static void main(String[] args) throws ClassNotFoundException {
Class myClass = Class.forName("MyClass");
}
}
运行后,控制台输出为:
static code block...
str1
由此可以看出,Class.forName() 方法成功的将 .class 文件加载到了虚拟机中(即类加载过程),但是没有创建实例,所以只会进行静态成员的赋值和静态代码块的运行。
我们来看 forName 方法的源码:
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
所以可以看到最后还是使用的 native 方法。需要注意的是这边有 ClassLoader 类的参与,这又涉及到类加载器和双亲委派这些知识点了。
(2)直接获取某个实例的 class:
public class Test{
public static void main(String[] args) throws ClassNotFoundException {
Class myClass = MyClass.class;
}
}
此时,控制台输出为空!!!可以看到,这种方法是不会将类进行加载的。。。这就有点难理解了,我们反编译一下:
...
#16 = Utf8 Exceptions
#17 = Class #18 // java/lang/ClassNotFoundException
#18 = Utf8 java/lang/ClassNotFoundException
#19 = Class #20 // java/lang/InstantiationException
#20 = Utf8 java/lang/InstantiationException
#21 = Class #22 // java/lang/IllegalAccessException
#22 = Utf8 java/lang/IllegalAccessException
#23 = Class #24 // MyClass
#24 = Utf8 MyClass
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 myClass
...
...
public static void main(java.lang.String[]) throws java.lang.ClassNotFoundException, java.lang.InstantiationException, java.lang.IllegalAccessException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Exceptions:
throws java.lang.ClassNotFoundException, java.lang.InstantiationException, java.lang.IllegalAccessException
Code:
stack=1, locals=2, args_size=1
0: ldc #23 // class MyClass
2: astore_1
3: return
LineNumberTable:
line 4: 0
line 5: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 myClass Ljava/lang/Class;
可以看到,其实 .class 是在编译完成的时候就完成了值的替换。我们知道,类加载是在类首次真正使用的时候完成的,在MyClass myClass = null
这种情况是不会进行类加载的。为什么呢?我个人的理解是,类加载实际上是为了在类进行使用前,将类的信息加载 JVM 所进行的操作,这时候需要开辟内存、静态变量的赋值和静态代码块的运行,这样才能确定在常量池中的空间占用。上面这种写法,在编译后发现实际上并不需要用到 MyClass 这个类,因此 JVM 就不会把它加载进来,因此不会执行静态代码块。
这么说来,反倒是 Class.forName() 把类直接加载了倒有点奇怪。不过看到参数里面有 ClassLoader 也应该想到了会直接加载把。
(3)对某个实例调用其 getClass() 方法:
public static void main(String[] args){
MyClass myClass = new MyClass();
Class my_class = myClass.getClass();
}
这段代码都 new 了一个实例了,因此输出肯定是静态代码块的运行结果和构造函数的结果,不提了。
2、判断一个对象是不是某个类的实例
通常我们为了判断一个对象是不是某个类的实例,用到了以下三种方法:
(1) obj.getClass() == Class?
(2) instanceof 关键字
(3) Class.isInstance(obj)
如果仅仅是判断一个实例和他本身所属类的比较,那也没什么意思。这边我们来看和其父类的比较。
为了测试,我们创建了一下几个类:
public class MyClass {
public MyClass(){
}
public MyClass(String name) {
}
}
public class MySubClass extends MyClass{
public MySubClass() {
}
public MySubClass(String name) {
}
}
public class Test{
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MySubClass mySubClass = new MySubClass();
Class myClass = MyClass.class;
System.out.println(mySubClass.getClass() == myClass);
System.out.println(mySubClass instanceof MyClass);
// isInstance 源码是 native 方法
System.out.println(myClass.isInstance(mySubClass));
}
}
输出结果是
false
true
true
可以看出来,后面两种方法是可以判断继承的情况,而第一个不行。
3、实例的创建
通过反射来创建实例有两种方法:
(1)使用 Class 对象的 newInstance() 方法来创建:
Class myClass = MyClass.class;
MyClass my_Class = (MyClass) myClass.newInstance();
(2)使用 Class 对象获取指定的Constructor 对象,再调用 Constructor 对象的 newInstance() 方法来创建实例(这种方法可以自己选择构造函数)。
public class MyClass {
public String name;
public MyClass(){
}
public MyClass(String name) {
this.name = name;
}
}
public class Test{
public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
// 获取类对象
Class myclass = MyClass.class;
// 获取对应构造函数的构造器
Constructor constructor1 = myclass.getConstructor();
Constructor constructor2 = myclass.getConstructor(String.class);
// 使用构造器进行实例化
MyClass obj1 = (MyClass) constructor1.newInstance();
MyClass obj2 = (MyClass) constructor2.newInstance("name");
// 结果查看
System.out.println(obj1.name); // null
System.out.println(obj2.name); // name
}
}
4、获取成员和使用
创建如下类文件:
public class MyClass {
public String name = "lewis";
protected String age = "24";
String height = "175cm";
private String weight = "55kg";
public String getName() {
return this.name;
}
protected String getWeight() {
return this.weight;
}
String getHeight() {
return this.height;
}
private String getAge() {
return this.age;
}
}
获取某个 Class 对象的成员变量,有以下几个方法:
(1)Field[] declaredFields = myClass.getDeclaredFields();
该方法返回类或者接口声明的所有成员变量,包括 public, protected, 默认, private 方法。
(2)Field[] fields = myClass.getFields();
该方法返回某个类的所有 public 成员变量。
(3)Field field = myClass.getField(name);
该方法返回一个特定名称的成员变量。
获取到特定的 Field 之后,如果想要获取某个具体对象 obj 中该属性的值,看如下代码:
public class Test{
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{
Class myClass = MyClass.class;
MyClass obj = new MyClass();
Field[] declaredFields = myClass.getDeclaredFields();
for(Field field:declaredFields) {
System.out.println(field.get(obj));
}
}
}
此时运行会报错,因为在这些 Field 中存在访问权限,比如说 private 的 age。因此,需要在使用 field.get(obj)
之前加上 field.setAccessible(true);
破坏访问权限即可。
相对的获取某个 Class 对象的方法(Method),主要有以下几个方法:
(1)Method[] declaredMethods = myClass.getDeclaredMethods();
该方法返回类或者接口声明的所有方法,包括 public, protected, 默认, private 方法,但不包括继承的方法。
(2)Method[] methods = myClass.getMethods();
该方法返回某个类的所有 public 方法,包括其继承类的公用方法。
(3)Method method = myClass.getMethod(name, parameterTypes);
该方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应 Class 的对象。
获取到特定的 Method 之后,如果想要调用某个具体的方法,如下代码所示:
public class Test{
public static void main(String[] args) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException, InvocationTargetException{
Class myClass = MyClass.class;
MyClass obj = new MyClass();
Method[] declaredMethods = myClass.getDeclaredMethods();
for(Method method:declaredMethods) {
// method.setAccessible(true); // 权限破坏
System.out.println(method.invoke(obj));
}
}
}
权限方面和 Field 相似
反射的一些注意事项
首先,利用反射访问成员属性和方法时,也会因为有访问修饰符而存在权限的限制。利用 setAccessible()
方法会破坏封装性,并且在访问完方法后不会回复。因此要注意使用安全问题。
其次,利用反射会额外消耗系统资源,如果不需要动态创建或者使用成员变量和方法,尽量少用反射。