Java中Class类和反射机制

发布时间:2024年01月04日


一、什么是反射?

一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题

1.1 反射出现的背景

Java程序中,所有的对象都有两种类型:编译时类型运行时类型,而很多时候对象的编译时类型和运行时类型不一致

Object obj = new String(“hello”);

例如:某些变量或形参的声明类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢?

解决这个问题,有两种方案:
方案1: 在编译和运行时都完全知道 类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
方案2: 编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射

1.2 什么是反射

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。

1.3 反射的优缺点

优点:

  • 提高了Java程序的灵活性和扩展性,降低了耦合性,提高自适应能力

  • 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

  • 反射的性能较低

    • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上
  • 反射会模糊程序内部逻辑,可读性较差

二、反射 与 Class类 与 类加载过程 之间的关系

2.1 反射与动态加载

反射机制是 Java实现动态语言的关键,也就是通过反射实现类的动态加载

静态加载: 编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。

当新创建一个对象时(new),该类会被加载;
当调用类中的静态成员时,该类会被加载;
当子类被加载时,其超类也会被加载;

动态加载: 运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。

通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;

2.2 类加载过程

类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程又分为:加载、链接、初始化三个阶段。
在这里插入图片描述

在这里插入图片描述
类的加载又分为三个阶段:

(1)装载(Loading)

将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成

(2)链接(Linking)

①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。

②准备Prepare:正式为类变量(static)分配内存设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。

public class ClassLoad {
    public static void main(String[] args) {
		// 属性=成员变量=字段
    	// 类加载的连接阶段-准备,属性是如何加载的

    	public int n1 = 10;
    	public static  int n2 = 20;
    	public static final  int n3 = 30;
    }
}
  1. n1 是实例属性, 不是静态变量,因此在准备阶段,是不会分配内存
  2. n2 是静态变量,在该阶段 JVM 会为其分配内存,n2 默认初始化的值为 0 ,而不是 20
  3. n3 被 static final 修饰,是常量, 它和静态变量不一样, 其一旦赋值后值就不变,因此其默认初始化 n3 = 30

③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

(3)初始化(Initialization)

  • 执行类构造器<clinit>()方法的过程。类构造器< clinit >()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。 负责对类的静态成员进行初始化。

  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

2.3 类加载器

作用:负责类的加载,并对应于一个Class的实例。

2.3.1 分类:(分为两种)

  • BootstrapClassLoader:引导类加载器、启动类加载器
    使用C/C++语言编写的,不能通过Java代码获取其实例。负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)

  • 继承于ClassLoader的类加载器

    • ExtensionClassLoader:扩展类加载器
      负责加载从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库
    • SystemClassLoader/ApplicationClassLoader:系统类加载器、应用程序类加载器
      我们自定义的类,默认使用的类的加载器。
    • 用户自定义类的加载器
      实现应用的隔离(同一个类在一个应用程序中可以加载多份);数据的加密。
      在这里插入图片描述

2.3.2 查看某个类的类加载器对象

(1)获取默认的系统类加载器

ClassLoader classloader = ClassLoader.getSystemClassLoader();

(2)查看某个类是哪个类加载器加载的

ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();
//如果是根加载器加载的类,则会得到null
ClassLoader classloader1 = Class.forName("java.lang.Object").getClassLoader();

(3)获取某个类加载器的父加载器

ClassLoader parentClassloader = classloader.getParent();

2.3.3 使用ClassLoader获取流

关于类加载器的一个主要方法:getResourceAsStream(String str):获取类路径下的指定文件的输入流

InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);
  • 获取properties文件的信息的两种方式
//需要掌握如下的代码
    @Test
    public void test5() throws IOException {
        Properties pros = new Properties();
        //方式1:使用文件输入字节流FileInputStream。
        //此时默认的相对路径是当前的module。但是可以自定义设置!
        FileInputStream is = new FileInputStream("info.properties");
        FileInputStream is = new FileInputStream("src//info1.properties");

        //方式2:使用类的加载器
        //此时默认的相对路径只能是当前module的src目录
        InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("info1.properties");
        
		pros.load(is);
        //获取配置文件中的信息
        
        String name = pros.getProperty("name");
        String password = pros.getProperty("password");
        System.out.println("name = " + name + ", password = " + password);
    }

