工厂方法模式(Factory Method Pattern)是" 建立对象的接口,让子类决定实例化哪个类,并使一个类的实例化延迟到其它子类"。工厂方法模式在咱们的开发中常常会用到。下面以汽车制造为例,看看通常的工厂方法模式是如何实现的,代码以下:java
1 //抽象产品 2 interface Car{ 3 4 } 5 //具体产品类 6 class FordCar implements Car{ 7 8 } 9 //具体产品类 10 class BuickCar implements Car{ 11 12 } 13 //工厂类 14 class CarFactory{ 15 //生产汽车 16 public static Car createCar(Class<? extends Car> c){ 17 try { 18 return c.newInstance(); 19 } catch (InstantiationException | IllegalAccessException e) { 20 e.printStackTrace(); 21 } 22 return null; 23 } 24 }
这是最原始的工厂方法模式,有两个产品:福特汽车和别克汽车,而后经过工厂方法模式来生产。有了工厂方法模式,咱们就不用关心一辆车具体是怎么生成的了,只要告诉工厂" 给我生产一辆福特汽车 "就能够了,下面是产出一辆福特汽车时客户端的代码: 程序员
public static void main(String[] args) { //生产车辆 Car car = CarFactory.createCar(FordCar.class); }
这就是咱们常用的工厂方法模式,但常用并不表明就是最优秀、最简洁的。此处再介绍一种经过枚举实现工厂方法模式的方案,谁优谁劣你自行评价。枚举实现工厂方法模式有两种方法:数组
(1)、枚举非静态方法实现工厂方法模式ide
咱们知道每一个枚举项都是该枚举的实例对象,那是否是定义一个方法能够生成每一个枚举项对应产品来实现此模式呢?代码以下:函数
1 enum CarFactory { 2 // 定义生产类能生产汽车的类型 3 FordCar, BuickCar; 4 // 生产汽车 5 public Car create() { 6 switch (this) { 7 case FordCar: 8 return new FordCar(); 9 case BuickCar: 10 return new BuickCar(); 11 default: 12 throw new AssertionError("无效参数"); 13 } 14 } 15 16 }
create是一个非静态方法,也就是只有经过FordCar、BuickCar枚举项才能访问。采用这种方式实现工厂方法模式时,客户端要生产一辆汽车就很简单了,代码以下: 性能
public static void main(String[] args) { // 生产车辆 Car car = CarFactory.BuickCar.create(); }
(2)、经过抽象方法生成产品ui
枚举类型虽然不能继承,可是能够用abstract修饰其方法,此时就表示该枚举是一个抽象枚举,须要每一个枚举项自行实现该方法,也就是说枚举项的类型是该枚举的一个子类,咱们俩看代码:this
1 enum CarFactory { 2 // 定义生产类能生产汽车的类型 3 FordCar{ 4 public Car create(){ 5 return new FordCar(); 6 } 7 }, 8 BuickCar{ 9 public Car create(){ 10 return new BuickCar(); 11 } 12 }; 13 //抽象生产方法 14 public abstract Car create(); 15 }
首先定义一个抽象制造方法create,而后每一个枚举项自行实现,这种方式编译后会产生CarFactory的匿名子类,由于每一个枚举项都要实现create抽象方法。客户端调用与上一个方案相同,再也不赘述。spa
你们可能会问,为何要使用枚举类型的工厂方法模式呢?那是由于使用枚举类型的工厂方法模式有如下三个优势:设计
public static void main(String[] args) { // 生产车辆 Car car = CarFactory.createCar(Car.class); }
Car是一个接口,彻底合乎createCar的要求,因此它在编译时不会报任何错误,但一运行就会报出InstantiationException异常,而使用枚举类型的工厂方法模式就不存在该问题了,不须要传递任何参数,只须要选择好生产什么类型的产品便可。
而枚举类型的工厂方法就没有这种问题了,它只须要依赖工厂类就能够生产一辆符合接口的汽车,彻底能够无视具体汽车类的存在。
为了更好地使用枚举,Java提供了两个枚举集合:EnumSet和EnumMap,这两个集合使用的方法都比较简单,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项,因为枚举类型的实例数量固定而且有限,相对来讲EnumSet和EnumMap的效率会比其它Set和Map要高。
虽然EnumSet很好用,可是它有一个隐藏的特色,咱们逐步分析。在项目中通常会把枚举用做常量定义,可能会定义很是多的枚举项,而后经过EnumSet访问、遍历,但它对不一样的枚举数量有不一样的处理方式。为了进行对比,咱们定义两个枚举,一个数量等于64,一个是65(大于64便可,为何是64而不是128,512呢,一会解释),代码以下:
1 //普通枚举项,数量等于64 2 enum Const{ 3 A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z, 4 AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ, 5 AAA,BBB,CCC,DDD,EEE,FFF,GGG,HHH,III,JJJ,KKK,LLL 6 } 7 //大枚举,数量超过64 8 enum LargeConst{ 9 A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z, 10 AA,BB,CC,DD,EE,FF,GG,HH,II,JJ,KK,LL,MM,NN,OO,PP,QQ,RR,SS,TT,UU,VV,WW,XX,YY,ZZ, 11 AAAA,BBBB,CCCC,DDDD,EEEE,FFFF,GGGG,HHHH,IIII,JJJJ,KKKK,LLLL,MMMM 12 }
Const的枚举项数量是64,LagrgeConst的枚举项数量是65,接下来咱们但愿把这两个枚举转换为EnumSet,而后判断一下它们的class类型是否相同,代码以下:
1 public class Client89 { 2 public static void main(String[] args) { 3 EnumSet<Const> cs = EnumSet.allOf(Const.class); 4 EnumSet<LargeConst> lcs = EnumSet.allOf(LargeConst.class); 5 //打印出枚举数量 6 System.out.println("Const的枚举数量:"+cs.size()); 7 System.out.println("LargeConst的枚举数量:"+lcs.size()); 8 //输出两个EnumSet的class 9 System.out.println(cs.getClass()); 10 System.out.println(lcs.getClass()); 11 } 12 }
程序很简单,如今的问题是:cs和lcs的class类型是否相同?应该相同吧,都是EnumSet类的工厂方法allOf生成的EnumSet类,并且JDK API也没有提示EnumSet有子类。咱们来看看输出结果:
很遗憾,二者不相等。就差一个元素,二者就不相等了?确实如此,这也是咱们重点关注枚举项数量的缘由。先来看看Java是如何处理的,首先跟踪allOf方法,其源码以下:
1 public static <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType) { 2 //生成一个空EnumSet 3 EnumSet<E> result = noneOf(elementType); 4 //加入全部的枚举项 5 result.addAll(); 6 return result; 7 }
allOf经过noneOf方法首先生成了一个EnumSet对象,而后把全部的枚举都加进去,问题可能就出在EnumSet的生成上了,咱们来看看noneOf的源码:
1 public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) { 2 //得到全部的枚举项 3 Enum[] universe = getUniverse(elementType); 4 if (universe == null) 5 throw new ClassCastException(elementType + " not an enum"); 6 //枚举数量小于等于64 7 if (universe.length <= 64) 8 return new RegularEnumSet<>(elementType, universe); 9 else 10 //枚举数量大于64 11 return new JumboEnumSet<>(elementType, universe); 12 }
看到这里,恍然大悟,Java原来是如此处理的:当枚举项数量小于等于64时,建立一个RegularEnumSet实例对象,大于64时则建立一个JumboEnumSet实例对象。
为何要如此处理呢?这还要看看这两个类之间的差别,首先看RegularEnumSet类,源码以下:
1 class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { 2 private static final long serialVersionUID = 3411599620347842686L; 3 /** 4 * Bit vector representation of this set. The 2^k bit indicates the 5 * presence of universe[k] in this set. 6 */ 7 //记录全部的枚举号,注意是long型 8 private long elements = 0L; 9 //构造函数 10 RegularEnumSet(Class<E>elementType, Enum[] universe) { 11 super(elementType, universe); 12 } 13 14 //加入全部元素 15 void addAll() { 16 if (universe.length != 0) 17 elements = -1L >>> -universe.length; 18 } 19 20 //其它代码略 21 }
咱们知道枚举项的排序值ordinal 是从0、一、2......依次递增的,没有重号,没有跳号,RegularEnumSet就是利用这一点把每一个枚举项的ordinal映射到一个long类型的每一个位置上的,注意看addAll方法的elements元素,它使用了无符号右移操做,而且操做数是负值,位移也是负值,这表示是负数(符号位是1)的"无符号左移":符号位为0,并补充低位,简单的说,Java把一个很少于64个枚举项映射到了一个long类型变量上。这才是EnumSet处理的重点,其余的size方法、contains方法等都是根据elements方法等都是根据elements计算出来的。想一想看,一个long类型的数字包含了全部的枚举项,其效率和性能能确定是很是优秀的。
咱们知道long类型是64位的,因此RegularEnumSet类型也就只能负责枚举项的数量不大于64的枚举(这也是咱们以64来举例,而不以128,512举例的缘由),大于64则由JumboEnumSet处理,咱们看它是怎么处理的:
1 class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> { 2 private static final long serialVersionUID = 334349849919042784L; 3 4 /** 5 * Bit vector representation of this set. The ith bit of the jth 6 * element of this array represents the presence of universe[64*j +i] 7 * in this set. 8 */ 9 //映射全部的枚举项 10 private long elements[]; 11 12 // Redundant - maintained for performance 13 private int size = 0; 14 15 JumboEnumSet(Class<E>elementType, Enum[] universe) { 16 super(elementType, universe); 17 //默认长度是枚举项数量除以64再加1 18 elements = new long[(universe.length + 63) >>> 6]; 19 } 20 21 void addAll() { 22 //elements中每一个元素表示64个枚举项 23 for (int i = 0; i < elements.length; i++) 24 elements[i] = -1; 25 elements[elements.length - 1] >>>= -universe.length; 26 size = universe.length; 27 } 28 }
JumboEnumSet类把枚举项按照64个元素一组拆分红了多组,每组都映射到一个long类型的数字上,而后该数组再放置到elements数组中,简单来讲JumboEnumSet类的原理与RegularEnumSet类似,只是JumboEnumSet使用了long数组容纳更多的枚举项。不过,这样的程序看着会不会以为郁闷呢?其实这是由于咱们在开发中不多使用位移操做。你们能够这样理解:RegularEnumSet是把每一个枚举项映射到一个long类型数字的每一个位上,JumboEnumSet是先按照64个一组进行拆分,而后每一个组再映射到一个long类型数字的每一个位上。
从以上的分析可知,EnumSet提供的两个实现都是基本的数字类型操做,其性能确定比其余的Set类型要好的多,特别是Enum的数量少于64的时候,那简直就是飞通常的速度。
注意:枚举项数量不要超过64,不然建议拆分。
Java从1.5版本开始引入注解(Annotation),其目的是在不影响代码语义的状况下加强代码的可读性,而且不改变代码的执行逻辑,对于注解始终有两派争论,正方认为注解有益于数据与代码的耦合,"在有代码的周边集合数据";反方认为注解把代码和数据混淆在一块儿,增长了代码的易变性,消弱了程序的健壮性和稳定性。这些争论暂且搁置,咱们要说的是一个咱们不经常使用的元注解(Meta-Annotation):@Inheruted,它表示一个注解是否能够自动继承,咱们开看它如何使用。
思考一个例子,好比描述鸟类,它有颜色、体型、习性等属性,咱们以颜色为例,定义一个注解来修饰一下,代码以下:
1 import java.lang.annotation.ElementType; 2 import java.lang.annotation.Inherited; 3 import java.lang.annotation.Retention; 4 import java.lang.annotation.RetentionPolicy; 5 import java.lang.annotation.Target; 6 7 @Retention(RetentionPolicy.RUNTIME) 8 @Target(ElementType.TYPE) 9 @Inherited 10 public @interface Desc { 11 enum Color { 12 White, Grayish, Yellow 13 } 14 15 // 默认颜色是白色的 16 Color c() default Color.White; 17 }
该注解Desc前增长了三个注解:Retention表示的是该注解的保留级别,Target表示的是注解能够标注在什么地方,@Inherited表示该注解会被自动继承。注解定义完毕,咱们把它标注在类上,代码以下:
1 @Desc(c = Color.White) 2 abstract class Bird { 3 public abstract Color getColor(); 4 } 5 6 // 麻雀 7 class Sparrow extends Bird { 8 private Color color; 9 10 // 默认是浅灰色 11 public Sparrow() { 12 color = Color.Grayish; 13 } 14 15 // 构造函数定义鸟的颜色 16 public Sparrow(Color _color) { 17 color = _color; 18 } 19 20 @Override 21 public Color getColor() { 22 return color; 23 } 24 } 25 26 // 鸟巢,工厂方法模式 27 enum BirdNest { 28 Sparrow; 29 // 鸟类繁殖 30 public Bird reproduce() { 31 Desc bd = Sparrow.class.getAnnotation(Desc.class); 32 return bd == null ? new Sparrow() : new Sparrow(bd.c()); 33 } 34 }
上面程序声明了一个Bird抽象类,而且标注了Desc注解,描述鸟类的颜色是白色,而后编写一个麻雀Sparrow类,它有两个构造函数,一个是默认的构造函数,也就是咱们常常看到的麻雀是浅灰色的,另一个构造函数是自定义麻雀的颜色,以后又定义了一个鸟巢(工厂方法模式),它是专门负责鸟类繁殖的,它的生产方法reproduce会根据实现类注解信息生成不一样颜色的麻雀。咱们编写一个客户端调用,代码以下:
1 public static void main(String[] args) { 2 Bird bird = BirdNest.Sparrow.reproduce(); 3 Color color = bird.getColor(); 4 System.out.println("Bird's color is :" + color); 5 }
如今问题是这段客户端程序会打印出什么来?由于采用了工厂方法模式,它最主要的问题就是bird变量到底采用了那个构造函数来生成,是无参构造函数仍是有参构造?若是咱们单独看子类Sparrow,它没有被添加任何注释,那工厂方法中的bd变量就应该是null了,应该调用的是无参构造。是否是如此呢?咱们来看运行结果:“Bird‘s Color is White ”;
白色?这是咱们添加到父类Bird上的颜色,为何?这是由于咱们在注解上加了@Inherited注解,它表示的意思是咱们只要把注解@Desc加到父类Bird上,它的全部子类都会从父类继承@Desc注解,不须要显示声明,这与Java的继承有点不一样,若Sparrow类继承了Bird却不用显示声明,只要@Desc注解释可自动继承的便可。
采用@Inherited元注解有利有弊,利的地方是一个注解只要标注到父类,全部的子类都会自动具备父类相同的注解,整齐,统一并且便于管理,弊的地方是单单阅读子类代码,咱们无从知道为什么逻辑会被改变,由于子类没有显示标注该注解。整体上来讲,使用@Inherited元注解弊大于利,特别是一个类的继承层次较深时,若是注解较多,则很难判断出那个注解对子类产生了逻辑劫持。
咱们知道注解的写法和接口很相似,都采用了关键字interface,并且都不能有实现代码,常量定义默认都是public static final 类型的等,它们的主要不一样点是:注解要在interface前加上@字符,并且不能继承,不能实现,这常常会给咱们的开发带来些障碍。
咱们来分析一下ACL(Access Control List,访问控制列表)设计案例,看看如何避免这些障碍,ACL有三个重要元素:
鉴权人是整个ACL的设计核心,咱们从最主要的鉴权人开始,代码以下:
interface Identifier{ //无权访问时的礼貌语 String REFUSE_WORD = "您无权访问"; //鉴权 public boolean identify(); }
这是一个鉴权人接口,定义了一个常量和一个鉴权方法。接下来应该实现该鉴权方法,但问题是咱们的权限级别和鉴权方法之间是紧耦合,若分拆成两个类显得有点啰嗦,怎么办?咱们能够直接顶一个枚举来实现,代码以下:
1 enum CommonIdentifier implements Identifier { 2 // 权限级别 3 Reader, Author, Admin; 4 5 @Override 6 public boolean identify() { 7 return false; 8 } 9 10 }
定义了一个通用鉴权者,使用的是枚举类型,而且实现了鉴权者接口。如今就剩下资源定义了,这很容易定义,资源就是咱们写的类、方法等,以后再经过配置来决定哪些类、方法容许什么级别的访问,这里的问题是:怎么把资源和权限级别关联起来呢?使用XML配置文件?是个方法,但对咱们的示例程序来讲显得太繁重了,若是使用注解会更简洁些,不过这须要咱们首先定义出权限级别的注解,代码以下:
1 @Retention(RetentionPolicy.RUNTIME) 2 @Target(ElementType.TYPE) 3 @interface Access{ 4 //什么级别能够访问,默认是管理员 5 CommonIdentifier level () default CommonIdentifier.Admin; 6 }
该注解释标注在类上面的,而且会保留到运行期。咱们定义一个资源类,代码以下:
@Access(level=CommonIdentifier.Author) class Foo{ }
Foo类只能是做者级别的人访问。场景都定义完毕了,那咱们看看如何模拟ACL实现,代码以下:
1 public static void main(String[] args) { 2 // 初始化商业逻辑 3 Foo b = new Foo(); 4 // 获取注解 5 Access access = b.getClass().getAnnotation(Access.class); 6 // 没有Access注解或者鉴权失败 7 if (null == access || !access.level().identify()) { 8 // 没有Access注解或者鉴权失败 9 System.out.println(access.level().REFUSE_WORD); 10 } 11 }
看看这段代码,简单,易读,并且若是咱们是经过ClassLoader类来解释该注解的,那会使咱们的开发更简洁,全部的开发人员只要增长注解便可解决访问控制问题。注意看加粗代码,access是一个注解类型,咱们想使用Identifier接口的identity鉴权方法和REFUSE_WORD常量,但注解释不能集成的,那怎么办?此处,可经过枚举类型CommonIdentifier从中间作一个委派动做(Delegate),委派?你能够然identity返回一个对象,或者在Identifier上直接定义一个常量对象,那就是“赤裸裸” 的委派了。
@Override注解用于方法的覆写上,它是在编译器有效,也就是Java编译器在编译时会根据注解检查方法是否真的是覆写,若是不是就报错,拒绝编译。该注解能够很大程度地解决咱们的误写问题,好比子类和父类的方法名少写一个字符,或者是数字0和字母O为区分出来等,这基本是每一个程序员都曾将犯过的错误。在代码中加上@Override注解基本上能够杜绝出现此类问题,可是@Override有个版本问题,咱们来看以下代码:
1 interface Foo { 2 public void doSomething(); 3 } 4 5 class FooImpl implements Foo{ 6 @Override 7 public void doSomething() { 8 9 } 10 }
这是一个简单的@Override示例,接口中定义了一个doSomething方法,实现类FooImpl实现此方法,而且在方法前加上了@Override注解。这段代码在Java1.6版本上编译没问题,虽然doSomething方法只是实现了接口的定义,严格来讲并非覆写,但@Override出如今这里可减小代码中出现的错误。
可若是在Java1.5版本上编译此段代码可能会出现错误:
The method doSomeThing() of type FooImpl must override a superclass method
注意,这是个错误,不能继续编译,缘由是Java1.5版本的@Override是严格遵照覆写的定义:子类方法与父类方法必须具备相同的方法名、输出参数、输出参数(容许子类缩小)、访问权限(容许子类扩大),父类必须是一个类,不能是接口,不然不能算是覆写。而这在Java1.6就开放了不少,实现接口的方法也能够加上@Override注解了,能够避免粗枝大叶致使方法名称与接口不一致的状况发生。
在多环境部署应用时,需呀考虑@Override在不一样版本下表明的意义,若是是Java1.6版本的程序移植到1.5版本环境中,就须要删除实现接口方法上的@Override注解。