1.反射的作用是什么?
通过反射机制可以操作字节码文件。
相关类:
- java.lang.Class:字节码文件类;
- java.lang.reflect.Field:属性字节码类;
- java.lang.reflect.Constructor:构造方法字节码;
- java.lang.reflect.Method:方法字节码。
要操作字节码文件首先就要获取到字节码文件。
2.获取Class的三种方式
(1)通过类的全称限定名
语法:
Class c = Class.forName("类的全称限定名");
(2)通过对象的getClass()方法
语法:
Class c = 对象.getClass();
(3)通过.class属性
Java语言中,任何一种类型,包括基本数据类型,都有class属性。
语法:
类.class;
(4)三种获取Class方式的实例
package com.dh.reflect;
public class Reflect01 {
public static void main(String[] args) {
Class c2 = null;
try {
//1.Class.forName()方法
Class c = Class.forName("com.dh.reflect.User");
c2 = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//2.对象.getClass()
String s = "abc";
Class c3 = s.getClass();
//不同方式获取到的字节码是一样的
System.out.println(c2 == c3); //true
//3.通过class属性
Class c4 = String.class;
System.out.println(c3 == c4); //true
}
}
3.Class类的常用方法
当获取到了一个字节码文件时,即Class对象,接下来就是通过Class类中的方法来操作字节码文件了。
我们先来看看,直接输出获取到的字节码文件的话是什么呢?
String s = "abc";
Class c3 = s.getClass();
System.out.println(c3);
结果:
class java.lang.String
由结果可知:直接输出通过反射获取到的字节码文件,输出的是:class+类的全程限定名。
如果我们只想要基础类简单的名称呢,即只要String。
(1)获取字节码文件的简单名称
- String getSimpleName()
例:
String s = "abc";
Class c3 = s.getClass();
String c3SimpleName = c3.getSimpleName();
System.out.println(c3SimpleName);
结果:
String
(2)构造对象
- Object newInstance():通过要构造对象的类的空的构造方法构造对象
所以必须存在空的构造方法!!!
User类:
package com.dh.reflect;
public class User {
public User() {
System.out.println("构造方法执行了。。。");
}
}
测试类:
package com.dh.reflect;
public class Reflect02 {
public static void main(String[] args) {
Class c = null;
try {
//获取字节码文件
c = Class.forName("com.dh.reflect.User");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
//构造对象
Object o = c.newInstance(); //被弃用,有替代方法,后续使用
System.out.println(o);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
结果:
构造方法执行了。。。
com.dh.reflect.User@49e4cb85
可以看到,无参构造方法被执行了。
如果没有提供无参构造,会出现实例化异常:
java.lang.InstantiationException: com.dh.reflect.User
at java.base/java.lang.Class.newInstance(Class.java:571)
at com.dh.reflect.Reflect02.main(Reflect02.java:15)
Caused by: java.lang.NoSuchMethodException: com.dh.reflect.User.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3349)
at java.base/java.lang.Class.newInstance(Class.java:556)
... 1 more
为什么要使用反射机制构造对象呢?
看到这,不知道你会不会有一个疑问呢?为什么要使用反射机制这种这么麻烦的方式去构造对象呢?直接new它不香吗?
如果是new的话,想要改变new的对象时,就得修改源代码;
但是如果是反射机制,还记得属性配置文件吗?
我们可以将类的全称限定名书写在属性配置文件中,要改变new的对象时,只需要修改属性配置文件,而不需要修改源代码。
更加灵活。
例:
属性配置文件:
className = com.dh.reflect.User
测试代码:
package com.dh.reflect;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class Reflect03 {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("Reflect/src/class.properties");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Properties properties = new Properties();
try {
properties.load(reader);
if(reader != null){
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
String className = properties.getProperty("className");
//获取Class
try {
Class c = Class.forName(className);
System.out.println(c);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
(高级框架底层实现原理都采用了反射机制)
扩展一:如何实现只执行一个类的静态代码块
Class.forName()会导致类加载,会把类加载到JVM中,所以会执行静态代码块;
所以如果只想让一个类的静态代码块执行的话,就可以使用Class.forName()。
例:
package com.dh.reflect;
public class Reflect04 {
public static void main(String[] args) {
try {
Class c = Class.forName("com.dh.reflect.Test");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Test {
static {
System.out.println("静态代码块");
}
}
结果:
静态代码块
扩展二:类加载器(双亲委派模型)
上述提到了类加载器,Java中有三种类加载器:
- 启动类加载器;(父加载器)
- 扩展类加载器;(母加载器)
- 应用类加载器。
假设有String s = "abc";
代码在开始执行之前,会将所需的类全部加载到JVM中,通过类加载器加载;
首先通过启动类加载器加载,在jdk\jre\lib\rt.jar(JDK中最核心的类库)中找;
找不到,就去扩展类加载器中找,在jdk\jre\lib\ext里找,
再找不到,就去应用类加载器中找(专门加载classpath下的jar包)
这种就是双亲委派模型:优先从启动类加载器(父加载器)中找,找不到再找扩展类加载器(母加载器),最后再去应用类加载器中找。
为什么需要双亲委派模型呢?
因为如果自己写了一个String类,sun公司也有一个写好的String类,自己写的植入了后门程序,如果在类似于银行的项目中使用了很多String,就很不安全了!
扩展三:获取类路径下文件的绝对路径
以上的参数都是相对路径,代码一换位置,就会找不到文件了,此时需要使用文件的绝对路径,但是又不能直接写死绝对路径,因为如果写死了绝对路径,也不能移植到Linux环境下,所以需要动态的去获取绝对路径。
前提:文件放在src下。
类文件的根目录是项目的out文件夹,out文件夹下直接是IDEA项目中src下的内容,所以我们可以当作类文件的根目录就是src。
死记硬背下来!!!
String absolutePath = Thread.currentThread().getContextClassLoader().getResource("文件相对于类文件根路径下的相对路径").getPath();
//路径不能有中文,否则会乱码
//再将absolutePath放于io中
上述是以String的形式返回,也可以直接返回流
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("文件相对于类文件根路径下的相对路径");
//此时不再需要构建新的io对象了
资源绑定器
Java还帮我们写好了一个工具类,java.util.ResourceBundle,可以直接获取属性配置文件中的内容。
但是有限制:
- 文件必须放在类路径下;
- 必须是properties文件。
ResourceBundle bundle = ResourceBundle.getBundle("属性文件相对于类文件根路径下的相对路径(注意:属性配置文件只要名字不要后缀.properties,写了会报错)");
bundle.getString("属性配置文件中的key");//获取属性配置文件中指定的值
为防止文章篇幅太长,使用Class类中操作类的属性、构造器和方法,以及父类和接口在下一篇文章介绍。