2.4 Class类

在Object类中定义了以下的方法,此方法将被所有子类继承:

2.4.1 Class类的特征

public final Class getClass()

在这里插入图片描述

以上的方法返回值的类型是一个Class类,此类是Java反射的源头。

  1. Class也是一个类,其类名就叫Class,因此它也继承 Object
  2. Class是由JVM在执行过程中动态加载的。JVM在第一次读取到一种类Class时,会将其加载进内存。每加载一种ClassJVM就为其创建一个Class类的对象。因此,一个Class对象对应的是一个加载到JVM中的一个.class文件。

以String类为例,当 JVM加载String类时,它首先读取String.class文件到内存,然后,在堆中为String类创建一个Class类对象并将两者关联起来:

Class cls = new Class(String);
  • 注意:这个Class类对象是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现Class类的构造方法是private,即只有 JVM 能创建Class类对象,我们程序员自己的 Java 程序是无法创建Class类对象的。
  1. 每个类的实例对象都会知道自己对应的Class对象(实例——>Class)
  2. 通过Class类对象可以完整地得到其对应的类的所有的信息(Class——>实例)

因此,如果获取了某个Class类对象,我们就可以通过这个Class类对象获取到其对应的类class的所有信息。
这种通过Class实例获取类lass信息的方法称为反射Reflection)。

  1. 类的字节码二进制数据,是存放在方法区的,又称为类的元数据(包括方法代码、变量名、方法名、访问权限等等)
  2. Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象。

哪些类型可以有Class对象?

1class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2interface:接口
(3[]:数组
(4enum:枚举
(5)annotation:注解@interface6)primitive type:基本数据类型
(7void

2.4.2 获取Class类的实例(四种方法)

方式1: 要求编译期间已知类型,已知具体的类

前提:通过类的class属性获取,该方法最为安全可靠,程序性能最高

Class clazz = String.class;

方式2:获取对象的运行时类型,已知某个类的实例
前提:调用该实例的getClass()方法获取Class对象

Class clazz = "www.atguigu.com".getClass();Person p1 = new Person();
Class clazz2 = p1.getClass();

方式3:可以获取编译期间未知的类型,已知某个类的全类名 且该类在类路径下

前提:通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException

Class clazz = Class.forName("java.lang.String");

方式4:其他方式(不做要求),

前提:用系统类加载对象或自定义加载器对象,可以加载指定路径下的类型

ClassLoader cl = this.getClass().getClassLoader();
Class clazz4 = cl.loadClass("类的全类名");

因为Class类对象在 JVM 中是唯一的,所以,上述方法获取的Class类对象是同一个对象。可以用==比较两个Class类对象:

2.4.3 Class类的常用方法方法

方法名功能说明举例
static Class forName(String name)返回指定类名 name 的 Class 对象
Object newInstance()调用缺省构造函数,返回该Class对象的一个实例
getName()返回此Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperClass()返回当前Class对象的父类的Class对象
Class [] getInterfaces()获取当前Class对象的接口
ClassLoader getClassLoader()返回该类的类加载器
Class getSuperclass()返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors()返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields()返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes)返回一个Method对象,此对象的形参类型为paramType

2.5 反射的基本应用

2.5.1 创建运行时类的对象

创建运行时类的对象有两种方式:

  • 方式1:直接调用Class对象的newInstance()方法

要求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足够。
步骤:
1)获取该类型的Class对象
2)调用Class对象的newInstance()方法创建对象

Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguClass");
//clazz代表com.atguigu.ext.demo.AtGuiguClass类型
//clazz.newInstance()创建的就是AtGuiguClass的对象
Object obj = clazz.newInstance();
System.out.println(obj);
  • 方式2:通过获取构造器对象来进行实例化
    步骤:
    1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
    2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
    3)通过Constructor实例化对象。

如果构造器的权限修饰符修饰的范围不可见,也可以调用setAccessible(true)

 Class<?> clazz = Class.forName("com.atguigu.ext.demo.AtGuiguDemo");
 /*
 * 获取AtGuiguDemo类型中的有参构造
 * 如果构造器有多个,我们通常是根据形参【类型】列表来获取指定的一个构造器的
 * 例如:public AtGuiguDemo(String title, int num)
  */
