类的加载、RTTI以及动态代理
类的加载:
双亲委派模型:
- 双亲委派:除非父加载器找不到相应的类,否则尽量将这个任务代理给当前类加载器的父加载器去做,用双亲委派模型的目的是为了避免重复加载类;
- 类加载器分类:
启动类加载器(Bootstrap Class Loader):加载jre/lib下的jar文件,即便开启了Security Manager的时候,JDK仍赋予了它加载程序的AllPermission;
-
扩展类加载器(Extension Class Loader):负责加载jre/lib/ext/下面的jar包,该目录可以通过设置java.ext.dirs来覆盖
java -Djava.ext.dirs=your_ext_dir HelloWorld
-
应用类加载器(Application Class Loader):加载我们最熟悉的classpath的内容,也有人叫系统类加载器;可以通过下面的参数修改:
java -Djava.system.class.loader.=com.yourcorp.YourClassLoader HelloWorld
-
图示例:
类加载器图.png类加载器示意图
-
注意情况:
- 不是所有情况都遵循双亲委派模型,有时候系统类加载器需要加载用户代码,如JDBC,这时候不会使用双亲委派模型去加载,而是使用上下文加载器;
- 可见性:子类加载器可以访问父类加载器加载的,但反过来是不允许的;
- 单一性:由于父加载器加载的类对子类是可见的,所以父加载器加载过的类子加载器是不会再加载的,但是注意,同级的加载器,同一类型仍然可能被加载多次,因为互相并不可见;
- 类加载器优先级:(待完善)
类加载过程分为加载、链接及初始化:
加载:由类加载器执行,查找字节码,并创建一个Class对象;
-
链接:
- 验证:校验字节信息是符合JVM虚拟机规范的,验证阶段有可能会触发更多的class加载(JVM基础中同样提到类的加载,并提到触发类的加载是在解析阶段,对符号引用的解析会触发更多的类加载);
- 准备:为静态域分配空间,并清空内存,值为0、false或者null之类;
- 解析:解析这个类创建的对其他类的所有符号引用;
-
初始化:执行静态初始化器和静态初始化块;初始化被延迟到对静态方法(构造器隐式的是静态的)或者非常数静态域进行首次引用时执行,注意访问以下字段会触发初始化,因为需要执行静态初始化器后才有具体的值:
public static final int number = new Random(xx).nextInt(100);
访问以下字段则不会触发初始化:
public static final int count = 20;
RTTI:
Class引用的一些基础知识:
-
Class引用可以引用某个Class对象,如果不指定泛型参数,那么该引用可指向任意的Class对象,如下代码是可以通过编译的:
Class ref = int.class; ref = double.class;
Class<?>与Class是等价的,但通常在不限制引用的Class对象类型时候推荐使用Class<?>,好处在于它表示我们并非是碰巧或者是疏忽,而是我们就是要选择非具体的版本;
-
泛型参数可以限定Class引用所引用的对象,让Class引用的类型更加具体,添加泛型语法的原因是为了提供编译器类型检查;如:
引用单一类型:Class<Integer> integerRef = int.class;
引用某一类型的子类:
Class<? extends Number> numberRef = int.class;
-
不能将Class<Integer>引用赋值给Class<Number>的引用,虽然Integer和Number之间存在继承关系,但Class<Integer>和Class<Number>之间并不存在继承关系,他们都是Class引用,泛型参数的作用是限定引用范围,需要仔细体会他们的区别,如下代码是错误实例:
Class<Number> numberRef = int.class;
-
Class引用可通过newInstance方法来创建一个对象,需要该类有无参构造函数,且当前代码位置可访问该构造函数,否则没有访问权限时将抛出IllegalAccessException,如果该Class不能被实例化将抛出(Class是接口、抽象类、原始数据类型或者类本身没有无参构造函数等情况)InstantiationException,他们均是ReflectiveOperationException的子类;如果Class引用指定了泛型参数,则newInstance的返回值会自动转换为泛型参数的类型;
带泛型参数的Class引用创建对象:Integer i = int.class.newInstance();
如果泛型参数指定上界则返回值转换为上界:
Class<? extends Number> ref = int.class;
Number number = ref.newInstance();
如果泛型参数指定的是下界,则返回值只能是Object,因为唯一确定的是返回值一定是Object或其子类;
Class<? super String> ref = String.class;
Object o = ref.newInstance();
获取Class对象引用的方式有三种:
-
通过Class类的public static Class<?> forName(String className)方法获取Class对象的引用,该方法需要传入一个String类型的参数,该参数指明需要获取的类对象的名字,注意className需要包含包名,返回Class对象(是Class对象,不是平时new出来的对象),如果找不到要加载的类则会抛出ClassNotFoundException;注意,返回值是Class<?>,泛型参数是?,代表任意类型,这意味着,即使我们知道forName加载的String类型,也不能直接用Class<String>类型来接收返回值;以下代码无法通过编译:
Class<String> stringClassRef = Class.forName("java.lang.String");
正确代码应该为:
Class<?> ref = Class.forName("java.lang.String");
或者进行强转:
Class<String> stringClassRef = (Class<String>)Class.forName("java.lang.String");
Class.forName会触发类的加载、链接及初始化
-
通过感兴趣的对象引用.getClass()函数来获取Class引用(这是Object的一个成员方法),如(注意泛型参数写法,因为String类型的引用其对象不一定就是String还有可能是String的子类对象):
String name = "abc"; Class<? extends String> ref = name.getClass();
-
通过“类名.class”来获取类对象引用,此时Class引用的泛型参数可填写对应的参数;值得注意的是,该访问方式并不会自动初始化该Class对象(static代码块不会被执行);该方法相比于第一种方法将会得到编译期检查的好处,但同时也变成了硬编码;
Class<String> ref = String.Class
包装类型有一个标准字段TYPE,指向对应的基本数据类型的class对象,即:
//true
int.class == Integer.TYPE;
注意,即使是基本类型的Class引用,泛型参数也应该填写包装类,因为泛型参数不能是基本类型,即:
Class<Integer> intRef = int.class;
//特别要注意的是int.class != Integer.class,以下代码会输出false;
System.out.println(int.class == Integer.class);
instanceof 与 isInstance
-
介绍:
-
instanceof:instanceof是关键字,使用方法如下:
Integer i = 1; if(i instanceof Integer){ //do someting }
instanceof关键字后面跟随的类名,是对象的类、父类及实现的接口时为true,不能跟Class对象,这意味着如果有多个判断则需要依次编写千篇一律的代码,isInstance可以很好的解决这个问题,但 通常如果程序中编写了许多instanceof或者isInstance,就说明我们的设计可能存在瑕疵,因为程序太过依赖具体的细节而失去泛化能力;
-
isInstance:isInstance是类Class的一个成员函数:
boolean isInstance(Object o);
使用方法如下:
Class<Integer> ref = Integer.class; if(ref.isInstance(Integer.valueof(1)){ //do someting }
-
-
与== 和equals的区别:
instanceof 和 isInstance的作用完全一致,他会判断是否是该类(接口)或者是否是其子类,但==或者equals则不会判断是否是其子类的情况,如下所示:
//true Number.class.isInstance(Integer.valueof(1)); //false Integer i = 1; i.getClass().equals(Number.class); //无法通过编译,提示类型不可比较 Integer i = 1; Number.class == i.getClass();
反射:运行时类信息
Class类与java.lang.reflect类库一起对反射的概念进行了支持;
-
常用反射相关类的介绍:
-
Field:提供类或者接口的单个字段的信息,以及对他的动态访问权限,是final class;
java.lang.Object java.lang.reflect.AccessibleObject java.lang.reflect.Field
Method:提供类或接口的单独某个方法的信息;同样继承自AccessibleObject,是final class
Constructor:提供类的单个构造方法的信息,继承自AccessibleObject,是final class;
-
-
常用反射相关类的获取,以Field为例,其他都是类似的:
通过Class对象获取Field获取方法有四个: //获取该类声明的公有属性,包括继承而来的属性(父类、接口)及静态属性,如果父类、子类存在同名属性,那么他们都会被加入到数组中,返回的数组中的元素没有排序,也没有任何特定的顺序; Field[] getFields(); //在该类声明的共有属性中查找名为name的属性,如果找不到则抛出NoSuchFieldException,如果父类和子类存在同名Field,则返回子类的Field; //查找顺序为:1.本类中查找。2.按声明顺序查找接口。3.查找超类; Field getFiled(String name); //获取该类的所有的属性(包括protected、private),不包括继承而来的属性 Field[] getDeclaredFields(); //从所有属性中搜索名为name的属性,找不到则抛出NoSuchFieldException Field getDeclaredField(String name); //如果类型为基本类型、void或者数组类型,则返回长度为0的数组,下面的代码将返回一个空的Field数组; int.class.getFields()
-
常用反射相关类的使用:
-
Field主要方法为set和get,并针对基本类型提供了多个版本,下面仅列出引用类型版本:
//o指定需要访问的对象; Object get(Object o); void set(Object o,Object value) //背景class声明 public class ReflectBase { public static String name = "Base"; private static String description = "base option"; public int n = 2; private int m = 3; } public class Reflect extends ReflectBase implements IReflect2,IReflect{ public static final int number = 1; private static String name = "reflect"; protected int a = 4; public int count = 2; private int sum = 3; } //Field访问共有属性; Reflect r = new Reflect(); Field field = Reflect.class.getField("count"); //输出2 System.out.println(field.get(r)); field.setInt(r,10); //输出10 System.out.println(field.get(r)); //Filed访问没有权限的字段: ReflectBase base = new ReflectBase(); Field f = ReflectBase.class.getDeclaredField("m"); //无法访问该字段,设置访问权限 f.setAccessible(true); //输出3 System.out.println("before modify sum : " + f.get(base)); f.setInt(base,100); //输出100; System.out.println("after modify sum : " + f.get(base));
-
method最常用的方法:
Object invoke(Object obj, Object... args)
方法说明:obj指定调用方法的实例,如果是静态方法,可以传null,args为参数,如果没有可以不传或者传null,如果参数是基本类型,则会触发拆箱操作;如果方法返回值为void,则返回null,如果返回值为基本类型,则会触发拆箱,如果是基本类型数组,则数组元素不会被包装在对象中;
-
Constructor
T newInstance(Object... initargs)
T为泛型参数,如果不需要形参,则可以传null或者不传,如果构造方法的声明类是非静态上下文的内部类,则构造方法的第一个参数需要是封闭实例,如果底层构造方法的类是抽象类则会抛出InstantiationException
-
反射通常可以使用AccessableObject类的SetAccessable方法来修改权限,让本来是私有的成员变量或方法变得可以访问,JDK8中并没有很好的方法去阻止这件事情的发生,JDK9以后进行了改进;但应该注意,对于final域的修改,系统会在不抛出异常的情况下接受任何修改尝试,并且不会发生任何修改;
JDK动态代理介绍、使用方法及在空对象方面的使用案例
简介:
代理是基本的设计模式之一,动态代理能够动态地创建代理并动态的处理对代理方法的调用,可以很方便的为已有的类插入额外逻辑而不必修改原有的代码;JDK动态代理使用反射机制来完成;-
基本使用方法:
//示例接口 public interface ICanTalk { public void talk(String content); } //被代理的类 public class People implements ICanTalk { @Override public void talk(String content) { System.out.println("people speak : " + content); } } //实现InvocationHandler的类声明 class PeopleProxy implements InvocationHandler{ Object proxied; Object proxy; PeopleProxy(Object proxied){ this.proxied = proxied; } /** * invoke函数为插入额外逻辑入口 * proxy为动态生成的代理类实例 * */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //额外逻辑一: System.out.println("**** proxy : " +proxy.getClass() + ", method : " + method + ",args: " + Arrays.toString(args)); //真正的逻辑调用 //如果将ivoke函数的proxied换成proxy将会无限递归调用自己,但在JDK8下此处不会造成栈溢出 //而是持续一段时间后程序退出(Process finished with exit code 1): Object result = method.invoke(proxied,args); //将其赋值给成员变量proxy,用来验证它就是动态生成的代理类实例; this.proxy = proxy; //额外逻辑二 System.out.println("**** proxy end"); return result; } } //测试main函数: public static void main(String[] args) { People p = new People(); PeopleProxy proxy = new PeopleProxy(p); ICanTalk canTalk = (ICanTalk) Proxy.newProxyInstance(People.class.getClassLoader(),People.class.getInterfaces(),proxy); p.talk("我困了"); canTalk.talk("我饿了"); //true System.out.println(canTalk == proxy.proxy); }
-
动态代理在空对象方面的使用:
-
空对象的定义:
简单理解为:该对象没有任何有意义的信息,但不将引用设置为null;书面解释:一个对象可以接受传递给它所代表的对象的消息,但是返回表示为:实际上并不存在任何真实对象的值;
-
空对象的意义:
如果将引用设置null,那么在每次使用引用时都需要测试其是否为null,并且null在除了在尝试操作它时抛出NullPointerException以外没有任何有效行为,那么我们可以创建一个空对象,能接受发给他的信息,但啥也不干,这样在原本需要将引用赋值为null时,将该空对象赋值给引用,这样客户端就不需要在去检查null,可以假设所有对象都是有效的,直接操作,这样也能简化代码,当然,我们也可能在实际使用中依然要去检测是否为空对象,这个问题使用mark interface即可解决;
-
动态代理实现空对象优势(仔细思考后,觉得动态代理实现空对象并没有什么优势):
-
手动实现统一的空对象:
我们可以手动实现空对象,如果针对接口实现一个统一的空对象,这样则没有更多的子类信息;如:有一个“动物”接口,有“猪”“猫”“狗”实现类,我们可以统一编写一个动物空对象,实际情况可能够用了,这里我们想讨论另一种情况,如果我们想区分这是一个空的猪对象或者空的猫对象,那么这种方法就无法实现了(此处有误,仔细思考可以在不同实现类中实例化不同的动物空对象,可以实现区分);
-
手动为每种动物实现类编写一个空对象类继承自该类
这样显得很枯燥,为什么要重新编写空对象类,不将对象的属性置为0或者null来实现空与非空的区别呢,如果该对象存在的意义就是属性且正常属性不会出现0
或者null的的话(否则无法分辨是否是空对象),我们可以这么做;但如果空对象的行为也需要改变,那么这么做可能达不到我们想要的效果; -
使用动态代理实现空对象(仔细思考,并无优势)
使用动态代理,invoke方法中不实际调用即可;每种实现类中实例化不同的代理实例,仔细思考,这相对于第一种方法并没有太大的优势,好处仅仅在于不需要编写那么的空函数,在IDE能自动生成空的接口方法的情况下,貌似已经没什么优势了,反而反射调用会带来性能开销;代码已实现,但发现并无意义,就不上代码了;
-
-
动态代理的JDK实现和cglib两种实现对比
-
JDK的优势:
- 最小化依赖关系
- 平滑的JDK版本升级
- 代码实现简单
-
cglib的优势:
- 有时候调用目标可能不便于实现额外接口,限定调用者实现接口是有些侵入性的实践,cglib没有这个限制;
- 只操作我们关系的类,不必为其他类增加工作量;
- 高性能;
因为cglib是创建被代理对象子类的关系,所以几乎没有能力退化,但也带来了一些问题,如被代理对象的final方法无法使用;