java高级编程——Class类与反射机制

文章目录

  • Class类
    • 类的加载过程
    • 类的加载时机
    • 类加载器
  • java的反射机制
    • 获取Class对象的三种方法
    • 通过反射实例化对象
    • 通过反射调用构造方法
    • 通过反射调用成员方法
    • 通过反射访问成员变量
  • 反射的简单应用
    • 反射运行配置文件内容
    • 利用反射越过泛型检查

很早之前就已经学过反射了,但是一直没有机会总结,最近在学习Spring框架的底层原理,频繁的遇到反射机制,借此机会稍微总结一下。几乎所有的开发框架以及应用技术都是基于反射技术的应用,所有这块知识非常重要,不掌握就没有办法了解框架的底层。

Class类

反射机制和java.lang.Class< T >这个类息息相关,因为整个反射机制就是基于对Class对象的操作。了解反射机制前,我们需要先了解Class这个类。

Class这个类名,很特殊的一个名字吧,那么它的意义一定非同一般。一句话,Class这个类就表示正在运行的Java应用程序中的类和接口。如果把一个类比作一个人的话,那么Class对象就相当于这个人的资料卡片,关于这个人的一切都在这个小小的资料卡片中存储着。

介绍Class类,我们需要稍微了解一下java中关于类的加载机制。

类的加载过程

当我们在程序中使用一个类时,java虚拟机会先判断这个类是否已经加载进内存,如果没有,就会按照下面的步骤进行类的初始化:

  1. 加载
  2. 链接
  3. 初始化

所谓加载,就是把*.class文件加载进内存,并为之创建一个Class对象,这个Class对象保存了关于这个类的所有信息,所有的类初次使用都会创建一个Class对象。Class对象在内存中只有一个,如果一个类多次使用,不会再重复创建Class对象,虚拟机会自己判断。

所谓链接,分为大概三个过程,

  1. 验证 : 是否有正确的内部结构,并和其他类协调一致
  2. 准备 : 负责为类的静态成员分配内存,并设置默认初始化值
  3. 解析: 把类中的符号引用转换为直接引用

所谓初始化,就是访问该类的父类中的成员和方法,完成对子类的初始化。

类的加载时机

那么,一个类什么时候才会被加载呢?也就是类的加载时机,大致有这么几个场景:

  1. 创建类的实例;
  2. 访问类的静态变量,后者为静态变量赋值;
  3. 调用类的静态方法;
  4. 使用反射方式来强制创建某个类或者接口对应的java.lang.Class对象;
  5. 初始化某个类的子类;
  6. 直接使用java.exe命令来运行某一个主类;

还需要提到的一个概念就是类的加载器,它负责将.Class文件加载到内存中,并为之生成相应的Class对象。

类加载器

Class 这个类并没有提供公共的构造方法,它是由虚拟机通过调用类加载器中defineClass()方法来创建的。

类加载器分为以下几种类型:

  1. Bootstrap ClassLoader - 根类加载器
  2. Extension ClassLoader - 扩展类加载器
  3. Sysetm ClassLoader - 系统类加载器

作用:

  1. 根类加载器:负责java核心类的加载 System String ……
  2. 扩展类加载器:负责JRE的扩展目录中的jar包的加载。
  3. 系统类加载器:负责在JVM启动时加载来自java命令的class文件

java的反射机制

一个很关键的概念,叫做RTTI(Run-Time Type Identification)运行时类型识别。
JVM虚拟机要运行我们的程序,就必须知道我们的每一个对象所属的类和这个类的所有信息,这就是运行时类型识别。

RTTI分为两种:

  1. 编译器已经确定类型
  2. 反射机制

我们回想一下,一般来说,我们创建一个对象,都是基于 类名 对象名 = new 类名 (形参列表) 这种形式利用公共的构造函数来创建一个对象。这就是所谓的编译器已经确定类型。而我们的反射,是在运行时才确定类型的,也就是说,java反射机制使得在运行状态中,对于任意一个类,虚拟机都能够知道这个类的所有属性和方法,对于任意一个对象,虚拟机都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为java的反射机制

要想使用反射,我们就必须获取到这个类的Class对象,因为Class对象保存了所有关于这个类的信息。

获取Class对象的三种方法

  1. 方法一:调用Object类的getClass()方法
// 调用Object类中的getClass方法来获取Class对象
Date date = new Date();
// 使用这种方法获取Class对象必须要有一个实例对象
Class aClass = date.getClass();
// Class中的getName方法用来返回类名
System.out.println(aClass.getName());
// java.util.Date