//(2)获取构造器对象
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);

//(3)创建实例对象
 // T newInstance(Object... initargs)  这个Object...是在创建对象时,给有参构造的实参列表
Object obj = constructor.newInstance("尚硅谷",2022);
System.out.println(obj);

2.5.2 访问字段

对任意的一个Object实例,只要我们获取了它对应的Class类对象,就可以获取它的一切信息。

2.5.2.1 获得字段信息field()

如:通过Class类对象获取其对应的类定义的字段信息(包括权限 修饰符 变量名...)

Field getField(name):根据字段名获取某个 public 的 field(包括父类)
Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)

Field[] getFields():获取所有 public 的 field(包括父类)
Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)
public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 获取public字段"score":
        System.out.println(stdClass.getField("score"));
        // 获取继承的public字段"name":
        System.out.println(stdClass.getField("name"));
        // 获取private字段"grade":
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}
class Student extends Person {
    public int score;
    private int grade;
}
class Person {
    public String name;
}
  • 一个Field对象包含了一个字段的所有信息

    • getName():返回字段名称,例如,“name”;

    • getType():返回字段类型,也是一个Class类对象,例如,String.class;

    • getModifiers():返回字段的修饰符,它是一个int,不同的 bit 表示不同的含义。

Field f = Student.class.getDeclaredField("grade");
f.getName(); // "grade"
f.getType(); // class int类型	
2.5.2.2 获得字段的值

利用反射拿到字段的一个Student类对象只是第一步,我们还可以通过Field.get(Object)拿到一个实例对象对应的该字段的值。其中第一个Object参数是指定的对象.

例如,对于一个Student类对象,我们可以先拿到其id字段对应的Field,再获取这个Student类对象的id字段的 值:

//1、获取Student的Class对象
Class clazz = Class.forName("com.atguigu.reflect.Student");

 //2、获取属性对象,例如:id属性
Field idField = clazz.getDeclaredField("id");

 //3、如果id是私有的等在当前类中不可访问access的,我们需要做如下操作
 idField.setAccessible(true);

//4、创建实例对象,即,创建Student对象
Object stu = clazz.newInstance();

 //5、获取属性值
/*
 * 以前:int 变量= 学生对象.getId()
* 现在:Object id属性对象.get(学生对象)
 */
Object value = idField.get(stu);
System.out.println("id = "+ value);

如果使用反射可以获取private字段的值,那么类的封装还有什么意义?

答案是一般情况下,我们总是通过p.name来访问Personname字段,编译器会根据public、protectedprivate这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。

