一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题
Java程序中,所有的对象都有两种类型:编译时类型
和运行时类型
,而很多时候对象的编译时类型和运行时类型不一致
。
Object obj = new String(“hello”);
例如:某些变量或形参的声明类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object中的方法,那么如何解决呢?
解决这个问题,有两种方案:
方案1: 在编译和运行时都完全知道 类型的具体信息,在这种情况下,我们可以直接先使用instanceof
运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
方案2: 编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息
来发现该对象和类的真实信息,这就必须使用反射
。
Reflection(反射)是被视为
动态语言
的关键,反射机制允许程序在运行期间
借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区
中就产生了一个Class类型的对象
(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。
优点:
提高了Java程序的灵活性和扩展性,降低了耦合性
,提高自适应
能力
允许程序创建和控制任何类的对象,无需提前硬编码
目标类
缺点:
反射的性能较低
。
反射会模糊
程序内部逻辑,可读性较差
。
反射机制是 Java实现动态语言的关键,也就是通过
反射
实现类的动态加载
。
静态加载: 编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。
当新创建一个对象时(new),该类会被加载;
当调用类中的静态成员时,该类会被加载;
当子类被加载时,其超类也会被加载;
动态加载: 运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。
通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;
类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程
又分为:加载、链接、初始化三个阶段。
类的加载又分为三个阶段:
(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>()方法
在多线程环境中被正确加锁和同步。
作用:负责类的加载,并对应于一个Class的实例。
BootstrapClassLoader:引导类加载器、启动类加载器
使用C/C++语言编写的,不能通过Java代码获取其实例。负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)
继承于ClassLoader的类加载器
(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();
关于类加载器的一个主要方法:getResourceAsStream(String str)
:获取类路径下的指定文件的输入流
InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\test.properties");
System.out.println(in);
//需要掌握如下的代码
@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);
}
在Object类中定义了以下的方法,此方法将被所有子类继承:
public final Class getClass()
以上的方法返回值的类型是一个Class
类,此类是Java反射的源头。
Class
也是一个类,其类名就叫Class
,因此它也继承 Object
类Class
是由JVM
在执行过程中动态加载的。JVM
在第一次读取到一种类Class
时,会将其加载进内存。每加载一种Class
,JVM
就为其创建一个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类对象的。
Class
对象(实例——>Class)Class
类对象可以完整地得到其对应的类的所有的信息(Class——>实例)因此,如果获取了某个
Class
类对象,我们就可以通过这个Class
类对象获取到其对应的类class
的所有信息。
这种通过Class
实例获取类lass
信息的方法称为反射(Reflection
)。
Class
类是Reflection
的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class
对象。哪些类型可以有Class对象?
(1)class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
(2)interface:接口
(3)[]:数组
(4)enum:枚举
(5)annotation:注解@interface
(6)primitive type:基本数据类型
(7)void
方式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类对象:
方法名 | 功能说明 | 举例 |
---|---|---|
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 |
创建运行时类的对象有两种方式:
要求: 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);
getDeclaredConstructor(Class … parameterTypes)
取得本类的指定形参类型的构造器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);
对任意的一个Object实例,只要我们获取了它对应的Class类对象,就可以获取它的一切信息。
如:通过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类型
利用反射拿到字段的一个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
来访问Person
的name
字段,编译器会根据public、protected
和private
这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。
此外,
setAccessible(true)
可能会失败。 如果JVM
运行期存在SecurityManager
,那么它会根据规则进行检查,有可能阻止setAccessible(true)
。例如,某个SecurityManager
可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证
JVM` 核心库的安全。
设置字段值是通过Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的对象,第二个Object
参数是待修改的值。
/*
* 以前:学生对象.setId(值)
* 现在:id属性对象.set(学生对象,值)
*/
idField.set(stu, 2);
value = idField.get(stu);
System.out.println("id = "+ value);
可以通过Class
类获取所有Method
信息。Class
类提供了以下几个方法来获取类class
中定义的Method
:
Method getMethod(name, Class...):获取某个public的Method(包括父类)
Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
Method[] getMethods():获取所有public的Method(包括父类)
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 表示不同的含义。对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);//张三
如果获取到的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
为了调用非 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
一般情况下,我们通常使用
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...):获取某个public的Constructor;
getDeclaredConstructor(Class...):获取某个Constructor;
getConstructors():获取所有public的Constructor;
getDeclaredConstructors():获取所有Constructor。
注意:Constructor类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。同样,调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。但setAccessible(true)也可能会失败。
Class i = Integer.class;
Class n = i.getSuperclass();
由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
getInterfaces()
方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。一个完整的注解应该包含三个部分:
(1)声明 (2)使用 (3)读取
@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文档中。@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;
}
......
}
我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是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();
}
}