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() 方法会破坏封装性,并且在访问完方法后不会回复。因此要注意使用安全问题。
其次,利用反射会额外消耗系统资源,如果不需要动态创建或者使用成员变量和方法,尽量少用反射。

posted @ 2020-05-11 09:20  LewisYoung  阅读(214)  评论(0编辑  收藏  举报