读书笔记----《编写高质量代码:改善Java程序的151个建议》第六/七章

第六章 枚举和注释

不废话了, 直接来学习吧。

83 推荐使用枚举定义常量

枚举类型是java 1.5之后提供的支持
JLS(Java Language Specification, Java语言规范)提倡枚举项全部大写, 字母之间用下划线分隔。
枚举常量相较于类常量和静态常量的优势:

  1. 枚举常量更简单
  2. 枚举常量属于稳态型【即使用时不用考虑取值不规范的情况】
  3. 枚举具有内置方法
    每个枚举都是java.lang.Enum的子类, 该基类提供了诸如获得排序值的ordinal方法、compareTo比较方法等。
  4. 枚举可以自定义方法

注:枚举自带一个int和String类型的成员, 这两个成员是编译器加上去的。分别对应着Enum的ordinal() 和 name()方法;
ordinal序号从0开始, 序号与定义顺序一一对应。
name直接返回枚举的成员名称。

84:使用构造函数协助描述枚举项

直接举例:

enum Role{
Admin("管理员", new Lifetime(), new Scope()), 
User("普通用户", new Lifetime(), new Scope());
//中文描述
private String name;
//角色的生命期
private Lifetime lifeTime;
//权限范围
private Scope scope;
Role(String_name, Lifetime_lt, Scope_scope){
name=_name;
lifeTime=_lt;
scope=_scope;
}
/*name、lifeTime、scope的get方法较简单, 不再赘述*/
}

85:小心switch带来的空值异常

public static void doSports(Season season){
switch(season){
case Spring:
System.out.println("春天放风筝");
break;
case Summer:
System.out.println("夏天游泳");
break;
case Autumn:
...
case Winter:
...
default:
System.out.println("输入错误!");
break;
}
}

初看没有错, 但是switch的season变量有可能是null, 而且该null不会触发“输入错误”, 因为编译时, 编译器判断出switch语句后的参数是枚举类型, 然后就会根据枚举的排序值继续匹配。而不是我们平常想象的编译器真的增加了一个switch新支持的类型。

其实以上代码与下面等效:

public static void doSports(Season season){
switch(season.ordinal()){
case Season.Spring.ordinal():
……
case Season.Summer.ordinal():
……
}
}

86:在switch的default代码块中增加AssertionError错误

switch代码与枚举之间没有强制约束关系, 也就是说两者只是在语义上建立了联系, 并没有一个强制约束。比如枚举发生改变, 增加了一个枚举项, 如果此时我们对switch语句不做任何修改, 编译虽不会出现问题, 但是运行期会发生非预期的错误, 为了避免出现这类错误, 建议在default后直接抛出一个AssertionError错误。

也有其他方法解决此问题, 比如修改IDE工具, 以Eclipse为例, 可以把Java→Compiler→Errors/Warnings中的“Enum type constant not covered on’switch’”设置为Error级别。

87:使用valueOf前必须进行校验

枚举的valueOf方法先通过反射从枚举类的常量声明中查找, 若找到就直接返回, 若找不到则抛出无效参数异常。valueOf本意是保护编码中的枚举安全性, 使其不产生空枚举对象, 简化枚举操作, 但是却又引入了一个我们无法避免的IllegalArgumentException异常。
源码如下

public static< T extends Enum< T>>T valueOf(Class< T>enumType, 
String name){
//通过反射, 从常量列表中查找
T result=enumType.enumConstantDirectory().get(name);
if(result!=null)
return result;
if(name==null)
throw new NullPointerException("Name is null");
//最后排除无效参数异常
throw new IllegalArgumentException("No enum const"+enumType+"."+name);
}

有两个方法可以解决此问题:
(1)使用try……catch捕捉异常
(2)扩展枚举类:通过增加一个contains方法来判断是否包含指定的枚举项

enum Season{
	Spring, Summer, Autumn, Winter;
//是否包含指定名称的枚举项
	public static boolean contains(String name){
		//所有的枚举值
		Season[]season=values();
		//遍历查找
		for(Season s:season){
			if(s.name().equals(name)){
				return true;
			}
		}
		return false;
	}
}

88:用枚举实现工厂方法模式更简洁

枚举实现工厂方法模式有两种方法:

  1. 枚举非静态方法实现工厂方法模式
enum CarFactory{
//定义工厂类能生产汽车的类型
FordCar, BuickCar;
//生产汽车
public Car create(){
switch(this){
case FordCar:
return new FordCar();
case BuickCar:
return new BuickCar();
default:
throw new AssertionError("无效参数");
}
}
}


