反射机制(上)
传送门:反射机制(下)
1. 基本概念
-
反射机制有什么用?
通过Java语言中的反射机制可以操作字节码文件(class文件)
-
反射机制的相关类在java.lang.reflect.*包下
-
反射机制相关的重要的类有哪些?
- java.lang.Class:代表整个字节码,代表一个类型,整个类
- java.lang.reflect.Method:代表字节码中的方法字节码,代表类中的方法
- java.lang.reflect.Constructor:代表字节码中的构造方法字节码,代表类中的构造方法
- java.lang.reflect.Field:代表字节码中的属性字节码,代表类中的成员变量(静态变量+实例变量)
2. 获取类字节码的三种方式
要操作一个类的字节码,需要首先获取到这个类的字节码,怎么获取java.lang.Class实例?
-
第一种方式:Class.forName(“完整类名带包名”)
- forName为静态方法,方法的参数是一个字符串
- 字符串需要是一个完整的类名(必须带包名)
public class Test { public static void main(String[] args) { try { //c1代表String.class文件,或者说c1代表String类型 CLass c1 = Class.forName("java.lang.String"); Class c2 = Class.forName("java.lang.Integer");//c2代表Integer类型 Class c3 = Class.forName("java.util.Date"); //c3代表Date类型 System.out.println(c1); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
-
第二种方式:对象.getClass()
Java中任何一个对象都有getClass()方法
public class Test { public static void main(String[] args) { Class c1 =null; try { c1 = Class.forName("java.lang.String"); } catch (ClassNotFoundException e) { e.printStackTrace(); } String s = "abc"; Class x = s.getClass(); System.out.println(c1==x);//true(说明两个地址对象的地址相同) } }
-
第三种方式:数据类型.class属性
Java语言中任何一种类型,包括基本数据类型,都有.class属性
public class Test { public static void main(String[] args) { Class c1 =null; try { c1 = Class.forName("java.lang.String"); } catch (ClassNotFoundException e) { e.printStackTrace(); } String s = "abc"; Class x = s.getClass(); System.out.println(c1==x); Class n = String.class; Class z = int.class; System.out.println(n==x);//true } }
-
总结
无论通过何种方式,只要获取同一类型的类字节码Class,存取它们的地址一定是相同的
3. 获取Class的作用
通过反射机制,获取Class,通过Class来实例化对象
public class Test {
public static void main(String[] args) {
//不使用反射机制创建对象
User user = new User();
System.out.println(user);
//通过反射机制的方式创建对象
try {
//c代表User类型
Class c = Class.forName("com.study.www.threadshou.User");
//newInstance()方法会调用User类的无参数构造方法,完成对象的创建
Object obj =c.newInstance();
System.out.println(obj);//com.study.www.threadshou.User@1b6d3586
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
class User{
}
注:newInstance()调用的是无参构造,必须保证无参构造的存在,否则会出现异常
4. 反射机制的灵活性
通过反射机制的方式创建对象看起来有些复杂,但是这样会使程序更灵活
-
实例
classinfo.properties文件
className=com.study.www.threadshou.User
Test.java
public class Test { public static void main(String[] args) throws Exception { //通过IO流读取classinfo.properties文件 FileReader reader = new FileReader("./classinfo.properties"); //创建属性类对象Map,key value都是String类型 Properties pro = new Properties(); //加载 pro.load(reader); //关闭流 reader.close(); //通过key获取value String className = pro.getProperty("className"); //通过反射机制实例化对象 Class c = Class.forName(className); Object obj = c.newInstance(); System.out.println(obj); } } class User{ public User(){ System.out.println("调用了无参构造"); } }
输出结果:
以上代码比较灵活,代码不需要改动,可以修改配置文件,配置文件修改之后,可以创建不同的实例对象
例如:将配置文件修改成className=java.util.Date
则就创建的是Date的实例对象
5. Class.forName()
研究一下,当调用Class.forName()的时候发生了什么?
public class Test {
public static void main(String[] args) {
try {
Class.forName("com.study.www.threadshou.User");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User{
//静态代码块在类加载初始化时执行,并且只执行一次
static{
System.out.println("User类的静态代码块执行了");
}
}
输出结果:
结论:Class.forName()这个方法的执行会导致类加载初始化
在开发中,如果你只是希望一个类的静态代码块执行,其他代码一律不执行,就可以使用Class.forName(“完整类名"),这个方法的执行会导致类加载,类加载初始化时,静态代码块执行。(该方法会应用到JDBC技术中)
6. 文件路径问题
我们之前都是直接采用相对路径或者是绝对路径来获取文件,但是这种获取文件的方式移植性比较差,比如说需要将程序打包到Linux系统下运行,而Linux系统下不可能存在C盘、D盘之类的盘符,这样就会导致我们的路径找不到所需要的文件,所以这节我们讨论一下通过一种新的方式来获取文件的绝对路径,有更好的移植性
注:使用如下方式的前提是文件必须在类路径下(可以看成在src路径下,但不代表类路径就是src)
(1)方法一
public class Test {
public static void main(String[] args) throws Exception{
String path = Thread.currentThread().getContextClassLoader().getResource("classinfo.properties").getPath();
///E:/Java-Project/Java-Code/out/production/Java-Code/classinfo.properties
System.out.println(path);
FileReader reader = new FileReader(path);
Properties pro = new Properties();
pro.load(reader);
reader.close();
String className = pro.getProperty("className");
//com.study.www.threadshou.User
System.out.println(className);
}
}
- Thread.currentThread():当前线程对象
- getContextClassLoader()是线程对象的方法,可以获取当前线程的类加载器对象
- getResource()是类加载器对象的方法,当前线程的累加器默认从类的根路径下加载资源
如果想获取src子目录下的文件可以通过:
String path = Thread.currentThread().getContextClassLoader().getResource("com/study/www/.../classinfo.properties").getPath();
(2)方法二
方法一是直接返回的绝对路径,方法二可以直接返回一个流
public class Test {
public static void main(String[] args) throws IOException {
InputStream reader = Thread.currentThread().getContextClassLoader().getResourceAsStream("classinfo.properties");
Properties pro = new Properties();
pro.load(reader);
reader.close();
String className = pro.getProperty("className");
System.out.println(className);
}
}
(3)方法三
java.util包下提供了一个资源绑定器,便于获取属性配置文件中的内容,使用以下这种方式,配置属性文件xxx.properties必须放到类路径下,且必须绑定的是xxx.properties文件,并且在写路径的时候,路径后面的扩展名不能写
public class Test {
public static void main(String[] args) throws IOException {
ResourceBundle bundle = ResourceBundle.getBundle("classinfo");
//ResourceBundle bundle = ResourceBundle.getBundle("com/study/www/.../classinfo");
String className = bundle.getString("className");
System.out.println(className);
}
}
7. 类加载器
-
什么是类加载器?
专门负责加载类的命令/工具,ClassLoader
-
JDK中自带的3个类加载器
- 启动类加载器
- 扩展类加载器
- 应用类加载器
-
假设有这样一段代码
String s = "abc";
代码在开始执行之前,会将所需类全部加载到JVM当中。通过类加载器加载,看到以上代码类加载器会找String.class文件,找到就加载,那么是怎么进行加载的呢?
- 首先通过”启动加载器“加载,启动加载器专门加载…\jdk1.8\jre\lib\rt.jar,rt.jar是JDK最核心的类库
- 如果通过”启动加载器“加载不到的时候,会通过”扩展类加载器“加载,扩展类加载器专门加载…\jdk1.8\jre\lib\ext*.jar
- 如果”扩展类加载器“没有加载到,那么会通过”应用类加载器“加载,应用类加载器专门加载classpath中的类
这样的加载顺序,提供安全特性,比如说某个用户想通过自己的String类对系统进行攻击(属于应用类加载器的范畴),但是JVM会用启动类加载器中的String。
这种机制叫做双亲委派机制,优先从启动类加载器中加载,这个成为”父“。”父“无法加载到,再从扩展类加载器中加载,这个称为”母“。双亲委派,如果都加载不到,才会考虑从应用类加载器中加载,知道加载到为止。