改善java程序之注解和枚举

枚举改变了常量的声明方式,注解耦合了数据和代码

83 使用枚举常量
常量声明方式:类常量、接口常量、枚举常量
enum Season{
Spring,Summer,Autumn,Winter;
}
JLS(Java Language Specification,java语言规范)提倡枚举项全部大写,字母之间用下划线分隔。
枚举的优点:
(1)枚举常量更简单
把Season枚举翻写成接口常量
interface Season{
int Spring = 0;
int Summer = 1;
int Autumn = 2;
int Winter = 3;
}
枚举常量只需要定义每个枚举项,不需要定义枚举值,而接口常量(或类常量)则必须定义值,虽然两者被引用的方式相同(都是“类名.属性”,如Season.Spring),但是枚举表示的是一个枚举项,字面含义是春天,而接口常量却是一个int类型,虽然其字面意思也是春天,但在运算中我们势必要关注其int值。
(2)枚举常量属于稳态型
例如使用switch语句判断是哪一个常量时,我们得对输入值进行检查,确定是否越界,如果常量非常庞大,校验输入就成了一件非常麻烦的事情,如果我们校验的不严格,虽然编译可以通过,但运行期就会产生无法预知的后果;枚举常量可以避免校验问题:
public void describe(Season s){
switch(s){
case Summer:
...
break;
case Winter:
...
break;
}
}
不用校验,已经限定了是Season枚举,所以只能是Season类的四个实例,在编译期间限定类型,不允许发生越界的情况。
(3)枚举具有内置方法
每个枚举都是java.lang.Enum的子类,该类提供了诸如获得排序值的ordinal方法、compareTo比较方法等。
例列出所有的季节常量,接口常量或类常量可以通过反射来实现,枚举则非常简单:
public tatic void main(String[] args){
for(Season s : Season.values())
System.out.println(s);
}
(4)枚举可以自定义方法
枚举常量不仅可以定义静态方法,还可以定义费静态方法,而且还能够从根本上杜绝常量类被实例化。
enum Season{
Spring,Summer,Autumn,Winter;
//最舒服的季节
public static Season getComfortableSeason(){
return Spring;
}
}
public static void main(String[] args){
System.out.println("The most comfortable season is "+Season.getComfortableSeason);
}
虽然枚举常量在很多方面比接口常量和类常量好用,但有一点它是比不上接口常量和类常量的,那就是继承,枚举类型是不能有继承的,也就是说一个枚举常量定义完毕后,除非修改重构,否则无法做扩展。

84 使用构造函数协助描述枚举项
枚举描述:通过枚举的构造函数,声明每个枚举项(也就是枚举的实例)必须具有的属性和行为,这是对枚举项的描述或补充,目的是使枚举项表述的意义更加清晰准确。
enum Season{
Spring("春"), Summer("夏"), Autumn("秋"), Winter("冬");
private String desc;
Season(String _desc){
desc = _desc;
}
//获得枚举描述
public String getDesc(){
return desc;
}
}
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;
}
public String getName(){
return name;
}
public String getLifetime(){
return lifeTime;
}
public String getScope(){
return scope;
}
}

85 小心switch带来的空值异常
使用枚举定义常量时,会伴有大量的switch语句判断,目的是为每个枚举项解释其行为。
public static void doSports(Season season){
switch(season){
case Spring:System.out.println("春天放风筝");break;
.
.
.
default:System.out.println("输入错误");break;
}
}
若doSports(null)会抛出空指针异常。
switch语句只能判断byte/short/char/int(JDK7之后允许使用String类型),java编译器在编译时,编译器判断出switch语句后的参数是枚举类型,然后就会根据枚举的排序值继续匹配。
public static void doSports(Season season){
switch(season.ordinal()){
case Season.Spring.ordinal(): ...
.
.
.
}
}
switch语句是先计算season变量的排序值,然后与枚举常量的每个排序值进行对比的。解决方法在doSports方法中判断输入参数是否是null即可。

86 在switch的default代码块中增加AssertionError错误
这样运行期马上就会报错,容易查找到错误。
其他方法:修改IDE工具,以Eclipse为例,可以把Java-->Compiler-->Errors/Warnings中的“Enum type constant not covered on 'switch'”设置为Error级别,如果不判断所有的枚举项就不能通过编译。

87 使用valueOf前必须进行校验
valueOf方法会把一个String类型的名称转变为枚举项,也就是在枚举项中查找出字面值与该参数相等的枚举项。
从String转换为枚举类型可能存在转换不成功的情况,比如没有匹配到指定值,报无效参数异常java.lang.IllegalArgumentException,例如summer(小写s)无法转换为Season枚举。
valueOf方法的源代码如下:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, SString name){
//通过反射,从变量列表中查找
T result = enumType.enumConstantDirectory().get(name);
if(result != null)
return result;
if(name == null)
throw new NullPointException("Name is null");
//最后抛出无效参数异常
throw new IllegalArgumentException("No enumconst "+enumType+"."+name);
}
valueOf方法先通过反射从枚举类的常量声明中查找,若找到就直接返回,若找不到则抛出无效canshuyichang 。
解决方法:
(1)使用try...catch捕获异常
try{
Season s = Season.valueOf(name);
//有该枚举项时的处理
System.out.println(s);
}catch(Exception e){
System.out.println("无相关枚举项");
}
(2)扩展枚举类
由于Enum类定义的方法基本上都是final类型的,所以不希望被覆写,那我们可以学习String和List,通过增加一个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 用枚举实现工厂方法模式更简洁
工厂方法模式(Factory Method Pattern)是“创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其子类”。
//抽象产品
interface Car{};
//具体产品类
class FordCar implements Car{};
//具体产品类
class BuickCar implements Car{};
//工厂类
class CarFactory{
//生产汽车
public static Car createCar(Class<? extends Car> c){
try{
return (Car)c.newInstance();
}catch(Exception e){
e.printStackTrace();
}
return null;
}
}
public static void main(String[] args){
//生产车辆
Car car = CarFactroy.createCar(FordCar.class);
}
枚举实现工厂方法模式有两种方法:
(1)枚举非静态方法实现工厂方法模式
enum CarFactory{
//定义工厂类能生产汽车的类型
FordCar,BuickCar;
//生产汽车
public Car create(){
switch(this){
case FordCar: return new FordCar();
case BuickCar: return new BuickCar();
default: throw new AssertionError("无效参数");
}
}
}
create是一个非静态方法,也就是只有通过FordCar、BuickCar枚举项才能访问:
public static void main(String[] args){
//生产汽车
Car car = CarFactory.BuickCar.create();
}
(2)通过抽象方法生成产品
枚举类型虽然不能继承,但是可以用abstract修饰其方法,此时就表示该枚举是一个抽象枚举,需要每个枚举项自行实现该方法,也就是说枚举项的类型是该枚举的一个子类。
enum CarFactory{
FordCar{
public Car create(){
return new FordCar();
}
},
BuickCar{
public Car create(){
return new BuickCar();
}
};
//抽象生产方法
public abstract Car create();
}
首先定义一个抽象制造方法create,然后每个枚举项自行实现。这种方式编译后会产生两个CarFactory的匿名子类,因为每个枚举项都要实现抽象create方法。
使用枚举类型的工厂方法模式有以下三个优点:
(1)避免错误调用的发生
一般工厂方法模式中的生产方法(也就是createCar方法)可以接收三种类型的参数:类型参数、String参数(生产方法中怕暖String参数hi需要生产什么产品)、int参数(根据int值判断需要生产什么类型的产品),这三种参数都是宽泛的数据类型,很容易产生错误(比如边界问题,null值问题),而且出现这类错误编译器还不会报警。
(2)性能好,使用便捷
枚举类型的计算是以int类型的计算为基础的,这是最基本的操作,心梗当然会快。
(3)降低类间耦合
不管生产方法接收的是Class、String还是int的参数,都会成为客户端类的负担,这些类并不是客户端需要的,而是因为工厂方法的限制必须输入的,例如Class参数,对客户端main方法来说,它需要传递一个FordCar.class参数才能生产一辆福特汽车,除了在create方法中传递该参数外,业务类不需要改Car的实现类。这严重违背了迪米特原则(Law of Demeter,简称LoD),也就是最少知识原则:一个对象应该对其他对象有最少的了解。
而枚举类型的工厂方法就没有这种问题,它只需要依赖工厂类就可以生产一辆符合接口的汽车,完全可以无视具体汽车类的存在。

89 枚举项的数量限制在64个以内
Java提供了两个枚举集合:EnumSet和EnumMap,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示key值必须是某一枚举的枚举项,由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其他Set和Map要高。
EnumSet对不同的枚举数量有不同的处理方式。
EnumSet<T> es = EnumSet.allOf(T.class);
es.size();
es.getClass();
java.util.RegularEnumSet(size小于等于64)
java.util.JumboEnumSet(size大于64)
allOf方法源代码如下:
public static <E extends Enum<E>> allOf(Class<E> elementType){
//生成一个空EnumSet
EnumSet[] 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);
}
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E>{
//记录所有枚举排序号,注意是long型
private long elements = 0L;
//构造函数
RegularEnumSet(Class<E> elementType,Enumm[] universe){
super(elementtType,universe);
}
//加入所有元素
void addAll(){
if(universe.length != 0 )
elements = -1L >>> -universe.length;
}
}
枚举项的排序值ordinal是从0、1、2...依次递增的,没有重号,没有跳号,RegularEnumSet就是利用这一点把每个枚举项的ordinal映射到一个long类型的每个位上的,addAll方法额elements元素使用了无符号右移操作,并且操作数是负值,位移也是负值这表示是负数(符号位是1)的“无符号左移”:符号位为0,并补充低位,简单地说,Java把一个不多于64个枚举项的枚举映射到了一个long类型变量上。
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数组容纳更多的枚举项。
RegularEnumSet是把每个枚举项编码映射到一个long类型数字的 每个位上,JumboEnumSet是先按照64个一组进行拆分,然后每个组在映射到一个long类型数字的每个位上。

90 小心注解继承
注解(Annotation)的目的是在不影响代码语义的情况下增强代码的可读性,并且不改变代码的执行逻辑。
在注解上加了@Inherited注解,它表示的是只要把注解@Xxxx加到父类上,它的所有子类都会自动从父类继承@Xxxx注解,不需要要显式声明。
采用@Inherited元注解有利有弊利的地方是一个注解只要标注到父类,所有的子类都会自动具有与父类相同的注解,整齐、统一而且便于管理,弊的地方是单单阅读子类代码,无从知道为何逻辑会被改变,因为子类没有显示标注该注解。总体上说,使用@Inherited元注解的弊大于利,特别是一个类的继承层次较深时,如果注解较多,则很难判断出是哪个注解对子类产生了逻辑劫持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值