public static void main(String[]args){
//生产汽车
Car car=CarFactory.BuickCar.create();
}
  1. 通过抽象方法生成产品
enum CarFactory{
FordCar{
public Car create(){
return new FordCar();
}
}, 
BuickCar{
public Car create(){
return new BuickCar();
}
};
//抽象生产方法
public abstract Car create();
}

使用枚举类型的工厂方法模式有以下三个优点:

  1. 避免错误调用的发生
  2. 性能好, 使用便捷
  3. 降低类间耦合

89:枚举项的数量限制在64个以内

Java提供了两个枚举集合:EnumSet和EnumMap
跟踪Enum的allOf方法, 其源代码如下:

public static< E extends Enum< E>>EnumSet< E>allOf(Class< E>elementType){
//生成一个空EnumSet
EnumSet< E>result=noneOf(elementType);
//加入所有的枚举项
result.addAll();
return result;
}

noneOf的源码:

public static< E extends Enum< E>>EnumSet< E>noneOf(Class< E>elementType){
//获得所有枚举项
Enum[]universe=getUniverse(elementType);
if(universe==null)
throw new ClassCastException(elementType+"not an enum");
if(universe.length< =64)
//枚举数量小于等于64
return new RegularEnumSet< E>(elementType, universe);
else
//枚举数量大于64
return new JumboEnumSet< E>(elementType, universe);
}

RegularEnumSet:

class RegularEnumSet< E extends Enum< E>>extends EnumSet< E>{
//记录所有枚举排序号, 注意是long型
private long elements=0L;
//构造函数
RegularEnumSet(Class< E>elementType, Enum[]universe){
super(elementType, universe);
}
//加入所有元素
void addAll(){
if(universe.length!=0)
elements=-1L>>>-universe.length;
}
}

注意看addAll方法的elments元素, 它使用了无符号右移操作, 并且操作数是负值, 位移也是负值, 这表示是负数(符号位是1)的“无符号左移”:符号位为0, 并补充低位, 简单地说, Java把一个不多于64个枚举项的枚举映射到了一个long类型变量上。
long类型是64位的, 所以RegularEnumSet类型也就只能负责枚举项数量不大于64的枚举。

再看JumboEnumSet:

class JumboEnumSet< E extends Enum< E>>extends EnumSet< E>{
//映射所有的枚举项
private long elements[];
//构造函数
JumboEnumSet(Class< E>elementType, Enum[]universe){
super(elementType, universe);
//默认长度是枚举项数量除以64再加1
elements=new long[(universe.length+63)>>>6];
}
void addAll(){
//elements中每个元素表示64个枚举项
for(int i=0;i< elements.length;i++)
elements[i]=-1;
elements[elements.length-1]>>>=-universe.length;
size=universe.length;
}
}

JumboEnumSet类把枚举项按照64个元素一组拆分成了多组, 每组都映射到一个long类型的数字上, 然后该数组再放置到elements数组中。简单来说JumboEnumSet类的原理与RegularEnumSet相似, 只是JumboEnumSet使用了long数组容纳更多的枚举项。

注意: 枚举项数量不要超过64, 否则建议拆分。

90:小心注解继承

注解 Annotation 也是java 1.5之后引入的。
关于注解的基本知识可以参考这篇文章
定义一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@interface Desc{
enum Color{
White, Grayish, Yellow;
}
//默认颜色是白色的
Color c()default Color.White;
}
@Desc(c=Color.White)
abstract class Bird{
...
}

//麻雀
class Sparrow extends Bird{
...
}

在注解上加了@Inherited注解表示只要把注解@Desc加到父类Bird上, 它的所有子类都会自动从父类继承@Desc注解, 不需要显式声明, 这与Java类的继承有点不同, 若Sparrow类继承了Bird, 则必须使用extends关键字声明, 而Bird上的注解@Desc继承自Bird却不用显式声明, 只要声明@Desc注解是可自动继承的。

91:枚举和注解结合使用威力更大

注解的写法和接口很类似, 都采用了关键字interface, 而且都不能有实现代码。

访问控制的实例:

interface Identifier{
//无权访问时的礼貌语
String REFUSE_WORD="您无权访问";
//鉴权
public boolean identify();
}


