读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code
建议93:Java的泛型是类型擦除的★☆☆☆☆
Java的泛型在编译期有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。
//下面这种方法的重载,编辑器会报错,提示方法冲突....
//'listMethod(List)' clashes with 'listMethod(List)'; both methods have same erasure
public void listMethod(List stringList){
}
public void listMethod(List intList) {
}
这就是Java泛型擦除引起的问题:在编译后所有的泛型类型都会做相应的转化。转换规则如下:
-
List
、List
、List
擦除后的类型为List
。 -
List
擦除后的类型为[] List[]
。 -
List extends E>
、List super E>
擦除后的类型为List
。 -
List
擦除后为List
。
Java之所以如此处理,有两个原因:
- 避免
JVM
的大换血。C++
的泛型生命期延续到了运行期,而Java是在编译器擦除掉的,如果JVM
也把泛型类型延续到运行期,那么JVM
就需要进行大量的重构工作了。 - 版本兼容。在编译期擦除可以更好地支持原生类型(Raw Type),在Java 1.5或1.6平台上,即使声明一个
List
这样的原生类型也是可以正常编译通过的,只是会产生警告信息而已。
我们就可以解释类似如下的问题了:
-
泛型的
class
对象是相同的public static void main(String[] args) { List
ls = new ArrayList (); List li = new ArrayList (); System.out.println(ls.getClass() == li.getClass()); //运行结果:true } 每个类都有一个class属性,泛型化不会改变
class
属性的返回值 -
泛型数组初始化时不能声明泛型类型
//如下代码编译时通不过: List
[] list = new List []; 可以声明一个带有泛型参数的数组,但是不能初始化该数组,因为执行了类型擦除操作,
List
与List
就是同一回事了,编译器拒绝如此声明。[] -
instanceof
不允许存在泛型参数//以下代码不能通过编译,原因一样,泛型类型被擦除了: List
list = new ArrayList (); System.out.println(list instanceof List )
建议94:不能初始化泛型参数和数组★☆☆☆☆
示例如下:
//这段代码是编译通不过的,因为编译器在编译时需要获得T类型,但泛型在编译期类型已经被擦除了
//所以new T()和new T[5]都会报错
public class Client {
private T t = new T();
private T[] tArray = new T[5];
private List list = new ArrayList();
}
在某些情况下,我们确实需要泛型数组,那该如何处理呢?代码如下:
public class Client {
//不在初始化,由构造函数初始化
private T t ;
private T[] tArray;
private List list = new ArrayList();
public Client() {
try {
Class> tType = Class.forName("");
t = (T) tType.newInstance();
tArray = (T[]) Array.newInstance(tType,5);
}catch (Exception e){
e.printStackTrace();
}
}
类的成员变量是在类初始化前初始化的,所以要求在初始化前它必须具有明确的类型,否则就只能声明,不能初始化。
建议95:强制声明泛型的实际类型★☆☆☆☆
示例:
class ArrayUtils{
//把一个变长参数转变为列表
public static List asList(T...t){
List list = new ArrayList();
Collections.addAll(list, t);
return list;
}
}
public class Client {
public static void main(String[] args) {
//强制声明泛型类型
//asList方法要求的是一个泛型参数,在输入前定义这是一个Integer类型的参数,当然,输出也是Integer类型的集合了
List list = ArrayUtils.asList();
}
}
注意:无法从代码中推断出泛型类型的情况下,即可强制声明泛型类型。
建议96:不同的场景使用不同的泛型通配符★★☆☆☆
Java泛型支持通配符(Wildcard),可以单独使用一个“?”
表示任意类,也可以使用extends
关键字表示某一个类(接口)的子类型,还可以使用super
关键字表示某一个类(接口)的父类型,但问题是什么时候该用extends
,什么时候该用super
呢?
-
泛型结构只参与“读”操作则限定上界(使用
extends
关键字)public static
void read(List extends E> list){ for(E e:list){ System.out.println(e.getClass()); //业务逻辑处理 } } -
泛型结构只参与“写”操作则限定下界(使用
super
关键字)public static void write(List super Number> list) { list.add(123); list.add(3.14); }
对于是要限定上界还是限定下界,JDK
的Collections.copy
方法是一个非常好的例子,它实现了把源列表中的所有元素拷贝到目标列表中对应的索引位置上,代码如下:
//源列表是用来提供数据的,所以src变量需要限定上界,带有extends关键字。
//目标列表是用来写入数据的,所以dest变量需要界定上界,带有super关键字。
public static void copy(List super T> dest, List extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i di=dest.listIterator();
ListIterator extends T> si=src.listIterator();
for (int i=0; i
如果一个泛型结构即用作“读”操作又用作“写”操作,那该如何进行限定呢?不限定,使用确定的泛型类型即可,如
List
。
建议97:警惕泛型是不能协变和逆变的★★☆☆☆
什么叫协变(covariance
)和逆变(contravariance
)?
在编程语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性,简单地说,协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。
泛型既不支持协变,也不支持逆变:
public static void main(String[] args) {
//数组支持协变
Number[] n = new Integer[10];
//编译不通过,泛型不支持协变
List ln = new ArrayList();
//报错:Type mismatch: cannot convert from ArrayList to List
}
-
可以使用通配符(Wildcard)模拟协变,代码如下所示:
//Number的子类型都可以是泛型参数类型 List extends Number> ln = new ArrayList
(); -
可以使用super关键字来模拟逆变,代码如下所示:
//Integer的父类型(包括Integer)都可以是泛型参数类型 List super Integer> li = new ArrayList
();
注意:Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变。
建议98:建议采用的顺序是List
、List>
、List
★★☆☆☆
原因如下:
-
List
是确定的某一个类型List
表示的是List集合中的元素都为T
类型,具体类型在运行期决定;List>
表示的是任意类型,与List
类似,而
List
则表示List
集合中的所有元素为Object
类型,因为Object
是所有类的父类,所以List
也可以容纳所有的类类型,从这一字面意义上分析,
List
更符合习惯:编码者知道它是某一个类型,只是在运行期才确定而已。 -
List
可以进行读写操作List
可以进行如add
、remove
等操作,因为它的类型是固定的T
类型,在编码期不需要进行任何的转型操作。List>
是只读类型的,不能进行增加、修改操作,因为编译器不知道List
中容纳的是什么类型的元素,也就无法校验类型是否安全了,而且List>
读取出的元素都是Object
类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意,List>
虽然无法增加、修改元素,但是却可以删除元素,比如执行remove
、clear
等方法,那是因为它的删除动作与泛型类型无关。List
也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据后需要向下转型(Downcast),而此时已经失去了泛型存在的意义了。
建议99:严格限定泛型类型采用多重界限★★★☆☆
比如在公交车费优惠系统中,对部分人员(如工资低于2500元的上班族并且是站立着的乘客)车费打8折,该如何实现呢?
//职员
interface Staff{
//工资
public int getSalary();
}
//乘客
interface Passenger{
//是否是站立状态
public boolean isStanding();
}
class Me implements Staff,Passenger{
public boolean isStanding(){
return true;
}
public int getSalary() {
return 2000;
}
}
//使用多重限定
public class Client {
//工资低于2500元的上斑族并且站立的乘客车票打8折
public static void discount(T t){
if(t.getSalary()<2500 && t.isStanding()){
System.out.println("恭喜你!您的车票打八折!");
}
}
public static void main(String[] args) {
discount(new Me());
}
}
在Java的泛型中,可以使用“&”
符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。
使用多重边界可以很方便地解决问题,而且非常优雅,建议在开发中考虑使用多重限定
建议100:数组的真实类型必须是泛型类型的子类型★★★☆☆
期望输入的是一个泛型化的List,转化为泛型数组,代码如下:
public class Client {
public static T[] toArray(List list) {
T[] t = (T[]) new Object[list.size()];
for (int i = 0, n = list.size(); i < n; i++) {
t[i] = list.get(i);
}
return t;
}
public static void main(String[] args) {
List list = Arrays.asList("A", "B");
for (String str : toArray(list)) {//这一句报错,Object数组不能向下转型为String数组
System.out.println(str);
}
}
}
运行异常如下:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at com.jyswm.demo.Client.main(Client.java:17)
因为泛型是类型擦除的,toArray
方法经过编译后与如下代码相同:
public static Object[] toArray(List list){
//此处的强制类型没必要存在,只是为了保持与源代码对比
Object[] t = (Object[])new Object[list.size()];
for(int i=0,n=list.size();i
那该如何解决呢?
其实要想把一个Obejct
数组转换为String
数组,只要Object
数组的实际类型(Actual Type)也是String
就可以了,例如:
public class Client {
public static void main(String[] args) {
//objArray的实际类型和表面类型都是String数组
Object[] objArray = {"A","B"};
//抛出ClassCastException
String[] strArray = (String[])objArray;
String[] ss = {"A","B"};
//objs的真实类型是String数组,显示类型为Object数组
Object[] objs = ss;
//顺利转换为String数组
String[] strs = (String[])objs;
}
}
如此,那就把泛型数组声明为泛型类的子类型吧!代码如下:
public class Client {
public static T[] toArray(List list, Class tClass) {
//声明并初始化一个T类型的数组
//通过反射类Array声明了一个T类型的数组,
//由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型
T[] t = (T[]) Array.newInstance(tClass, list.size());
for(int i=0,n=list.size();i list = Arrays.asList("A", "B");
for (String str : toArray(list,String.class)) {
System.out.println(str);
}
}
}
注意:当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型类型的父类型(比如顶层类Object),只能是泛型类型的子类型(当然包括自身类型),否则就会出现类型转换异常。
建议101:注意Class类的特殊性★☆☆☆☆
Java语言是先把Java源文件编译成后缀为class
的字节码文件,然后再通过ClassLoader
机制把这些类文件加载到内存中,最后生成实例执行的,这是Java处理的基本机制,但是加载到内存中的数据是如何描述一个类的呢?
Java使用一个元类(MetaClass
)来描述加载到内存中的类数据,这就是Class
类,它是一个描述类的类对象。
Class
类特殊的地方:
无构造函数。Java中的类一般都有构造函数,但是
Class
类却没有构造函数,不能实例化,Class
对象是在加载类时由 Java 虚拟机通过调用类加载器中的defineClass
方法自动构造的。可以描述基本类型。虽然8个基本类型在
JVM
中并不是一个对象,它们一般存在于栈内存中,但是Class
类仍然可以描述它们,例如可以使用int.class
表示int
类型的类对象。-
其对象都是单例模式。一个
Class
的实例对象描述一个类,并且只描述一个类,反过来也成立,一个类只有一个Class
实例对象,如下代码返回的结果都为true
:public class Client { public static void main(String[] args) throws Exception { //类的属性class所引用的对象与实例对象的getClass返回值相同 String.class.equals(new String().getClass()); "ABC".getClass().equals(String.class); //class实例对象不区分泛型 ArrayList.class.equals(new ArrayList
().getClass()); } }
建议102:适时选择getDeclared×××
和get×××
★☆☆☆☆
Java的Class
类提供了很多的getDeclared×××
方法和get×××
方法,如下:
public static void main(String[] args) throws Exception {
//方法名称
String methodName = "doStuff";
Method m1 = Foo.class.getDeclaredMethod(methodName);
Method m2 = Foo.class.getMethod(methodName);
}
getMethod
方法获得的是所有public
访问级别的方法,包括从父类继承的方法,而getDeclaredMethod
获得是自身类的所有方法,包括公用(public
)方法、私有(private
)方法等,而且不受限于访问权限。
其他的getDeclaredConstructors
和getConstructors
、getDeclaredFields
和getFields
等与此相似。
建议103:反射访问属性或方法时将Accessible
设置为true
★★☆☆☆
Java中通过反射执行一个方法的过程如下:获取一个方法对象,然后根据isAccessible
返回值确定是否能够执行,如果返回值为false
则需要调用setAccessible(true)
,最后再调用invoke执行方法。如下:
Method method= ...;
//检查是否可以访问
if(!method.isAccessible()){
method.setAccessible(true);
}
//执行方法
method.invoke(obj, args);
那为什么要这么写呢?
首先,Accessible
的属性并不是访问权限,而是指是否要更容易获得,是否进行安全检查。
AccessibleObject
类的源代码如下:
//它提供了取消默认访问控制检查的功能
public class AccessibleObject implements AnnotatedElement {
//定义反射的默认操作权限suppressAccessChecks
static final private java.security.Permission ACCESS_PERMISSION =
new ReflectPermission("suppressAccessChecks");
//是否重置了安全检查,默认为false
boolean override;
//构造函数
protected AccessibleObject() {}
//是否可以快速获取,默认是不能
public boolean isAccessible() {
return override;
}
}
Accessible
属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度的提升系统性能了(注意:取消了安全检查,也可以运行private
方法、访问private
属性的)。经过测试,在大量的反射情况下,设置Accessible
为true
可以提高性能20倍左右。
建议104:使用forName
动态加载类文件★★☆☆☆
动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件。
对Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否要加载一个类。
比如从Web上接收一个String
参数作为类名,然后在JVM
中加载并初始化,这就是动态加载,此动态加载通常是通过Class.forName(String)
实现的,只是为什么要使用forName
方法动态加载一个类文件呢?
因为我们不知道将要生成的实例对象是什么类型(如果知道就不用动态加载),而且方法和属性都不可访问。
动态加载的意义在什么地方呢?示例如下:
class Utils{
//静态代码块
static{
System.out.println("Do Something.....");
}
}
public class Client {
public static void main(String[] args) throws ClassNotFoundException {
//动态加载
Class.forName("Utils");
//此时输出了:Do Something.....
}
}
如上,并没有对Utils
做任何初始化,只是通过forName
方法加载了Utils
类,但是却产生了一个Do Something
的输出,这就是因为Utils
类被加载后,JVM
会自动初始化其static
变量和static
代码块,这是类加载机制所决定的。
经典的应用:数据库驱动程序的加载片段
//加载驱动
Class.forName("com.mysql..jdbc.Driver");
String url="jdbc:mysql://localhost:3306/db?user=&password=";
Connection conn =DriverManager.getConnection(url);
Statement stmt =conn.createStatement();
Class.forName("com.mysql..jdbc.Driver");
这一句的意义,示例如下:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//静态代码块
static {
try {
//把自己注册到DriverManager中
DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
//异常处理
throw new RuntimeException("Can't register driver!");
}
}
//构造函数
public Driver() throws SQLException {
}
}
程序逻辑如下:当程序动态加载该驱动时,也就是执行到Class.forName("com.mysql.jdbc.Driver")
时,Driver
类会被加载到内存中,也就是把自己注册到DriverManager
中。
forName
只是把一个类加载到内存中,并不保证由此产生一个实例对象,也不会执行任何方法,之所以会初始化static
代码,那是由类加载机制所决定的,而不是forName
方法决定的。也就是说,如果没有static
属性或static
代码块,forName
就只是加载类,没有任何的执行行为。
注意:
forName
只是加载类,并不执行任何代码。
建议105:动态加载不适合数组★☆☆☆☆
在Java中,数组是一个非常特殊的类,虽然它是一个类,但没有定义类路径。
示例:
public static void main(String[] args) throws ClassNotFoundException {
String [] strs = new String[10];
Class.forName("java.lang.String[]");
}
运行异常,如下:
Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[]
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:186)
因为编译器编译后为不同的数组类型生成不同的类,具体如下表所示:
所以动态加载一个对象数组只要加载编译后的数组对象就可以了,修改代码如下:
//加载一个String数组
Class.forName("[Ljava.lang.String;");
//加载一个Long数组
Class.forName("[J");
但是这种操作没有什么意义,因为它不能生成一个数组对象,只是把一个String
类型的数组类和long
类型的数组类加载到了内存中,它没有定义数组的长度,在Java中数组是定长的,没有长度的数组是不允许存在的。
因为数组的特殊性,所以Java专门定义了一个Array
数组反射工具类来实现动态探知数组的功能,如下:
// 动态创建数组
String[] strs = (String[]) Array.newInstance(String.class, 8);
// 创建一个多维数组
int[][] ints = (int[][]) Array.newInstance(int.class, 2, 3);
注意:通过反射操作数组使用
Array
类,不要采用通用的反射处理API
。
建议106:动态代理可以使代理模式更加灵活★★★☆☆
Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。
首先,简单的静态代理实现示例如下:
/**
* 抽象角色-厨师
*/
interface Chef {
/**
* 提供饺子
*/
String dumplings();
/**
* 提供面条
*/
String noodles();
}
/**
* 具体角色-厨师老张
*/
class RealChef implements Chef {
@Override
public String dumplings() {
return "老张秘制酸汤水饺";
}
@Override
public String noodles() {
return "老张秘制兰州牛肉面";
}
}
/**
* 代理角色(proxy)-幸福餐厅
*/
public class HappyRestaurant implements Chef {
/**
* 要代理哪个实现类(要让哪个厨师做)
*/
private Chef chef = null;
/**
* 默认被代理者(默认的厨师老张)
*/
public HappyRestaurant() {
chef = new RealChef();
}
/**
* 通过构造函数传递被代理者(客户点名哪个厨师做)
*/
public HappyRestaurant(Chef _chef) {
chef = _chef;
}
@Override
public String dumplings() {
before();
return chef.dumplings();
}
@Override
public String noodles() {
before();
return chef.noodles();
}
/**
* 预处理
*/
private void before() {
// 先收银
}
}
//调用
public static void main(String[] args) {
//来到幸福餐厅
HappyRestaurant happyRestaurant = new HappyRestaurant();
//点了一份饺子
String food = happyRestaurant.dumplings();
System.out.println(food);
//得到:老张秘制酸汤水饺
}
代理:"你去餐厅吃饭,并没有见给你真正做饭的厨师老张,而是由餐厅的服务人员端到你面前的。"
改为动态代理示例如下:
/**
* 抽象角色-厨师
*/
interface Chef {
/**
* 提供饺子
*/
String dumplings();
/**
* 提供面条
*/
String noodles();
}
/**
* 具体角色-厨师老张
*/
class RealChef implements Chef {
@Override
public String dumplings() {
return "老张秘制酸汤水饺";
}
@Override
public String noodles() {
return "老张秘制兰州牛肉面";
}
}
/**
* 委托处理(不是具体的哪一家餐厅,而是美团了)
*/
public class ChefHandler implements InvocationHandler {
/**
* 被代理的对象(厨师)
*/
private Chef chef;
public ChefHandler(Chef _chef) {
chef = _chef;
}
/**
* 委托处理方法(点外卖)
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 预处理
System.out.println("预处理...");
//直接调用被代理的方法
Object obj = method.invoke(chef, args);
// 后处理
System.out.println("后处理...");
return obj;
}
}
注意看,这里没有了餐厅这个角色,取而代之的是ChefHandler
作为主要的逻辑委托处理,其中invoke
方法是接口InvocationHandler
定义必须实现的,它完成了对真实方法的调用。
InvocationHanlder
接口:动态代理是根据被代理的接口生成所有方法的,也就是说给定一个(或多个)接口,动态代理会宣称“我已经实现该接口下的所有方法了”
动态代理的场景类,代码如下:
public static void main(String[] args) {
//被代理类(想吃老张做的饭,确定目标)
Chef chef = new RealChef();
//代理实例的处理Handler(打开美团app搜索老张)
InvocationHandler handler = new ChefHandler(chef);
//当前加载器(美团开始搜索并加载老张的信息)
ClassLoader classLoader = chef.getClass().getClassLoader();
//动态代理(美团已经拥有了老张的所有能力,比如提供一份水饺等)
Chef proxy = (Chef) Proxy.newProxyInstance(classLoader, chef.getClass().getInterfaces(), handler);
//调用具体方法(点一份酸汤水饺)
String food = proxy.dumplings();
System.out.println(food);
//得到: 老张秘制酸汤水饺
}
此时就实现了不用显式创建代理类即实现代理的功能。例如可以在被代理角色执行前进行权限判断,或者执行后进行数据校验。
建议107:使用反射增加装饰模式的普适性★★★☆☆
装饰模式(Decorator Pattern)的定义是 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活,
使用Java的动态代理也可以实现装饰模式的效果,而且其灵活性、适应性都会更强。
装饰一只小老鼠,让它更强大,示例如下:
interface Animal{
public void doStuff();
}
class Rat implements Animal{
@Override
public void doStuff() {
System.out.println("Jerry will play with Tom ......");
}
}
/**
* 使用装饰模式,给老鼠增加一些能力,比如飞行,钻地等能力
*/
//定义某种能力
interface Feature{
//加载特性
public void load();
}
//飞行能力
class FlyFeature implements Feature{
@Override
public void load() {
System.out.println("增加一对翅膀...");
}
}
//钻地能力
class DigFeature implements Feature{
@Override
public void load() {
System.out.println("增加钻地能力...");
}
}
/**
* 要把这两种属性赋予到老鼠身上,那需要一个包装动作类
*/
class DecorateAnimal implements Animal {
// 被包装的动物
private Animal animal;
// 使用哪一个包装器
private Class extends Feature> clz;
public DecorateAnimal(Animal _animal, Class extends Feature> _clz) {
animal = _animal;
clz = _clz;
}
@Override
public void doStuff() {
InvocationHandler handler = new InvocationHandler() {
// 具体包装行为
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object obj = null;
if (Modifier.isPublic(method.getModifiers())) {
obj = method.invoke(clz.newInstance(), args);
}
animal.doStuff();
return obj;
}
};
//当前加载器
ClassLoader cl = getClass().getClassLoader();
//动态代理,又handler决定如何包装
Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
proxy.load();
}
}
/**
* 注意看doStuff方法,
* 一个装饰类型必然是抽象构建(Component)的子类型,它必须实现doStuff方法,此处的doStuff方法委托给了动态代理执行,
* 并且在动态代理的控制器Handler中还设置了决定装饰方式和行为的条件(即代码中InvocationHandler匿名类中的if判断语句),
* 当然,此处也可以通过读取持久化数据的方式进行判断,这样就更加灵活了。
*/
/**
* 客户端进行调
*/
public static void main(String[] args) {
//定义Jerry这只老鼠
Animal jerry = new Rat();
//为Jerry增加飞行能力
jerry = new DecorateAnimal(jerry, FlyFeature.class);
//jerry增加挖掘能力
jerry = new DecorateAnimal(jerry, DigFeature.class);
//Jerry开始戏弄猫了
jerry.doStuff();
}
// 装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。
建议108:反射让模板方法模式更强大★★★☆☆
模板方法模式(Template Method Pattern
)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单地说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它使用了简单的继承和覆写机制。
普通模板方法,示例如下:
public abstract class AbsPopulator {
// 模板方法
public final void dataInitialing() throws Exception {
// 调用基本方法
doInit();
}
// 基本方法
protected abstract void doInit();
}
//子类实现
public class UserPopulator extends AbsPopulator{
@Override
protected void doInit() {
//初始化用户表,如创建、加载数据等
}
}
改造,使用反射增强模板方法模式,使模板方法实现对一批固定的规则的基本方法的调用。如下:
public abstract class AbsPopulator {
// 模板方法
public final void dataInitialing() throws Exception {
// 获得所有的public方法
Method[] methods = getClass().getMethods();
for (Method m : methods) {
// 判断是否是数据初始化方法
if (isInitDataMethod(m)) {
m.invoke(this);
}
}
}
// 判断是否是数据初始化方法,基本方法鉴定器
private boolean isInitDataMethod(Method m) {
return m.getName().startsWith("init")// init开始
&& Modifier.isPublic(m.getModifiers())// 公开方法
&& m.getReturnType().equals(Void.TYPE)// 返回值是void
&& !m.isVarArgs()// 输出参数为空
&& !Modifier.isAbstract(m.getModifiers());// 不能是抽象方法
}
}
//子类实现
public class UserPopulator extends AbsPopulator {
public void initUser() {
/* 初始化用户表,如创建、加载数据等 */
}
public void initPassword() {
/* 初始化密码 */
}
public void initJobs() {
/* 初始化工作任务 */
}
}
在一般的模板方法模式中,抽象模板(这里是AbsPopulator
类)需要定义一系列的基本方法,一般都是protected
访问级别的,并且是抽象方法,这标志着子类必须实现这些基本方法,这对子类来说既是一个约束也是一个负担。但是使用了反射后,不需要定义任何抽象方法,只需定义一个基本方法鉴别器(例子中isInitDataMethod
)即可加载符合规则的基本方法。鉴别器在此处的作用是鉴别子类方法中哪些是基本方法,模板方法(例子中的dataInitialing
)则根据基本方法鉴别器返回的结果通过反射执行相应的方法。
注意:决定使用模板方法模式时,请尝试使用反射方式实现,它会让你的程序更灵活、更强大。
建议109:不需要太多关注反射效率★★☆☆☆
反射的效率相对于正常的代码执行确实低很多(经过测试,相差15倍左右),但是它是一个非常有效的运行期工具类,只要代码结构清晰、可读性好那就先开发起来,等到进行性能测试时证明此处性能确实有问题时再修改也不迟(一般情况下反射并不是性能的终极杀手,而代码结构混乱、可读性差则很可能会埋下性能隐患)。
对于反射效率问题,不要做任何的提前优化和预期,这基本上是杞人忧天,很少有项目是因为反射问题引起系统效率故障的,而且根据二八原则,80%的性能消耗在20%的代码上,这20%的代码才是我们关注的重点,不要单单把反射作为重点关注对象。
注意:反射效率低是个真命题,但因为这一点而不使用它就是个假命题。