Java反射机制,reflect
写在前面:这个教程适合边看边练这种方式,如果你手边没有电脑又是第一次了解这部分知识的,建议不用看了!
反射机制的作用:
通过Java语言的反射机制可以操作字节码文件(class文件),有点类似与黑客(可以读和修改字节码文件),通过反射机制可以操作代码片段。
反射机制相关类的包在:
java.lang.reflect.*
反射机制相关重要的几个类:
java.lang.Class,代表整个字节码,代表一个类型,代表一个类;
java.lang.reflect.Method,代表字节码中的方法字节码,代表类中的方法;
java.lang.reflect.Constructor,代表字节码中的构造方法字节码,代表类中的构造方法;
java.lang.reflect.Field,代表字节码中的属性字节码,代表类中的成员变量(静态和实例变量);
// Class
public class User{
// Field
int no;
// Constructor
public User(){
}
public User(int no){
this.no = no;
}
// Method
public void setNo(int no){
this.no = no;
}
public int getNo(){
return no;
}
}
1、获取字节码文件
要操作一个类的字节码,需要首先获取这个类的字节码,怎么获取java.lang.Class实例?
有三种方法:
- 第一种:Class c = Class.forName(“完整的类名带包名”);
- 第二种:Class c = 对象.getClass();
- 第三种:Class c = 任何类型.class;
package javase.reflect;
import java.util.Date;
public class ReflectTest01 {
public static void main(String[] args) {
/*
Class.forName()的说明:
1. 静态方法;
2. 方法的参数是一个字符串;
3. 字符串需要的是一个完整的类名(包括包名);
*/
Class c1 = null;
Class c2 = null;
try {
// c1 代表String.class文件,或者说c1代表String类型
c1 = Class.forName("java.lang.String");
// c2 代表Date类型
c2 = Class.forName("java.util.Date");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// java中任何一个对象都有一个方法(Object中定义的):getClass()
String s = "abc";
// x 代表String.class字节码文件,即String类型
Class x = s.getClass();
// 这里c1和x两个变量中保存的内存地址都是一样的,都指向方法区中的字节码文件,所以返回true
System.out.println(c1 == x); // true , 双等号比较的两个对象保存的内存地址
Date time = new Date();
Class y = time.getClass();
System.out.println(c2 == y); // true
// java语言中任何一种类型,包括基本数据类型,它都有.class属性
Class z = String.class; // z 代表String类型
Class k = Date.class; // k 代表Date类型
Class f = int.class; // f 代表int类型
System.out.println(z == x); // true
}
}
2、通过反射获取实例
获取到Class能干啥?
- 通过Class的newInstance()方法来实例化对象。
- 注意:newInstance()方法内部实际上调用了类的无参数构造方法,必须保证无参构造方法存在才可以。
package javase.reflect;
import javase.bean.User;
public class ReflectTest02 {
public static void main(String[] args) {
// 不使用反射机制创建对象
User user = new User();
// 使用反射机制创建对象,相对于上面的那种方式,反射机制更加灵活!!!
try {
// 通过反射机制获取Class,在通过Class来实例化对象
// 这里的 c 的类型为 User,也可以将 c 理解为 User 的对象
Class c = Class.forName("javase.bean.User");
// newInstance()这个方法会调用User这个类的无参数构造方法,完成对象的创建
/*
注:当我们编写一个类时,无参构造方法默认就已经存在了;
但是如果我们只在类中定义有参构造而没有编写无参构造方法时,这时使用newInstance()方法
会出现著名的:InstantiationException异常。所以一般在编写完类之后,习惯性的就将无参构造编写好。
*/
// 重点: newInstance()方法调用的是类的无参构造方法;
Object obj = c.newInstance();
System.out.println(obj);
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
User类
package javase.bean;
public class User {
String no;
public User() {
System.out.println("无参数构造方法被执行了");
}
public User(String no) {
this.no = no;
}
}
3、通过配置文件创建实例
验证反射机制的灵活性:
-
java代码写一遍,在不修改java源代码的基础之上,可以到不同对象的实例化,非常之灵活!
-
符合OCP开闭原则:即对扩展开放,对修改关闭。
-
后期在学习高级框架以及工作的过程之中使用的都是高级框架,如:ssh、ssm…
-
即:Spring Struts Hibernate 、Spring SpringMVC Mybatis…
-
这些高级框架底层实现原理都是采用了反射机制。所以反射机制还是很重要的。
package javase.reflect;
import javase.bean.User;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
public class ReflectTest03 {
public static void main(String[] args) {
// 这种方式代码就写死了,只能创建 User 类型的对象
User user = new User();
// 以下代码是灵活的,代码不需要改动,可以在修改配置文件的基础上创建出不同的实例对象。
// 通过IO流读取classinfo.properties文件
FileReader reader = null;
Properties properties = new Properties();
try {
reader = new FileReader("javase-reflect/classinfo.properties");
properties.load(reader);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (reader != null)
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// 通过key获取value
String className = properties.getProperty("className");
Object o = null;
// 通过反射机制创建对象
try {
Class c = Class.forName(className);
o = c.newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
System.out.println(o);
}
}
4、Class.forName()执行过程
研究一下:Class.forName()执行过程放生了什么?
- 重点:如果只是希望一个类的静态代码块执行,其他代码一律不执行,则可以使用Class.forName(“完整类名”);
- 这个方法的执行会导致类加载,类加载时,被反射类的静态代码块会被执行;
- 提示:这个在后面JDBC技术的时候需要。
package javase.reflect;
public class ReflectTest04 {
public static void main(String[] args) {
try {
// Class.forName()这个方法的执行会导致类的加载
Class.forName("javase.reflect.MyClass");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class MyClass{
// 静态代码快在类加载时会被执行,并且只执行一次
static {
System.out.println("MyClass类的静态代码块执行了");
}
}
5、类路径下文件的绝对路径
研究文件路径:
- 怎么获取一个文件的绝对路径?一下讲解的这种方式是通用的,但前提该文件必须是在类路径下才能使用这种方式。
package javase.reflect;
import java.io.FileReader;
import java.io.InputStream;
import java.util.Properties;
/**
* 研究文件路径:
* 怎么获取一个文件的绝对路径?一下讲解的这种方式是通用的,但前提该文件必须是在类路径下才能使用这种方式。
*/
public class AboutPath {
public static void main(String[] args) throws Exception{
// 这种方式的路径缺点是:移植性查,在IDEA中默认当前路径是project的根。
// 这种写法只要将文件的位置稍微更换一下,这个路径就无效了。
// FileReader fileReader = new FileReader("javase-reflect/classinfo.properties");
/*
接下来讲解的这种方式比较通用,即使是代码位置换了,这样编写仍然是通用的;
注意:这种方法使用前提是:改文件必须在类路径下;
什么是类路径?即src目录下的都是类路径,src被称为类的根路径;
*/
/*
Thread.currentThread() 表示当前线程对象
getContextClassLoader() 是当前线程对象的方法,可以获取到当前线程的类加载器对象
getResource() 这是类加载器对象的方法,当前线程的类加载器默认从类的根路径下加载资源
*/
// 这种方式可以获取到类路径下文件的绝对路径
String dbPath = Thread.currentThread().getContextClassLoader()
.getResource("javase/bean/db.properties").getPath();
System.out.println(dbPath);
// 通过以上的方法获取文件绝对路径变得更加灵活了,下面直接使用流的形式返回文件内容
// 以前获取文件流的方式为:
//FileReader fileReader = new FileReader(dbPath);
// 直接以流的形式返回
InputStream resource = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("javase/bean/db.properties");
Properties properties = new Properties();
properties.load(resource);
// 关闭流
resource.close();
// 通过key获取value
String username = properties.getProperty("username");
System.out.println(username);
}
}
6、资源绑定器
- java.util包下提供了一个资源绑定器,便于获取属性配置文件中的内容;
- 使用资源绑定器获取的属性配置文件xxx.properties必须位于类路径下(即以src作为根路径)。
package javase.reflect;
import java.util.ResourceBundle;
public class ResourceBundleTest {
public static void main(String[] args) {
// 资源绑定器,只能绑定properties文件,并且在写文件路径时,属性配置文件的后缀不用写
ResourceBundle db = ResourceBundle.getBundle("db");
ResourceBundle info = ResourceBundle.getBundle("javase/bean/info");
String username = db.getString("username");
String service = info.getString("service");
System.out.println(username); // root
System.out.println(service); // javaweb
}
}
7、类加载器(了解)
关于JDK中自带的类加载器,什么是类加载器?
专门负责加载类的命令/工具:ClassLoader
JDK中自带了三个类加载器:
启动类加载器:rt.jar
扩展类加载器:ext/*.jar
应用类加载器:classpath
假设有这样一段代码:
String s = "abc";
代码在开始执行之前,会将所需要的类全部加载到JVM当中,通过类加载器加载,看到以上代码类加载器会找到String.class文件,找到就加载,那么是如何加载的呢?
首先通过“启动类加载器”加载:
注意:启动类加载器专门加载:%JAVA_HOME%/jre/lib/rt.jar,rt.jar中都是JDK最核心的类库。
如果通过“启动类加载器”加载不到的时候,会通过“扩展类加载器”加载:
扩展类加载器专门加载:%JAVA_HOME%/jre/lib/ext/*.jar
如果扩展类加载器没有加载到,那么会通过“应用类加载器”加载:
应用类加载器专门加载:classpath中的类
Java中为了保证类加载的安全,使用了双亲委派机制。优先从启动类加载器中加载,这个称为“父”,当它无法加载到时,再从扩展类加载器中加载,这个称为“母”。双亲委派如果都加载不到,才会考虑从应用类加载器中加载,直到加载到位置。