一、认识反射
反射(reflection):视为动态语言的关键,反射机制允许程序在执行期间借助JDK中提供的Reflection API来取得任何类的内部信息,并能够直接操作任意对象的内部属性及方法,很多框架中都使用到了反射,例如Spring。
那么它是如何在运行期间通过反射获取对象及类中的属性呢?
JVM加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个class对象),这个对象包含了完整的类的结构信息,可以通过该类来获取或使用我们想要的属性及方法。
关于反射的API:
java.lang.Class:表示一个类(表示通用的类)。
java.lang.reflect.Method:代表类的方法。
java.lang.reflect.Field:代表类的成员变量。
java.lang.reflect.Constructor:代表类的构造器。
二、认识Class类
Class类
java.lang.Class类:
类的加载过程:程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾),接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内容中(类的加载)。加载到内容中的类,我们称为运行时类,即为Class实例,一个类只有一个class对象。
注意:获得Class实例不能使用new来获取,则需要使用相应的方法获取。
获取Class实例的四种方式
加载到内存中的运行时类,会缓存一定的时间,在这段时间中,我们可以通过不同的方式来获取此运行时类。
四种方式如下:
class Person { } public class Main { public static void main(String[] args) throws ClassNotFoundException { //方式一:调用运行时类的属性:类名.class Class<Person> class1 = Person.class; System.out.println(class1); //方式二:通过运行时类的对象调用getClass()获取 Class<? extends Person> class2 = new Person().getClass(); System.out.println(class2); //方式三:通过调用Class的静态方法:Class.forName(String classpath) Class<?> class3 = Class.forName("xyz.changlu.reflection.Person"); System.out.println(class3); //方式四:通过使用类的加载器代用loadClass()方法获取 ClassLoader loader = Main.class.getClassLoader(); Class<?> class4 = loader.loadClass("xyz.changlu.reflection.Person"); System.out.println(class4); //比较这四个实例是否相同 System.out.println(class1 == class2); System.out.println(class1 == class3); System.out.println(class1 == class4); } }
这四种方式获取到的Class实例都是指向同一个实例内存空间,也就是说都是同一份。
哪些类型可以是class对象?
①class:如外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类
②interface:接口
③[]:数组
④enum:枚举类
⑤annotation:注解@interface
⑥primitive type:基本数据类型,如int、String
⑦void
public class Main { public static void main(String[] args) throws ClassNotFoundException { Class<Object> class1 = Object.class;//类 Class<Comparable> class2 = Comparable.class;//接口 Class<String[]> class3 = String[].class;//一维数组(引用类型) Class<int[][]> class4 = int[][].class;//二维数组(基本数据类型) Class<ElementType> class5 = ElementType.class;//枚举类 Class<Override> class6 = Override.class;//注解 Class<Integer> class7 = int.class;//基本数据类型 Class<Void> class8 = void.class;//void返回类型,也是一个类 //元素类型与维度(指一维、二维)一样,就是同一个Class Class<? extends int[]> c9 = new int[10].getClass(); Class<? extends int[]> c10 = new int[100].getClass(); System.out.println(c9 == c10);//true } }
对于数组,只要其元素类型与维度相同那么它们的Class类实例都是同一个
三、反射的方法使用
获取构造器、类属性及方法
下面演示了构造器、类属性、类方法获取:
package xyz.changlu.reflection; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; class Person { private String name; private Integer age; public Person() { } public Person(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public void say(){ System.out.println("Person说话啦"); } @Override public String toString() { return "xyz.changlu.reflection.Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class Main { public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException { Class clazz = null; try { clazz = Class.forName("xyz.changlu.reflection.Person"); } catch (ClassNotFoundException e) { e.printStackTrace(); } //1、构造器 // 无参构造 Person person = (Person) clazz.newInstance(); System.out.println("反射创建无参构造:"+person); // 有参构造 Constructor constructor = clazz.getConstructor(String.class, Integer.class); Person person1 = (Person) constructor.newInstance("changlu", 18); System.out.println("反射创建无参构造:"+person1); // 获取所有的构造器 Constructor[] constructors = clazz.getConstructors(); System.out.println("反射获取所有的构造器:"+Arrays.toString(constructors)); //2、属性 // 获取所有的公共属性(也包含父类所有的public属性) Field[] fields = clazz.getFields(); System.out.println("反射获取所有公共属性:"+Arrays.toString(fields)); // 获取当前运行类中所有的属性(包含private,单不包含继承的) Field[] declaredFields = clazz.getDeclaredFields(); System.out.println("反射获取所有属性:"+Arrays.toString(declaredFields)); Field name = clazz.getDeclaredField("name"); name.setAccessible(true);//禁用访问安全检查的开关 name.set(person,"Liner"); System.out.println("反射修改person对象的name属性值:"+person.getName()); //3、方法 // 获取所有的公共方法 Method[] method = clazz.getMethods(); System.out.println("反射获取所有公共方法:"+Arrays.toString(method)); // 获取所有的方法(不包含继承的) Method[] declaredMethods = clazz.getDeclaredMethods(); System.out.println("反射获取所有方法:"+Arrays.toString(declaredMethods)); // 执行指定方法say() Method method1 = clazz.getMethod("say"); method1.invoke(person); } }
说明:Class的许多方法调用方法几乎都相同,无非就是获取指定的名称属性或是获取所有的属性(返回数组)
getFields与getDeclaredFields区别:第一个是获取对应类中的公共属性包含父类的,第二个是获取对应类中的所有属性不包含父类的。其他方法都相同。
第77行的setAccessible(true),例如Mehtod、Field类都有该方法,值为true则指示反射的对象在使用时应该取消Java语言访问检查,默认是开启也就是false。
为什么要设置为true呢?因为JDK的安全检查耗时较多,所以对于属性设置、方法调用时通常会设置为true来达到提升反射速度的目的。
如何看待反射与封装性两个技术?
问题1:通过直接new的方式或反射方式都可以调用公共的结构,开发中如何使用?
一般来说是直接使用new的方式(效率更高)。相对于反射的特征是其动态性能够动态的加载未知的外部配置对象,临时生成字节码进行加载使用,极大提高应用的扩展性。反射应用场景有Tomcat服务器、Spring的AOP与IOC 。
问题2:反射机制与面向对象中的封装是不是矛盾,如何看待两个技术?
封装性:通过设置权限符如private、public告知我们该私有的时候外部无法调用,公共的外部则允许调用,一些私有的方法内部会自行使用,体现了封装性。
反射:告诉我们可以公共私有都可以调,但是不建议调私有的方法。
简单点讲封装性解决的是建议你要不要调的问题,而反射则是能不能调的问题。
四、类的加载与ClassLoader理解
类的加载过程(含例子)
当程序主动使用某个类时,若类还未被加载到内存中,则系统会通过三个步骤对类进行初始化:类的加载(Load) => 类的链接(Link) => 类的初始化(Initialize)
加载:将类的class文件字节码内容加载到内存,将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即引用地址)。所有需要访问和使用类数据只能通过这个Class对象,该过程由类的加载器完成。
链接:将类的二进制数据合并到JVM的运行状态之中。
验证:确保加载的类信息符合JVM规范,保证加载类的正确性,包含四种验证。例如:以cafe开头,没有安全方面问题。
准备:正式为类变量(static)分配内存并设置类变量默认初始值即零值,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:JVM负责对类进行初始化。
执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
当初始化一个类时,若发现其父类还没有进行初始化,则需要先触发其分类的初始化。
虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
大致过程是这样的(重要):对于一个类的初始化是包含上面三个步骤的,分为加载、链接、初始化。我们在理清一下思路,首先使用javac工具将.java源代码转为class字节码,接着使用java命令开始执行程序,jvm会先进行加载(使用类加载器)及链接的操作,而对于初始化这个步骤分为主动使用与被动使用。
JVM虚拟机规范规定了,每个类或者接口被java程序首次主动使用时才会对其进行初始化,不排除JVM在运行期间提前预判并初始化某个类。
主动引用类方式:即初始化类
启动类(重要):也就是执行main函数所在的类会导致该类的初始化
使用new导致类的初始化(也会导致父类的初始化)
访问类的静态变量(若是静态变量是其父类独有的,那么只会初始化父类;除了final常量)、静态方法
对某个类进行反射操作(如Class.forName())
被动引用类:不会发生类的初始化
当通过子类引用父类的静态变量,不会导致子类初始化。
通过数组定义类引用,不会触发初始化类。
引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常 量池中了)。
见例子说明加载过程(引用一个博文例子)
情况一:②在①上面时
public class Main { private static Main instance = new Main(); // ② // ① private static int x = 0; private static int y; private Main(){ x++; y++; } public static Main getInstance(){ return instance; } public static void main(String[] args) { Main singleton = Main.getInstance(); System.out.println(singleton.x); System.out.println(singleton.y); } }
//详细过程 ①加载:执行程序,将class文件加载到内存中,之后生成Class类,这个过程由类加载器完成 ②链接:Main中的静态变量初始值赋值放置到方法区 instance=null x=0 y=0 ③初始化: <clint> new Main() => 相当于执行x++,y++ 此时x=1 y=1 instance=引用地址 x=0 => 此时x=0 y=1 <clint> //对于单独的int y;是没有赋值操作的所以还是1,最后输出结果则为0 1
情况二:①在②上面时
public class Main { // ① private static int x = 0; private static int y; private static Main instance = new Main(); // ② private Main(){ x++; y++; } public static Main getInstance(){ return instance; } public static void main(String[] args) { Main singleton = Main.getInstance(); System.out.println(singleton.x); System.out.println(singleton.y); } }
//详细过程 ①加载:执行程序,将class文件加载到内存中,之后生成Class类,这个过程由类加载器完成 ②链接:Main中的静态变量初始值赋值放置到方法区 x=0 y=0 instance=null ③初始化: <clint> x=0 => 此时x=0 y=0 new Main() => 相当于执行x++,y++ 此时x=1 y=1 instance=引用地址 <clint> //此时的new Main()是后执行的,所以会先执行x=0,之后执行构造器中内容,输出结果则为1 1
上面仅仅是我根据一些博客大致推出来的过程(如有问题,请求指出)
ClassLoader理解(各个类加载器)
认识各个类加载器ClassLoader
ClassLoader(类加载器)作用:用来把类class装载进内存。
类加载的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表该类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
JVM规范定义了如下类型的类加载器:
引导类加载器:用c++编写的,嵌在JVM内核中的加载器,主要负载加载JDK中$JAVA_HOME/jre/lib下的类库(包含核心类库),该加载器无法直接获取。(不是ClassLoader子类)
扩展类加载器:用Java编写的,其父类加载器是Bootstrap,负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。
系统类加载器:也称为应用程序类加载器,父加载器是Extension,负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性或者CLASSPATH换将变量所指定的JAR包和类路径,是最常用的类加载器。
获取系统类加载器的方法:ClassLoader.getSystemClassLoader()
查看引导类加载器所加载的核心类库:
public class Main { public static void main(String[] args) { URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); for (URL urL : urLs) { System.out.println(urL.toExternalForm()); } } }
我们可以看到其中也包含了rt.jar类库,这里我们就可以知道为什么使用核心类库时不需要导包了吧。
获取各个类加载器:
public class Main { public static void main(String[] args) { //获取三个类加载器 //①对于自定义类,其加载器是系统类加载器 System.out.println(Main.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2 //②系统类加载器的父类加载器是扩展类加载器 System.out.println(Main.class.getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@677327b6 //③扩展类加载器的父类是引导类加载器 System.out.println(Main.class.getClassLoader().getParent().getParent());//null //测试JDK核心类的类加载器:引导类加载器 System.out.println(String.class.getClassLoader()); } }
第9行:获取ExtClassLoader的父类加载器结果为null,并不是说没有该加载器,是因为Bootstrap Classloader加载器是C++写的。
双亲委派机制
当一个类加载器收到一个类加载请求时,例如调用ClassLoader.loadClass("")方法,该方法会通过委派模型去加载类。
机制说明:当第一个类进行加载请求时,首先会在AppClassLoader中检查是否加载过该类,如果有则无需加载直接返回,如果没有那么会将这个请求委托给父类的加载器去执行,在父类加载器中同时也会检查是否加载过该类,若是没有则继续向上。直到Bootstrap Classloader之前都是会先去检查是否加载类。到BootStrap classloader会考虑自己是否能加载,若不能加载会依次让子加载器去依次去尝试加载,若是没有任何类加载器能够加载,最后会抛出ClassNotFoundException。
首先看一下Launcher类:该类会在ClassLoader类中进行初始化
这里的BootClassPathHolder并不是指Bootstrap ClassLoader,该类可以使用其中方法获取到引导加载器类加载的类库。
除了启动类加载器Bootstrap ClassLoader,其他的类加载器都是ClassLoader的子类。
我们看ClassLoader的源码:
public abstract class ClassLoader { public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { //检查是否已经加载过该类了 Class<?> c = findLoadedClass(name); //若是没有加载过,则会调用父加载器 if (c == null) { long t0 = System.nanoTime(); try { //若是父加载器不为空(说明此时有父类加载器) if (parent != null) { //会调用父类加载器的loadClass()方法,也就是本方法 c = parent.loadClass(name, false); } else { //父加载器为空,表示该类加载器的父类加载器就是Bootstrapclassloader了 //会直接使用引导类加载器Bootstrapclassloader去进行加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {//若是一直到最底层的子类加载器都加载不到 } //父类加载器若是没有找到指定类 if (c == null) { long t1 = System.nanoTime(); //使用自己的findClass()方法去加载类 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } }
实现双亲委托机制(准确说是父委托机制)就是使用的该方法,其中进行了递归的操作。
为什么要使用双亲委派机制呢?
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系能够避免类的重复加载。例如通过网络传输来的java.lang.String类进行类初始化时,在这种机制下这些系统级的类已经被BootStrapclassloader加载过了,所以其他类加载器就没有机会再去加载,从一定程度上防止了危险代码的植入。
保证了系统级别的类的安全性,使一些基础类不会受到开发人员“定制化”的破坏。
加载properties文件
import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class Main { public static void main(String[] args) throws ClassNotFoundException, IOException { Properties properties = new Properties(); //通过系统类加载器的getResourceAsStream()获取输入流 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); properties.load(is); //获取键值对 System.out.println(properties.getProperty("username") + ":" + properties.getProperty("password")); } }
注意这里的jdbc.properties需要放置到src目录下,否则会报空指针。