enum CommonIdentifer implements Identifier{
//权限级别
Reader, Author, Admin;
//实现鉴权
public boolean identify(){
return false;
}
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Access{
//什么级别可以访问, 默认是管理员
CommonIdentifier level()default CommonIdentifier.Admin;
}


@Access(level=CommonIdentifier.Author)
class Foo{
}

public static void main(String[]args){
//初始化商业逻辑
Foo b=new Foo();
//获取注解
Access access=b.getClass().getAnnotation(Access.class);
//没有Access注解或者鉴权失败
if(access==null||!access.level().identify()){
//没有Access注解或者鉴权失败
System.out.println(access.level().REFUSE_WORD);
}
}

注意@Override不同版本的区别

java 1.5版中@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输入参数、输出参数(允许子类缩小)、访问权限(允许子类扩大), 父类必须是一个类, 不能是一个接口, 否则不能算是覆写。
而这在Java 1.6就开放了很多, 实现接口的方法也可以加上@Override注解了, 可以避免粗心大意导致方法名称与接口不一致的情况发生。

第7章 泛型和反射

93:Java的泛型是类型擦除的

Java泛型(Generic)的引入加强了参数类型的安全性, 减少了类型的转换, 它与C++中的模板(Templates)比较类似, 但是有一点不同的是:Java的泛型在编译期有效, 在运行期被删除, 也就是说所有的泛型参数类型在编译后都会被清除掉。
如以下两个方法:
public void listMethod(List< String>stringList){
}
public void listMethod(List< Integer>intList){
}
在同一个类中无法编译。

List< String>、List< Integer>、List< T>擦除后的类型为List。
List< String>[]擦除后的类型为List[]。
List< ?extends E>、List< ?super E>擦除后的类型为List< E>。
List< T extends Serializable&Cloneable>擦除后为List< Serializable>。

注意以下场景:

  1. 泛型的class对象是相同的
public static void main(String[]args){
List< String>ls=new ArrayList< String>();
List< Integer>li=new ArrayList< Integer>();
System.out.println(li.getClass()==li.getClass());
}
//输出为 true
  1. 泛型数组初始化时不能声明泛型类型
    List< String>[]listArray=new List< String>[];

  2. instanceof不允许存在泛型参数

94:不能初始化泛型参数和数组

class Foo< T>{
//不再初始化, 由构造函数初始化
private T t;
private T[]tArray;
private List< T>list=new ArrayList< T>();
//构造函数初始化
public Foo(){
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< T>List< T>asList(T……t){
List< T>list=new ArrayList< T>();
Collections.addAll(list, t);
return list;
}
}
public static void main(String[]args){
//正常用法
List< String>list1=ArrayUtils.asList("A", "B");
//参数为空
List list2=ArrayUtils.asList();
//参数为数字和字符串的混合
List list3=ArrayUtils.asList(1, 2, 3.1);
}
// List1没有问题
// List2 可以写为
List<Integer> list2=ArrayUtils.<Integer>asList();
//List3 可以写为
List< Number>list3=ArrayUtils.< Number>asList(1, 2, 3.1);

96:不同的场景使用不同的泛型通配符

示例解释
String实际类型参数
List< String>参数化类型
E形式类型参数
List< E>泛型
List< ?>无限制通配符类型
List原生类型
< E extends Number>有限制类型参数
<T extends Coparable< T>>递归类型限制
List<? extends Number>有限制通配符类型
static < E> List< E> asList(E[] a)泛型方法

97:警惕泛型是不能协变和逆变的

协变(covariance)和逆变(contravariance):
在编程语言的类型框架中, 协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性, 简单地说, 协变是用一个窄类型替换宽类型, 而逆变则是用宽类型覆盖窄类型。

class Base{
public Number doStuff(){
return 0;
}
}
class Sub extends Base{
@Override
public Integer doStuff(){
return 0;
}
}

//子类的doStuff方法返回值的类型比父类方法要窄, 
//此时doStuff方法就是一个协变方法, 
//同时根据Java的覆写定义来看, 这又属于覆写

class Base{
public void doStuff(Integer i){
}
}
class Sub extends Base{
public void doStuff(Number n){
}
}
//子类的doStuff方法的参数类型比父类要宽, 此时就是一个逆变方法

但是 泛型即不支持协变, 也不支持逆变。

  1. 泛型不支持协变
public static void main(String[]args){
//数组支持协变
Number[]n=new Integer[10];
//编译不通过, 泛型不支持协变
List< Number>ln=new ArrayList< Integer>();
}
// 泛型不支持协变, 但可以使用通配符(Wildcard)模拟协变, 代码如下所示:
//Number的子类型(包括Number类型)都可以是泛型参数类型
List< ?extends Number>ln=new ArrayList< Integer>();
  1. 泛型不支持逆变
    如下:
    //Integer的父类型(包括Integer)都可以是泛型参数类型
    List< ?super Integer>li=new ArrayList< Number>();
    编译不通过

注意