此外,setAccessible(true)可能会失败。 如果JVM运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证 JVM` 核心库的安全。

2.5.2.3 设置字段的值

设置字段值是通过Field.set(Object, Object)实现的,其中第一个Object参数是指定的对象,第二个Object参数是待修改的值。

 /*
* 以前:学生对象.setId(值)
* 现在:id属性对象.set(学生对象,值)
*/
idField.set(stu, 2);
value = idField.get(stu);
System.out.println("id = "+ value);

2.5.3 调用方法

可以通过Class类获取所有Method信息。Class类提供了以下几个方法来获取类class中定义的Method

2.5.3.1 获得方法的信息
Method getMethod(name, Class...):获取某个publicMethod(包括父类)
Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
Method[] getMethods():获取所有publicMethod(包括父类)
Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
Class stdClass = Student.class;
// 获取 public方法 getScore,形参类型为 String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的 public方法 getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取 private方法 getGrade,形参类型为 int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
  • 一个Method类对象包含一个方法的所有信息:
    • getName():返回方法名称,例如:“getScore”;
    • getReturnType():返回方法的返回值类型,也是一个Class实例,例如:String.class;
    • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};
    • getModifiers():返回方法的修饰符,它是一个int,不同的 bit 表示不同的含义。
2.5.3.2 调用方法

Method类对象调用invoke方法就相当于调用该substring(int)方法,invoke的第一个参数是实例对象(即在哪个实例对象上调用该方法),后面的实参要与方法参数的类型一致,否则将报错。

// 1、获取Student的Class对象
Class<?> clazz = Class.forName("com.atguigu.reflect.Student");

//2、获取方法对象
/*
* 在一个类中,唯一定位到一个方法,需要:(1)方法名(2)形参列表,因为方法可能重载
*
* 例如:void setName(String name)
*/
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
//3、创建实例对象
Object stu = clazz.newInstance();

//4、调用方法
/*
* 以前:学生对象.setName(值)
* 现在:方法对象.invoke(学生对象,值)
 */
Object setNameMethodReturnValue = setNameMethod.invoke(stu, "张三");

System.out.println("stu = " + stu);
//setName方法返回值类型void,没有返回值,所以setNameMethodReturnValue为null
System.out.println("setNameMethodReturnValue = " + setNameMethodReturnValue);

Method getNameMethod = clazz.getDeclaredMethod("getName");
Object getNameMethodReturnValue = getNameMethod.invoke(stu);
//getName方法返回值类型String,有返回值,getNameMethod.invoke的返回值就是getName方法的返回值
System.out.println("getNameMethodReturnValue = " + getNameMethodReturnValue);//张三
2.5.3.3 调用静态方法

如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)方法为例:

 // 获取 Integer.parseInt(String) 方法,参数为 String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 调用该静态方法并获取结果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印调用结果:
System.out.println(n);// 12345
2.5.3.4 调用非 public方法

为了调用非 public 方法,我们通过Method.setAccessible(true)允许其调用:

Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);// Bob

2.5.4 调用构造方法

一般情况下,我们通常使用new操作符创建新的对象:

Person p = new Person();

如果通过反射来创建新的对象,可以调用Class提供的newInstance()方法:

Person p = Person.class.newInstance();
  • 调用Class.newInstance()的局限是,它只能调用该类的public无参构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
Constructor cons1 = Integer.class.getConstructor(int.class);
 // 调用构造方法:
// 传入的形参必须与构造方法的形参类型相匹配
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);

为了调用任意的构造方法,Java 的反射 API 提供了Constructor类对象,它包含一个构造方法的所有信息,通过Constructor类对象可以创建一个类的实例对象。Constructor类对象和Method类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象。

通过Class实例获取Constructor的方法如下:

getConstructor(Class...):获取某个publicConstructorgetDeclaredConstructor(Class...):获取某个ConstructorgetConstructors():获取所有publicConstructorgetDeclaredConstructors():获取所有Constructor

注意:Constructor类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。同样,调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。但setAccessible(true)也可能会失败。

2.5.5 获得继承关系

2.5.5.1 获得父类的Class
Class i = Integer.class;
Class n = i.getSuperclass();
2.5.5.2 获取interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
     System.out.println(i);
}
  • getInterfaces()方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。

2.5.6 读取注解信息

一个完整的注解应该包含三个部分:
(1)声明 (2)使用 (3)读取

2.5.6.1 声明自定义注解
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value();
}
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String columnName();
    String columnType();
}

(这里的注解是以Mysql中的表和表中某列 的写的)

  • 自定义注解可以通过四个元注解@Retention,@Target@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
2.5.6.2 使用自定义注解
@Table("t_stu")
public class Student {
	@Column(columnName = "sid",columnType = "int")
	private int id;
    @Column(columnName = "sname",columnType = "varchar(20)")
    private String name;
	public int getId() {
        return id;
    }
    ......
}
2.5.6.3 读取和处理自定义注解

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME

Class studentClass = Student.class;
        Table tableAnnotation = (Table) studentClass.getAnnotation(Table.class);
        String tableName = "";
        if(tableAnnotation != null){
            tableName = tableAnnotation.value();
        }
        
        Field[] declaredFields = studentClass.getDeclaredFields();
        String[] columns = new String[declaredFields.length];
        int index = 0;
        for (Field declaredField : declaredFields) {
            Column column = declaredField.getAnnotation(Column.class);
            if(column!= null) {
                columns[index++] = column.columnName();
            }
        }
文章来源:https://blog.csdn.net/2301_79561226/article/details/135359928
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。