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

  • 相关阅读:
    算法与数据结构 (四) 排序 一 交换类排序
    算法与数据结构 (三) 二叉树的简单应用 二叉查找树,二叉堆排序
    算法与数据结构 (二) 二叉树的简单实现,非递归的前序遍历 中序遍历 后序遍历
    算法与数据结构 (一) 链表,栈,队列的简单实现
    服务器端的redis和MySQL的远程连接的简单解决方案
    记一次自定义监听器使用spring 管理的bean的问题
    基于java开发的RBAC模型权限管理系统
    2019 本科java开发春招面经(实习)
    记一次Bootstrap框架下 使用Ajax失效的问题
    [转]在static代码块或static变量的初始化过程中使用ServiceManager提供的api的陷阱
  • 原文地址:https://www.cnblogs.com/lewisyoung/p/12867069.html
Copyright © 2011-2022 走看看