  1. List< Integer> 不是 List< Number>的子类型
  2. List< Integer> 不是 List<? extends Integer>的子类型
  3. List< Integer> 不是 List<? super Integer>的子类型

98:建议采用的顺序是List< T>、List< ?>、List< Object>

List< T>是确定的某一个类型
List< T>可以进行读写操作
List< ?>是只读类型的
List< Object>也可以读写操作, 但是它执行写入操作时需要向上转型。
Dao< T>应该比Dao< ?>、Dao< Object>更先采用, Desc< Person>则比Desc< ?>、Desc< Object>更优先采用。

99:严格限定泛型类型采用多重界限

< T extends Staff&Passenger>
“&”符号设定多重边界(Multi Bounds), 指定泛型类型T必须是Staff和Passenger的共有子类型。

100:数组的真实类型必须是泛型类型的子类型

public static< T>T[]toArray(List< T>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< String>list=Arrays.asList("A", "B");
for(String str:toArray(list)){
System.out.println(str);
}

// 这段编译没问题, 但是运行报错:
Exception in thread"main"java.lang.ClassCastException:[Ljava.lang.Object;
cannot be cast to[Ljava.lang.String;at Client.main(Client.java:20)

这里Object数组不能向下转型为String数组很好理解。
但是为什么是main方法抛出异常, 而不是toArray方法?
实际上在toArray方法中进行的类型向下转换, 而不是main方法中。

原因:泛型是类型擦除的
改进:

public static< T>T[]toArray(List< T>list, Class< T>tClass){
//声明并初始化一个T类型的数组
T[]t=(T[])Array.newInstance(tClass, list.size());
for(int i=0, n=list.size();i< n;i++){
t[i]=list.get(i);
}
return t;
}

101:注意Class类的特殊性

Java使用一个元类(MetaClass)来描述加载到内存中的类数据, 这就是Class类, 它是一个描述类的类对象。他的特殊性如下:

  1. 无构造函数。Class对象是在加载类时由Java虚拟机通过调用类加载器中的defineClass方法自动构造的。
  2. 可以描述基本类型。虽然8个基本类型在JVM中并不是一个对象, 它们一般存在于栈内存中, 但是Class类仍然可以描述它们, 例如可以使用int.class表示int类型的类对象。
  3. 其对象都是单例模式。一个Class的实例对象描述一个类, 并且只描述一个类, 反过来也成立, 一个类只有一个Class实例对象。
  4. Class类是Java的反射入口, 只有在获得了一个类的描述对象后才能动态地加载、调用, 一般获得一个Class对象有三种途径:
  1. 类属性方式, 如String.class。
  2. 对象的getClass方法, 如new String().getClass()。
  3. forName方法加载, 如Class.forName(“java.lang.String”)。

102:适时选择getDeclared×××和get×××

Class类提供了很多的getDeclared×××方法和get×××方法, 例如getDeclared-Method和getMethod成对现, getDeclaredConstructors和getConstructors也是成对出现。
区别:
getMethod方法获得的是所有public访问级别的方法, 包括从父类继承的方法, 而getDeclaredMethod获得是自身类的所有方法, 包括公用(public)方法、私有(private)方法, 而且不受限于访问权限。

其他的getDeclared×××方法和get×××方法类似。

103:反射访问属性或方法时将Accessible设置为true

通过反射方式执行方法时, 必须在invoke之前检查Accessible属性。

Accessible的属性并不是我们语法层级理解的访问权限, 而是指是否更容易获得, 是否进行安全检查。

动态修改一个类或方法或执行方法时都会受Java安全体系的制约, 而安全的处理是非常消耗资源的(性能非常低), 因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查。

来看AccessibleObject类的源代码, 它提供了取消默认访问控制检查的功能。首先查看isAccessible方法

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;
}
}

AccessibleObject是Field、Method、Constructor的父类, 决定其是否可以快速访问而不进行访问控制检查, 在AccessibleObject类中是以override变量保存该值的, 但是具体是否快速执行是在Method类的invoke方法中决定的:

public Object invoke(Object obj, Object……args)throws……{
//检查是否可以快速获取, 其值是父类AccessibleObject的override变量
if(!override){
//不能快速获取, 要进行安全检查
if(!Reflection.quickCheckMemberAccess(……){
……
Reflection.ensureMemberAccess(……);
……
}
}
//直接执行方法
return methodAccessor.invoke(obj, args);
}

Accessible属性只是用来判断是否需要进行安全检查的, 如果不需要则直接执行, 这就可以大幅度地提升系统性能(当然了, 由于取消了安全检查, 也可以运行private方法、访问private私有属性了)。经过测试, 在大量的反射情况下, 设置Accessible为true可以提升性能20倍以上。

104:使用forName动态加载类文件

动态加载的意义在于:加载一个类即表示要初始化该类的static变量, 特别是static代码块, 在这里我们可以做大量的工作, 比如注册自己, 初始化环境等。

如JDBC连接类Driver类的源码

public class Driver extends NonRegisteringDriver implements java.sql.Driver{
//静态代码块
static{
try{
//把自己注册到DriverManager中
java.sql.DriverManager.registerDriver(new Driver());
}catch(SQLException E){
//异常处理
}
}
//构造函数
public Driver()throws SQLException{
}

Driver类只是负责把自己注册到DriverManager中。当程序动态加载该驱动时(执行到Class.forName(“com.mysql.jdbc.Driver”)时), Driver类会被加载到内存中, 于是static代码块开始执行, 也就是把自己注册到DriverManager中。
注意 forName只是加载类, 并不执行任何非static代码。

105:动态加载不适合数组

//加载一个String数组
Class.forName("[Ljava.lang.String;");
//加载一个long数组
Class.forName("[J");

虽然以上代码可以动态加载一个数组类, 但是这没有任何意义, 因为它不能生成一个数组对象, 也就是说以上代码只是把一个String类型的数组类和long类型的数组类加载到了内存中(如果内存中没有该类的话), 并不能通过newInstance方法生成一个实例对象, 因为它没有定义数组的长度, 在Java中数组是定长的, 没有长度的数组是不允许存在的。

可以通过以下方式来反射出一个数组:

//动态创建数组
String[]strs=(String[])Array.newInstance(String.class, 8);
//创建一个多维数组
int[][]ints=(int[][])Array.newInstance(int.class, 2, 3);

注意 通过反射操作数组使用Array类, 不要采用通用的反射处理API。

106:动态代理可以使代理模式更加灵活

简单静态代理

//抽象主题
interface Subject{
public void request();
}
//具体主题(被代理者)
class RealSubject implements Subject{
public void request(){
//业务逻辑处理
}
}
//代理主题(代理者)
class Proxy implements Subject{
private Subject subject=null;
//默认被代理者
public Proxy(){
subject=new RealSubject();
}
//通过构造传递被代理者
public Proxy(Subject _subject){
subject=_subject;
}
//实现方法
public void request(){
before();
subject.request();
after();
}
//预处理
private void before(){
//do something
}
//善后处理
private void after(){
//do something
}
}

Java还提供了java.lang.reflect.Proxy用于实现动态代理:

//抽象主题角色
interface Subject{
public void request();
}
//具体主题
class RealSubject implements Subject{
public void request(){
//业务逻辑处理
}
}
class SubjectHandler implements InvocationHandler{
//被代理的对象
private Subject subject;
public SubjectHandler(Subject_subject){
subject=_subject;
}
//委托处理方法
public Object invoke(Object proxy, Method method, Object[]args)throws Throwable{
//预处理
System.out.println("预处理");
//直接调用被代理类的方法
Object obj=method.invoke(subject, args);
//后处理
System.out.println("后处理");
return obj;
}
}

使用场景:

public static void main(String[]args){
//具体主题, 即被代理类
Subject subject=new RealSubject();
//代理实例的处理Handler
InvocationHandler handler=new SubjectHandler(subject);
//当前加载器
ClassLoader cl=subject.getClass().getClassLoader();
//动态代理
Subject proxy=(Subject)Proxy.newProxyInstance(cl, subject.getClass().
getInterfaces(), handler);
//执行具体主题角色方法
proxy.request();
}

动态代理很容易实现通用的代理类, 只要在InvocationHandler的invoke方法中读取持久化数据即可实现, 而且还能实现动态切入的效果, 这也是AOP(Aspect Oriented Programming)编程理念。

107:使用反射增加装饰模式的普适性

装饰模式(Decorator Pattern)的定义是“动态地给一个对象添加一些额外的职责。就增加功能来说, 装饰模式相比于生成子类更为灵活”。
示例:

interface Animal{
public void doStuff();
}
//老鼠是一种动物
class Rat implements Animal{
public void doStuff(){
System.out.println("Jerry will play with Tom.");
}
}

定义装饰类:

Object obj=null;
//设置包装条件
if(Modifier.isPublic(m.getModifiers())){
obj=m.invoke(clz.newInstance(), args);
}
animal.doStuff();
return obj;
}
};
//当前加载器
ClassLoader cl=getClass().getClassLoader();
//动态代理, 由Handler决定如何包装
Feature proxy=(Feature)Proxy.newProxyInstance(cl, clz.
getInterfaces(), handler);
proxy.load();
}
}

包装动作:

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(){
//具体包装行为
public Object invoke(Object p, Method m, Object[]args)throwsThrowable{
Object obj=null;
//设置包装条件
if(Modifier.isPublic(m.getModifiers())){
obj=m.invoke(clz.newInstance(), args);
}
animal.doStuff();
return obj;
}
};
//当前加载器
ClassLoader cl=getClass().getClassLoader();
//动态代理, 由Handler决定如何包装
Feature proxy=(Feature)Proxy.newProxyInstance(cl, clz.
getInterfaces(), handler);
proxy.load();
}
}