这个程序通过Date类的实例化对象,调用了从Object类继承来的getClass方法,这样就取得了java.util.Date类型的反射操作类对象,通过getName()方法可以直接输出其完整名称。

  1. 方法二:使用类的静态属性class获取
 // 通过类的静态属性class来获取Class对象
 Class<Date> aClass1 = Date.class;

这是一种比较特殊的格式,而且不只是java的核心类,所有的类(包括自己写的)都有这个静态属性,所以不必担心自己创建的类没法通过这种方法来获取Class对象。

  1. 方法三:调用Class类提供的forName()方法
    static Class forName(String className) throws ClassNotFoundException
    返回带有给定字符串名的类或接口相关联的Class对象,className是类的全限定名。
Class<?> aClass2 = Class.forName("java.util.Date");

本程序使用Class类的静态方法forName来获取Class对象,如果通过className找到了这个类,就不会抛出‘ClassNotFoundException’的异常。

以上提到的三种常见的Class类的实例化方法几乎在所有的开发框架中都会用到,也很难分辨出那种方法使用的更多,三种方法都有各自适用的场景。

  • 利用“getClass()”方法操作往往出现在简单的javaBean与提及参数的自动赋值操作中,比如Struts、SpringMVC都会体用表单参数和javaBean属性的绑定。
  • 利用“类.class”静态属性的方法往往是将反射操作的类型设置交给用户使用,想Hibernate中进行数据及保存以及根据ID查询中会使用到此类操作。
  • 利用“Class.forName()”方法可以实现配置文件以及Annotation配置的反射操作,几乎所有的开发框架都是依靠次方式实现的。

通过反射实例化对象

我们已经学会了怎用获取一个类的Class对象,有了这个Class对象,我们就可以利用它来进行类的反射控制了。
Class类有几个常用的方法需要了解一下:

方法 解释
static Class forName(String className) throws ClassNotFoundException 获取某个类的Class对象
public Class[] getInterfaces() 获取类实现的所有接口
public String getName() 获取反射操作类的全名
public String getSimplename() 获取反射操作类的类名
public Package getPackage() 获取反射操作类所在的包
public Class() 获取反射操作类的父类
public boolean isEnum() 判断反射操作类是否是枚举类
public boolean isInterface() 判断反射操作类是否是接口
public boolean isArray() 判断反射操作类是否是数组
public T newInstance() throws InstantiationException,IllegalAccessException 获取反射操作类的实例化对象

通过这些方法我们可以发现,在反射操作中类或者接口都是利用Class来包装的,利用Class类可以表示任意类、枚举、接口、数组等引用类型的操作。

这一小节的关键方法在newInstance方法,我们就是用它来创建我们的反射操作类的实例化对象的。

值得注意的是,使用newInstance()方法来实例化对象,那么这个类型中一定要提供无参构造方法


public static void main(String[] args) throws ClassNotFoundException, 
	IllegalAccessException, InstantiationException {
	    // 获取Class对象
	    Class<?> aClass2 = Class.forName("java.util.Date");
	    // 获取Date类的实例化对象
	    Date newInstance = (Date) aClass2.newInstance();
	
	    System.out.println(newInstance);
	    //    Mon May 27 21:26:55 CST 2019
}

可以发现,这个程序中并没有使用new关键字来创建Date类的对象,而是通过由Date类创建的Class对象的newInstance()方法来创建Date对象的,newInstance方法默认访问的是Date类中的无参构造方法,返回的类型是Object类型,根据需要向下转型。
可以利用一下方法来判断是否可以转型为某个类型:

//判断这个对象能不能被转化为这个类
class.inInstance(obj)

比如:

 Class<?> aClass2 = Class.forName("java.util.Date");
 // Date 能不能转换成 String ?
 boolean b = aClass2.isInstance(new String());
 System.out.println(b); //false

好吧,现在我们已经掌握了通过反射机制创建一个类对象,可是,问题来了?你可能想说,这么做有什么用啊?我简简单单的用new实例化一个类不好吗?你整这么多行代码不都是创建对象吗?

首先,必须承认,通过new进行对象的实例化是最正统的做法,也是使用最多的方法。但是通过new实例化对象时需要明确的指定类的构造方法,所以new也是造成代码耦合的最大元凶!要想解决代码的耦合问题,就要先解决关键字 new 来创建对象的操作。比如,来看一个简单的工厂模式:

package com.xx.test1;

interface Animal{
    public void eat();
}

class Dog implements Animal{
    public Dog() {
        System.out.println("Dog构造方法");
    }

    @Override
    public void eat() {
        System.out.println("我爱吃骨头");
    }
}

class Cat implements Animal{

    public Cat() {
        System.out.println("Cat构造方法");
    }

