最近才开始学Java,这两天接触了框架后对于反射的概念有很大的困惑,在网上检索了很多信息后发现大多都是在说反射怎么使用的。将反射的原理以及为什么需要反射的比较少,即使有讲很多都比较晦涩难懂。
参考: 学习java应该如何理解反射? - 罗大然不写代码的回答 - 知乎
后来终于找到了一篇知乎回答讲反射的作用的,我觉得讲得很好,加上一些个人的些微见解记录下来。
简单的水果工厂
这位答主用了一个简单的工厂模式举例来说明反射的作用:
// Fruit接口
public interface Fruit {
public void eat();
}
// Apple类
public class Apple implements Fruit{
public void eat() {
System.out.println("eat apple.");
}
}
// Factory 用于生产Fruit
public class Factory {
public static Fruit getInstance(String className){
if(className.equals("Apple")){
return new Apple();
}else{
return null;
}
}
}
// demo
Fruit f = Factory.getInstance("Apple");
f.eat();
这是一个简单的工厂模式范例,工厂通过判断传入的参数来决定生产何种水果。下面我画了一张非常简陋的示意图来说明:
代码和示意图相结合来看:demo程序使用字符串传入了需求,工厂使用if条件来判断需求,使用new对象来生产水果。而判断部分和需求存在严重的耦合:需求的任何改动都需要判断做相应变动。我在需求内每增加一种水果,判断内就得多一条条件分支。
反射的使用
这个代码非常简单,所以修改一下单个类中的代码并不麻烦。如果我们生产水果的不同类型的工厂有一百个、一千个,如果还有和水果相关的其他工厂(生产果汁等)也需要传入需求,那就完全无法解决了。而反射就能够解决这一困难:
// 重写Factory的代码
public class Factory {
public static Fruit getInstance(String className){
// if(className.equals("Apple")){
// return new Apple();
// }else{
// return null;
// }
Fruit f = null;
try {
// 这里要注意className应包含包名
f = (Fruit) Class.forName(className).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
我们通过反射,利用传入的字符串直接获取对应类的class对象,并利用class对象来创建对应的水果实体,这样就不需要在增加需求时反复修改判断部分的代码了。(这应该是称作解耦吧),同样用一张图来解释:
原本的判断部分替换成了反射,也就是说:
- 不再由工厂对需求进行判断
- 需求通过反射直接传递给JVM去查找class
通俗解释
未使用反射之前的代码,我们可以想象成一个工厂有一个专门的订单管理员,这个订单管理员每次接到甲方的需求后都会跟自己手上的生产表单(判断代码)去比对,如果表单里有这一项那么这位管理员就会前往生产线(JVM)上要求开工生产。
生产线接到管理员的指令后,便开始着手准备生产(类加载器相关内容),如果增加了新的需求,那么生产表单也需要同步进行更新,非常麻烦。而且工厂很多,每次新需求都更新表单非常的困难。
于是,这位管理员就直接下岗了(取消判断部分),直接由甲方和生产线对接,需求直接提到了生产线上,生产线根据需求来进行生产:
这张图涉及到了较浅显的类加载器概念,了解一下类加载器就能将这张图和实际的类加载对应起来了。
如果是现实中的工厂,那么我们很容易就能想到这种优化的方法,然而在不具备反射特性的情况下要实现甲方和生产线(需求代码和JVM)的对接是十分困难的一件事,因为Java的class对象是不允许程序员手动进行创建的,而class对象是创建对应类实例的必要部分(生产线)。
我们首先考虑一门纯静态的语言,比如C语言,可以将整个过程抽象为:编译-->链接-->形成二进制文件-->执行,其最大的特点就是:代码一经编译链接形成二进制文件,执行时的内存加载是完全确定的,即只要源代码不变则不会根据不同的执行情况产生不同的内存加载结果。
举个例子:在C语言中我写了一个main.c文件和两个头文件a.h和b.h,如果在.c文件中导入了a.h文件后编译执行,无论执行多少遍,程序接收了怎样的输入,都不可能存在b.h文件的代码被执行,因为这是从编译时就确定好的。
假设Java不具备动态特性,那么其结果也是一样的,我不可能根据className这个变量选择加载哪一个class文件,如果我要加载它,我必须要在代码里写明(new一个对象)。
而现在有了反射之后,工厂接收了Apple这个参数,那么JVM就给你加载Apple.class,接收了Orange这个参数,就给你加载Orange.class,每次程序执行都能通过不同的参数动态地控制加载进来的class文件。
最后
上面的内容其实相当不成熟,肯定也有理解得有错误的地方,如果有的话还希望大家能够帮忙指正出来。