使用:

ublic static void main(String[]args)throws Exception{
//定义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{
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开始
&&Modifer.isPublic(m.getModifers())//公开方法
&&m.getReturnType().equals(Void.TYPE)//返回值是void
&&!m.isVarArgs()//输入参数为空
&&!Modifer.isAbstract(m.getModifers());//不能是抽象方法
}
}



public class UserPopulator extends AbsPopulator{
public void initUser(){
/*初始化用户表, 如创建、加载数据等*/
}
public void initPassword(){
/*初始化密码*/
}
public void initJobs(){
/*初始化工作任务*/
}
}

UserPopulator类中的方法只要符合基本方法鉴别器条件即会被模板方法调用, 方法的数据量也不再受父类的约束, 实现了子类灵活定义基本方法、父类批量调用的功能, 并且缩减了子类的代码量。

这其实跟Junit必须以test开头写方法名的方式类似。

109:不需要太多关注反射效率

反射的效率相对于正常的代码执行确实低很多(经过测试, 相差15倍左右), 但是它是一个非常有效的运行期工具类, 只要代码结构清晰、可读性好那就先开发起来, 等到进行性能测试时证明此处性能确实有问题时再修改也不迟(一般情况下反射并不是性能的终极杀手, 而代码结构混乱、可读性差则很可能会埋下性能隐患)。
例子:在运行期获得泛型类的泛型类型:

class Utils{
//获得一个泛型类的实际泛型类型
public static< T>Class< T>getGenricClassType(Class clz){
Type type=clz.getGenericSuperclass();
if(type instanceof ParameterizedType){
ParameterizedType pt=(ParameterizedType)type;
Type[]types=pt.getActualTypeArguments();
if(types.length>0&&types[0]instanceof Class){
//若有多个泛型参数, 依据位置索引返回
return(Class)types[0];
}
}
return(Class)Object.class;
}
}

Java的泛型类型只存在于编译期, 那为什么这个工具类可以取得运行期的泛型类型呢?那是因为该工具只支持继承的泛型类, 如果是在Java编译时已经确定了泛型类的类型参数。例如:

abstract class BaseDao< T>{
//获得T的运行期类型
private Class< T>clz=Utils.getGenricClassType(getClass());
//根据主键获得一条记录
public void get(long id){
session.get(clz, id);
}
}
//操作user表
class UserDao extends BaseDao< String>{
}

BaseDao和UserDao是ORM中的常客, BaseDao实现对数据库的基本操作, 比如增删改查, 而UserDao则是一个比较具体的数据库操作, 其作用是对User表进行操作, 如果BaseDao能够提供足够多的基本方法, 那些与UserDao类似的BaseDao子类就可以省去大量的开发工作, 但Hibernate 持久层的session对象(特指Hibernate Session)需要明确一个具体的类型才能操作, 最好的办法就是父类泛型化, 子类明确泛型参数, 然后通过反射读取相应的类型即可, 于是就有了我们代码中的clz变量:通过反射获得泛型类型。

对于反射效率问题, 不要做任何的提前优化和预期, 这基本上是杞人忧天, 很少有项目是因为反射问题引起系统效率故障的(除非是拷贝工的垃圾代码)
反射效率低是个真命题,但因为这一点而不使用它就是个假命题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值