    @Override
    public void eat() {
        System.out.println("我爱吃鱼");
    }
}

class Factory{
    public static Animal getInstance(String animalName){
        if(animalName.equals("Cat")) return new Cat();
        else if(animalName.equals("Dog")) return  new Dog();
        else return null;
    }
}

class Factory{
    public static Animal getInstance(String animalName){
        if(animalName.equals("Cat")) return new Cat();
        else if(animalName.equals("Dog")) return  new Dog();
        else return null;
    }
}

public class newIssue {

    public static void main(String[] args) {
        Animal dog = Factory.getInstance("Dog");
        Animal cat = Factory.getInstance("Cat");
        dog.eat();
        cat.eat();
    }

}
// 运行结果:
Dog构造方法
Cat构造方法
我爱吃骨头
我爱吃鱼

这个程序的最大问题在于,我每新增加一个动物,就需要在Factory中改动代码,因为new这个关键字需要访问到子类的构造方法,我们需要在Factory中去调用。这就是耦合,很麻烦不是吗?我们可以利用反射机制对工厂类进行改造:

class Factory {
    public static Animal getInstance(String animalName) {
        //if(animalName.equals("Cat")) return new Cat();
        //else if(animalName.equals("Dog")) return  new Dog();
        //else return null;
        Animal animal = null;
        try {
        	// 使用反射来创建对象返回
            Class aClass = Class.forName(animalName);
            animal = (Animal) aClass.newInstance();
        } catch (Exception e) {
            System.out.println("该类型不存在");
        } finally {
            return animal;
        }
    }
}

你看,这么一来,不管有多少动物,我们都无需对工厂类再做改动。

在实际的开发中,如果将以上的工厂模式再结合上一些配置文件,常用的是XML文件,就可以利用配置文件来动态定义项目中的所需的操作类,此时程序将会变得十分灵活。

值得一提的是,使用new关键字仍是创建对象最正统的方法,在应用开发中(非架构层次的开发)大部分都使用它而不是反射。

通过反射调用构造方法

上面提到了,使用newInstance方法来实例化对象有一个要求就是类中必须提供无参构造方法,但是,实际中由于开发者个人原因,可能只会提供有参构造方法,此时就会报错。解决这个问题的方法就是使用反射调用构造方法。Class类为我们提供了这样的方法:

方法 解释
public Constructor[] getConstructors() throws SecurityException 获取反射操作类中的所有构造方法
public Constructor getConstructor(Class... parameterTypes) throws NoSuchMethodException,SecurityException 获取指定参数类型的构造方法

这两个方法会返回类中的构造方法,是Constructor类型,Constructor有些常用方法需要了解一下:

方法 解释
public Class[] getExceptionTypes() 获取构造方法上抛出的所有异常的类型
public int getModifiers() 获取构造方法上的修饰符
public String getName() 获取构造方法的名字
public String getParameterCount 获取构造方法的形参个数
public Class[] getParameterTypes() 获取构造方法中的参数类型
public T newInstance(Object... initargs) 调用指定参数的构造实例化类对象

一个有趣的方法是public int getModifiers(),获取构造方法上的修饰符,发现了吗,它返回的是int类型。实际上,所有的修饰符都是一个数字,修饰符的组成就是数字的加法操作,比如public用1表示,static用8表示,那么public static 就是1+8=9。在java.lang.reflect.Modifer类中明确定义了各个修饰符对应的常量操作,同时也提供了将数字转成修饰符的方法public static String toString(int mod)

可以看到,Constructor类中也提供了newInstance方法来实例化对象,我们取得了有参构造的Constructor对象,就用它来执行以实例化对象:

package com.xx.test1;

import java.lang.reflect.*;

class Student{
    String name;
    Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
        System.out.println("有参构造方法执行了");
    }

    public Student() {
        System.out.println("无参构造方法执行了");
    }
}

public class GetCons {

    public static void main(String[] args) throws Exception {
        Class aClass = Class.forName("com.xx.test1.Student");
        // 调用有参构造
        Constructor<Student> constructor = aClass.getConstructor(String.class, Integer.class);
        // 使用有参构造的newInstance方法来实例化对象
        Student student = constructor.newInstance("张三", 19);
        
    }
}

这样,就可以明确指定用有参构造方法来实例化对象了。

另外,如果构造方法是私有的,就需要用另外一种方法来获取public Constructor[] getDeclaredConstructors()获取所有的构造方法,包括私有的。或者public Constructor getDeclaredConstructor(Class... parameterTypes)获取单个的构造方法包含私有的。

通过反射调用成员方法

方法是类的主要操作手段,以往我们调用一个方法是通过对象.方法名的方式,通过反射也可以实现类方法的操作。一样,它也是通过操作Class对象来完成的。
具体方法:

方法 解释
public Method[] getMethods() 获取所有的成员方法,不包括私有
public Method[] getDeclaredMethods() 获取所有的成员方法,包括私有
public Method getMethod(String name,Class... parameterTypes) 通过指定参数类型的Class对象获取单个成员方法,不包括私有
public Method getDeclaredMethod(String name,Class... parameterTypes) 通过指定参数类型的Class对象获取单个成员方法,包括私有

其实有一个规律,如果你想用私有方法,就必须要使用getDeclaredXxxxx()这个方法,前面获取私有的构造方法也是一样的。

上述的方法会返回java.lang.reflect.Method类型的对象或者数组对象,有必要了解Method类型的常用方法:

方法 解释
public Class[] getExceptionTypes() 获取方法抛出的异常类型
public int getModifiers() 获取方法的修饰符
public Class getReturnType() 获取方法的返回值类型
public int getParameterCount() 获取方法中定义的参数数量
public Class[] getParameterTypes() 获取方法中定义的所有参数类型
public Object invoke(Object obj,Object... args) 反射调用方法并且传递参数

这些方法中最重要的就是这个invoke方法了,它是实现反射调用方法的核心操作。

必须之处,任何情况下调用方法都必须产生类的实例化对象,反射也不例外,invoke方法接收的第一个参数就是一个实例化对象,不过,它不需要指定具体的对象类型,我们通过Class对象的newInstance方法获取到的实例化对象是一个obj类型,不需要向下转型,可以直接当做参数传递。

还有一个需要注意的是,获取到私有方法的method对象之后,要想通过invoke方法来调用,必须设置或者取消访问检查(使用继承父类(AccessibleObject类)来的setAccessible()方法),以达到执行私有方法的目的。

举个例子:

package com.xx.test1;

import java.lang.reflect.*;

class Student {
   
    public Student() {
        System.out.println("无参构造方法执行了");
    }

    public void doA(String s) {
        System.out.println("公有方法执行了" + s);
    }

    private void doB(Integer i) {
        System.out.println("私有方法执行了" + i);
    }

}

public class GetCons {

    public static void main(String[] args) throws Exception {
        Class aClass = Class.forName("com.xx.test1.Student");

        // 获取公有方法,第一个参数name是方法名
        // 第二个parameterTypes参数是以声明顺序标识方法的形式参数类型的Class对象的数组,可以是null
        Method doA = aClass.getMethod("doA", String.class);
        // 通过Student类的Class对象的newInstance方法获取实例化对象,不需要转型
        Object obj = aClass.newInstance();
        // 执行方法,第一个参数是对象,第二个参数是参数
        doA.invoke(obj, "hahaha");

        // 反射调用私有方法
        Method doB = aClass.getDeclaredMethod("doB", Integer.class);
        // 取消访问检查
        doB.setAccessible(true);
        // 执行方法
        doB.invoke(obj, 10000);
    }
}
// 运行结果:
// 无参构造方法执行了
// 公有方法执行了hahaha
// 私有方法执行了10000

在这个案例中,我们的Student类中有一个无参构造,有一个公有方法doA,和一个私有方法doB,我们利用反射都可以获取到并且调用,前提是获得这个类的实例化对象。

既然提到了反射访问私有方法,有人就不明白了,我们都清楚java的特性之一就是封装,现在使用反射机制,那么封装性会不会被破坏了?

答案当然是否定的,我们都知道,封装的意义在于对外部隐藏具体的细节,只暴露实现功能的具体方法,这样有利于数据的安全性。也就是说,仅仅通过公有的方法我们就可以实现这个类的所有功能,通过反射来获取私有类是没有任何意义的,不仅实现不了功能,可能还会报错。反射就相当于你看病时医生使用的X光片,医生
(开发者)可以通过它来查看问题,普通的病人看不懂也用不着来看。所以说,封装性并没有被破坏,反而提高了java语言的灵活性。

通过反射访问成员变量

成员的获取依然要通过Class类的方法。成员有变量有常量之分,并且部分成员还有可能是从父类继承过来的。

方法 解释
public Field[] getFields() 获取所有的成员变量包含从父类继承过来的
public Field[] getDeclaredFields() 获取所有的成员变量 包含私有的 也包含从父类继承过来的成员变量
public Field getField(String name) 获取单个成员变量包含从父类继承过来的
public Field getDeclaredField(String name) 获取单个成员变量 包含私有的 也包含从父类继承过来的成员变量

这四种方法返回的类型都是java.lang.reflect.Field类型,这个类用来描述类中的成员信息。
Field类常用的方法有:

方法 解释
public Class getType() 获取该成员的类型
public Object get(Object obj) 获取指定对象中的成员的内容,相当于直接访问成员
public void set(Object obj,Object value) 设置指定对象中的成员内容,相当于直接利用对象调用成员设置内容

实际在Field中还定义了许多setXxx(),getXxx()方法,可以直接设置这些方法或方法具体的类型。

再次重申,不管使用反射调用普通方法还是成员,都必须存在实例化对象(通常依靠反射获取),因为类中的属性必须在类产生实例化对象(有堆内存空间)后才可以使用。

举个例子:

package com.xx.test1;

import java.lang.reflect.*;

class Student {
    private String name;

    public Student() {
    }
}

public class GetCons {

    public static void main(String[] args) throws Exception {
        Class aClass = Class.forName("com.xx.test1.Student");
        // 实例化Student类
        Object obj = aClass.newInstance();
        // 获取成员对象
        Field nameField = aClass.getDeclaredField("name");
        // 取消封装
        nameField.setAccessible(true);
        // 赋值
        nameField.set(obj, "James");
        // 相当于Student类对象.name
        Object o = nameField.get(obj);
        // 输出
        System.out.println(o);
    }
}

我们在访问私有方法和私有成员都用到了这句代码对象.setAccessible(true);,这个方法是public void setAccessible(boolean flag) throws SecurityException,如果设置为true则表示此内容可以被直接操作,它是AccessibleObject类中的方法。AccessibleObject类是Filed和Executable类的父类,而Executable类又是Constuctor类和Method类的父类,所以Filed,Constuctor,Method这三个类都可以使用这个方法来取消封装

反射的简单应用

反射运行配置文件内容

需求:通过配置文件的内容来动态创建类对象,调用类中的set方法为对象赋处置,并调用指定方法。

  1. 配置文件reflect.properties
# 反射操作类
className=com.xx.apply.User
# 初值
name=James
age=19
# 方法名
methodName=showUser
  1. User类
package com.xx.apply;

public class User {
    private String name;
    private Integer age;

    public User() {

    }

    public String showUser() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }


}

  1. 测试类
package com.xx.apply;

import java.io.FileReader;
import java.lang.reflect.Method;
import java.util.Properties;


public class Test {

    public static void main(String[] args) throws Exception {
        //    首先要读取配置文件
        Properties properties = new Properties();
        properties.load(new FileReader("src\\com\\xx\\apply\\reflect.properties"));
        // 获取指定参数
        String className = properties.getProperty("className");
        String userName = properties.getProperty("name");
        String userAge = properties.getProperty("age");
        String methodName = properties.getProperty("methodName");
        //    获取User类的Class对象
        Class<?> aClass = Class.forName(className);
        User user = (User) aClass.newInstance();
        //    为user对象通过set方法设值
        user.setAge(Integer.valueOf(userAge));
        user.setName(userName);
        //    调用指定方法
        Method method = aClass.getMethod(methodName);
        Object invoke = method.invoke(user);
        System.out.println(invoke);
        // User{name='James', age=19}

    }
}

大概过程就是这样,有缺陷的地方就是需要赋初值的set方法需要自己写,如果把它改为配置文件写,测试类获取,采用动态拼串的方法就非常完美了。这种应用其实就是IOC的雏形,即所谓的控制反转。这里思想一致,但效果太小儿科了,过两天在Spring分类里我会写一个比较完善的IOC例子。

利用反射越过泛型检查

如果我有一个ArrayList的对象,我想在这个集合中添加一个字符串数据,如何实现呢?我们都知道泛型指定的类型会在编译器进行校验,既然我们不能在编译器进行,就只能在运行期完成。因为泛型到了运行期以后,泛型会自动擦除,那么要在运行期完成这个需求,那么我们就需要使用反射。

具体实现:

package com.xx.apply;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class Test2 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List<Integer> list = new ArrayList<Integer>();
        // 泛型在编译期会进行类型校验,我们只能插入Integer类型的数据
        list.add(1);
        // 而不能插入String类型的数据
        //list.add("sss");

        //    使用反射来插入,反射是在运行期操作的,此时泛型已经擦除
        Class<? extends List> aClass = list.getClass();
        Method add = aClass.getMethod("add", Object.class);
        add.invoke(list, "测试");
        //    输出
        System.out.println(list);
        // [1, 测试]
    }
}

这个应用暂时还想不到有什么实际用处,有点鸡肋,姑且当个好玩的图个乐子吧。

全文完。

你可能感兴趣的:(JavaSE)