引序:开始反射概念学习之前,先来段引序。
请牢记:计算机的三大程序结构;顺序执行结构、条件分支结构、循环处理结构。
有时我们写的程序会自我感觉这里写了代码,它是怎么被调度执行的呢,按照程序的顺序执行来考究,有点说不通。我举几个例子说明看起来说不通的现象:
- 说不通1:注解
import java.lang.reflect.*;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnno{ //注解定义
String name() default "12345";
int value();
}
@MyAnno(name="haha",value=123) //注解钉点
class B {
public static void main(String[] args){
if( B.class.isAnnotationPresent(MyAnno.class) ){
Annotation annos[] = B.class.getAnnotations(); //消费注解
for(Annotation anno : annos){
System.out.println(anno);
System.out.println( ((MyAnno)anno).name() );
System.out.println( ((MyAnno)anno).value() );
System.out.println( anno.annotationType().getName() );
}}}}
上面注解的小示例:如果看《注解定义》《注解钉点》 这两个地方,能搞懂我们写这两处的意思嘛?完全是懵圈的,这是什么代码,又什么时候被调度执行,咋执行的呢?正是因为有 《注解消费》 这个梗,我们才恍然大悟,注解是啥时用起来的。(这也是很多框架利用大量注解的底层技术手段,第三方框架有其自己的《注解定义》以及《注解消费》底层,开放给我们开发时用的就是“注解钉点”,我们利用好《注解钉点》这一点就OK)
- 说不通2:匿名内部类
匿名内部类:作为方法的入参来使用,例如:
1:事件监听处理器类:处理事件时作为监听处理器方法的形参的Handler类
event.addEventListener( new Handler() { //这里的Handler就是匿名内部类
public void xxxMethed(Event event){ //这是匿名类中的方法xxxMethod();
}
}); //这个xxxMtd()是由事件调度线程来执行的。
以上代码我们只是调用了addEventListener(Handler hd); 传进来一个hd对象而已,那么Handler类中的xxxMethod()方法怎么会被调用的呢?答:是被事件调度线程来执行的。事件调度线程是JVM管理的,不受我们的代码控制。
2:多线程编程时的任务类:往线程池当中提交一个实现了Runnable接口的类
threadPoolExecutor.execute( new Runnable(){ //这里的Runnable就是匿名内部类
public void run(){ //这个匿名类中有一个方法run();
}
}); //这个run()是由线程池调度工作线程来执行的。
以上代码只是调用了execute(Runnable task); 传进来一个task任务而已,那么Runnable类中的run()方法怎么会被调用的呢?答:是被线程池调度工作线程来执行的。线程池是一个框架,它怎么调度和管理工作线程,不受我们的代码控制。我们“写好匿名内部类中的方法业务逻辑”这一点就OK。理解匿名内部类很重要,它是静态代理模式的一种体现。之所以匿名内部类很重要,是因为它是理解lambda函数式编程思想的基础。
- 说不通3:动态代理
import java.lang.reflect.*;
//接口
interface InterfaceHello {
void sayHello();
void read();
}
//业务类:实现接口
class HelloImpl implements InterfaceHello {
public void sayHello() {
System.out.println("hello world");
}
public void read() {
System.out.println("I am reading...");
}
}
//我是代理:业务类真正业务逻辑的前后,我搞点事(比如:打个广告)
class ProxyHello implements InvocationHandler{
private Object target;
public ProxyHello(Object target) {
this.target=target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("我是代理,看视频之前我打个广告,haha!");
Object obj = method.invoke(target,args); // 真正的业务逻辑
System.out.println("我是代理,视频看完给评价下呗,多谢!\r\n");
return obj;
}
}
//测试
public class HelloTest {
public static void main(String[]args) {
try{
InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(
HelloTest.class.getClassLoader(),
new Class<?>[]{InterfaceHello.class},
new ProxyHello(new HelloImpl())
);
h.sayHello(); //调用InterfaceHello接口中的sayHello()方法
h.read(); //调用InterfaceHello接口中的read()方法
}
catch(Exception e){}
}
}
程序输出:
我是代理,看视频之前我打个广告,haha!
hello world
我是代理,视频看完给评价下呗,多谢!
我是代理,看视频之前我打个广告,haha!
I am reading...
我是代理,视频看完给评价下呗,多谢!
如果单看以上的测试类:h.sayHello(); 这句代码,是蒙圈的,因为按照我们以往的程序经验,这不就是实现了InterfaceHello接口的HelloImpl干的事嘛,但是程序的输出却不是这样,前有广告,中间是正事,后有评论。为什么这样呢?是因为:h是Proxy.newProxyInstance()出来的,下面简要分析下:
Proxy.newProxyInstance(loader,intfs[],handler){
Class<?> cl = getProxyClass0(loader, intfs); //内存中生成一个类$Proxy0,实现intfs所有接口
final Constructor<?> cons = cl.getConstructor(constructorParams); //类$Proxy0的构造函数
return cons.newInstance(new Object[]{handler}); //引入handler,new $Proxy0对象返回
}
那这个内存中动态生成的类$Proxy0到底长什么样子呢?我写个简易版的样子:
public class $Proxy0{
private InvocationHandler handler;
public $Proxy0(InvocationHandler handler){
this.handler = handler;
}
public void sayHello(){ //这就是动态代理,【魔术揭秘】的地方
handler.invoke();
}
public void read(){
handler.invoke();
}
}
看:InterfaceHello h = (InterfaceHello)Proxy.newProxyInstance(loader,intfs[],handler);
我们恍然大悟,我们拿到的是$Proxy0对象,这个对象实现了所有的接口,所以向下类型强转后,我们拿到的变量h可以调用接口中的任意方法。我们代码随后编写的 h.sayHello(); 这下彻底明白,它的内部逻辑是调用handler.invoke();//看上方的【魔术揭秘】。所以我的总结是:程序的顺序执行结构永远是这么个套路,大家别以为这不是废话嘛,程序不顺序执行难道还来回胡乱的乱跑执行嘛?不不不,要深刻体会,有助于碰到新技术时的理解的。在用很多第三方框架的时候,我们写好“业务实现逻辑”就OK,代理都是框架帮我们做好的,比如我们的方法业务逻辑里面并没有事务管理,但CRUD增删查改时都是能反应到数据库并提交/回滚的。
总结:以上三个小示例不过是解释了随着编程技术的更多手段,让我们对程序总是顺序执行结构在感官上的认知有所偏差。其实呢?说到底,程序的执行永远是从上至下的顺序执行结构,只不过有些代码比较显而易见,有些代码并不显而易见是由于框架/JVM等有一些我们看不见的底层机制,来调度顺序执行我们的代码。所以很多时候我们是了解底层原理即可,(依托底层机制抛出来的)面向应用高层面的一些API来编写好代码即可,必定我们不是java公司要制作jvm或者api,我们只是利用api写应用程序。
反射:
官方定义,反射是程序在运行时分析代码的能力(更贴切的说,就是知晓类结构信息的能力)。结合上述三个小示例的演示和说明,反射不过就是一种编程手段而已,就这么简单。我给反射下的定义是:“知晓类结构”
- 强调:
类对象 Class<?> clazz = myObj.getClass(); 这个clazz就是类对象。每一个类文件被JVM加载后,JVM就自动的创建好了一个类对象clazz,这个类对象包装了这个类的结构信息,一个类大致的结构:就是它实现了什么接口,它继承了哪个超类,它的构造函数,它的数据成员,它的方法成员,方法成员的入参有几个都是什么类型,大致一个类的结构就这么点玩意。正是由于JVM自动创建出来一个类对象,所以我们才能写出来clazz.getFields(); //知道类的数据成员有哪些,clazz.getMethods(); //知道类的方法成员有哪些,等等API。如果这种技术手段给一个更贴切的汉字含义,其实应该叫“知晓类结构”,不过这个名字太土了点,java就给它起了个高大上的名字:反射。
- 又强调:
知道了类对象的存在,那么现在大家都应该理解所谓的静态成员为什么都是 类名.静态成员名 的访问手法了,这看起来像什么呢?不就是 对象.方法名 的写法嘛,这里的类名其实类对象的意思。
- 再三强调:
请看下方示例代码,为什么静态方法上的synchronized和非静态方法上的synchronized 并不会互相干扰呢?因为它们的内置锁不同。静态方法的内置锁是类对象,而非静态方法(/也叫对象方法)的内置锁是具体的new出来的对象,它们不是同步一把锁,所以静态方法和对象方法并不会互斥。
class A{
//我是静态方法,我的内置锁是A类对象
public synchronized static void a1(){
/**我在执行
* 此时想调用a2(), a2()被互斥
* 此时想调用b1()或者b2()是可以的
*/
}
//我是静态方法,我的内置锁是A类对象
public synchronized static void a2(){}
//我是对象方法,我的内置锁是当前调用我的对象
public synchronized void b1(){
/**我在执行
* 此时想调用b2(), b2()被互斥
* 此时想调用a1()或者a2()是可以的
*/
}
//我是对象方法,我的内置锁是当前调用我的对象
public synchronized void b2(){}
}
以上是对Class类对象的一个浅显的总结,下面我们接着回到话题:反射。
类对象:知晓类结构
Class<?> clazz = MyClass.class; //得到类对象clazz 第1种手段 Class<?> clazz = myObj.getClass(); //得到类对象clazz 第2种手段 Class<?> clazz = Class.forName(pkg.MyClass); //得到类对象clazz 第3种手段 | ||||
方法 | 方法返回值 | 说明(/知晓类结构) | ||
类 结 构 | 实现接口 | getInterfaces() | Class<?>[ ] | 类实现的所有接口 |
继承父类 | getSuperclass() | Class<? extends T> | 类所继承的父类 | |
构造函数 | getConstructors() | Constructor<?>[ ] | 类所有的构造函数 | |
getConstructor(Class<?>...paramTypes) | Constructor<T> | 类的某个构造函数 | ||
数据成员 | getFields() | Field[ ] | 类所有的数据成员 | |
getField(String name) | Field | 类的某个数据成员 | ||
方法成员 | getMethods() | Method[ ] | 类所有的方法成员 | |
getMethod(String name,Class<?>...paramTypes) | Method | 类的某个方法成员 | ||
注解成员 | 最下方有个注解的单独表格。 Class类、Field字段、Method方法、Constructor构造函数、都支持注解的。 |
类自身的,不包括继承 | 方法 | 方法返回值 | |
类 结 构 | 内部成员 所属的声明类 | getDeclaringClass() | Class<?> |
构造函数 | getDeclaredConstructors() | Constructor<?>[ ] | |
getDeclaredConstructor(Class<?>...paramTypes) | Constructor<T> | ||
数据成员 | getDeclaredFields() | Field[ ] | |
getDeclaredField(String name) | Field | ||
方法成员 | getDeclaredMethods() | Method[ ] | |
getDeclaredMethod(String name,Class<?>...paramTypes) | Method |
通过以上对Class类的部分方法(这里集中出了在编写反射代码时常用的一些API)的总结,可以知晓,通过clazz类对象调用这些API我们可以做到知晓类结构的信息。比如:知道类中有哪些属性,哪些方法等。
类结构之:内部成员
类对象 Class<?> clazz = myObj.getClass(); 这个clazz就是类对象,通过调用类对象的API,我们可以获知类的结构,其内部成员我主要从以下三个着手讲解:构造函数、数据成员、方法成员。如图:

内部成员--构造函数
Constructor<T> cons = clazz.getConstructor(Class<?>...paramTypes);
T obj = cons.newInstance(initargs[]); //平时主要用构造函数的这句代码来生成一个对象
内部成员--数据成员
Field f = clazz.getDeclaredField(String fieldName);
f.setAccessible(true); //意指能访问f字段的值(默认反射只能访问public修饰的)
f.getAnnotationByType(MyAnno.class); //字段除了值以外能携带额外信息,注解
内部成员--方法成员
Method m = clazz.getMethod(String mtdName, Class<?>...paramTypes); //参数的类型
m.invoke(Object target, Object...paramArgs); //实际的参数
静态代理:
静态代理要求:接口,实现该接口的两货色:真实角色,代理角色;代理角色要持有真实角色的引用。静态代理的本质是运用java的多态性的体现模式一,方法重写。java多态性,请参考我的博客 Java基础 -- 02类 /接口 /继承
静态代理模式:匿名内部类就是一种静态代理模式的体现。lambda表达式又是匿名内部类的一种“语法糖”的简易版本,所以lambda表达式也是一种静态代理模式的体现。
静态代理这个简单,一个小示例:
//接口
interface Hello {
void say();
}
// 真实角色
class HelloImpl implements Hello {
public void say() {
System.out.println("..我是业务实现方!");
}
}
// 代理角色
class ProxyH implements Hello {
private Hello hImpl; // 持有真实业务方的引用
public ProxyH(){}
public ProxyH(Hello hImpl){ this.hImpl = hImpl; }
public void say() {
System.out.println("代理干的事:业务开始前,来段广告~~");
hImpl.say(); //真实业务执行
System.out.println("代理干的事:业务结束后,下评论呗^^");
}
}
//测试
public class StaticProxyTest {
public static void main(String[] args) {
Hello hImpl = new HelloImpl(); //创建真实业务
Hello proxyH = new ProxyH(hImpl); //创建静态代理
proxyH.say(); //代理执行
}
}
静态代理小示例执行结果:
代理干的事:业务开始前,来段广告~~
..我是业务实现方!
代理干的事:业务结束后,下评论呗^^
动态代理:
在本文刚开始的引序里面的3,动态代理,已经说的很详细。这里只是总结下:
动态代理--核心技术:
运行时JVM动态生成内存中的代理类 $Proxy0
public class $Proxy0{
private InvocationHandler handler;
public $Proxy0(InvocationHandler handler){
this.handler = handler;
}
public void sayHello(){ //这就是动态代理,【魔术揭秘】的地方
handler.invoke();
}
}
动态代理--解决问题:
在不影响原有代码结构的情况下(有的时候也影响不了,比如:别人封装好的jar包,无法反编译的,我们就无法窥探class文件所对应的源码java文件到底长什么样,自然无法对其进行修改扩容。当然此话不绝对,比如Hibernate架构就是通过CGLIB技术分析class文件的字节码来动态的在内存中生成代理类的,只是这种手段一般程序员无法掌握而已,太复杂),我们又想在原有代码做业务行为的前后横加干涉一下,比如穿插广告,事后提醒评论,记录日志等,那这时用动态代理就很合适。
动态代理--使用场景:
日志系统,Spring的AOP技术,声明式事务管理,等等。Hibernate用CGLIB技术实现动态代理,Spring用Proxy技术实现动态代理。
反射配套注解,更强悍:
举一个简单的说明大家即可明白,我们通过反射能知道类的数据成员有 id,name,age等等,但是通过反射Field的若干方法,我们能知道哪个是主键嘛?这玩意单单靠反射Field的API达不到要求。我们回想一下,注解,干嘛的呢?注解就是个标签(标签可以携带额外信息),注解钉点在哪里,当前被钉点的元素(可以是类/字段/方法/构造函数等)就能携带更多的额外信息,然后我们简单的针对当前元素调用出注解field.getAnnotations(MyAnno.class)来消费,就能很好的在这么多的字段中,判别出哪个字段才是主键。注解不理解的话,可以参考我的博客 Java基础 -- 06注解Annotation
反射结合泛型再配套注解,更更更强悍:
以上两个图,是jdk5之后,java为了迎合到底怎么处理泛型而扩展出来的类型。从jdk5之后,java有了这些类型:
- 8种基本数据类型、
- Class类类型、
- ParameterizedType泛型、
- TypeVariable泛型变量类型、
- AnnotatedArrayType泛型(/泛型变量)数组类型
上图中的配套小示例以及不了解泛型的话,请参考我的博客 Java基础 -- 04泛型
为什么说,更更更强悍呢?先看小示例: