【编写高质量代码:改善Java程序的151个建议】第6章:枚举和注解___建议83~92

枚举和注解都是在Java1.5中引入的,枚举改变了常量的声明方式,注解耦合了数据和代码。 

建议83:推荐使用枚举定义常量

常量声明是每一个项目都不可或缺的,在Java1.5之前,我们只有两种方式的声明:类常量和接口常量。不过,在1.5版本以后有了改进,即新增了一种常量声明方式:枚举声明常量,看如下代码:

enum Season {
    SPRING, SUMMER, AUTUMN, WINTER;
}

提倡枚举项全部大写,字母之间用下划线分割,这也是从常量的角度考虑的。

那么枚举常量与我们经常使用的类常量和静态常量相比有什么优势?问得好,枚举的优点主要表现在四个方面:

1、枚举常量简单

2、枚举常量属于稳态型

    public void describe(int s) {
        // s变量不能超越边界,校验条件
        if (s >= 0 && s < 4) {
            switch (s) {
            case Season.SPRING:
                System.out.println("this is spring");
                break;
            case Season.SUMMER:
                System.out.println("this is summer");
                break;
                ......
            }
        }
    }

对输入值的检查很吃力。

    public void describe(Season s){
        switch(s){
        case Spring:
            System.out.println("this is "+Season.Spring);
            break;
        case Summer:
            System.out.println("this is summer"+Season.Summer);
            break;
                  ......
        }
    }

不用校验,已经限定了是Season枚举。

3、枚举具有内置方法

public void query() {
    for (Season s : Season.values()) {
         System.out.println(s);
    }
}

通过values方法获得所有的枚举项。

4、枚举可以自定义方法

关键是枚举常量不仅可以定义静态方法,还可以定义非静态方法。

虽然枚举在很多方面比接口常量和类常量好用,但是有一点它是比不上接口常量和类常量的,那就是继承,枚举类型是不能继承的,也就是说一个枚举常量定义完毕后,除非修改重构,否则无法做扩展,而接口常量和类常量则可以通过继承进行扩展。但是,一般常量在项目构建时就定义完毕了,很少会出现必须通过扩展才能实现业务逻辑的场景。

注意:在项目中推荐使用枚举常量代替接口常量或类常量

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

枚举描述:通过枚举的构造函数,声明每个枚举项必须具有的属性和行为,这是对枚举项的描述和补充。

enum Role {
    Admin("管理员", new LifeTime(), new Scope()), User("普通用户", new LifeTime(), new Scope());
    private String name;
    private LifeTime lifeTime;
    private Scope scope;
    /* setter和getter方法略 */

    Role(String _name, LifeTime _lifeTime, Scope _scope) {
        name = _name;
        lifeTime = _lifeTime;
        scope = _scope;
    }

}

class LifeTime {
}
class Scope {
}

这是一个角色定义类,描述了两个角色:管理员和普通用户,同时它还通过构造函数对这两个角色进行了描述:

1、name:表示的是该角色的中文名称

2、lifeTime:表示的是该角色的生命周期,也就是多长时间该角色失效

3、scope:表示的该角色的权限范围

这样一个描述可以使开发者对Admin和User两个常量有一个立体多维度的认知,有名称,有周期,还有范围,而且还可以在程序中方便的获得此类属性。所以,推荐大家在枚举定义中为每个枚举项定义描述,特别是在大规模的项目开发中,大量的常量定义使用枚举项描述比在接口常量或类常量中增加注释的方式友好的多,简洁的多。

建议85:小心switch带来的空指针异常

使用枚举定义常量时。会伴有大量switch语句判断,目的是为了每个枚举项解释其行为,例如这样一个方法: 

public static void doSports(Season season) {
    switch (season) {
        case Spring:
            System.out.println("春天放风筝");
            break;
        case Summer:
            System.out.println("夏天游泳");
            break;
        case Autumn:
            System.out.println("秋天是收获的季节");
            break;
        case Winter:
            System.out.println("冬天滑冰");
            break;
        default:
            System.out.println("输出错误");
            break;
    }
}
public static void main(String[] args) {
    doSports(null);
}
Exception in thread "main" java.lang.NullPointerException
    at com.book.study85.Client85.doSports(Client85.java:8)
    at com.book.study85.Client85.main(Client85.java:28)

输入null时应该default的啊,为什么空指针异常呢?

目前Java中的switch语句只能判断byte、short、char、int类型(JDk7允许使用String类型),这是Java编译器的限制。问题是为什么枚举类型也可以跟在switch后面呢?

因为编译时,编译器判断出switch语句后跟的参数是枚举类型,然后就会根据枚举的排序值继续匹配,也就是或上面的代码与以下代码相同: 

public static void doSports(Season season) {
    switch (season.ordinal()) {//枚举的排序值
        case season.Spring.ordinal():
            System.out.println("春天放风筝");
            break;
        case season.Summer.ordinal():
            System.out.println("夏天游泳");
            break;
            //......
    }
}

switch语句是先计算season变量的排序值,然后与枚举常量的每个排序值进行对比,在我们的例子中season是null,无法执行ordinal()方法,于是就报空指针异常了。问题清楚了,解决很简单,在doSports方法中判断输入参数是否为null即可。

建议86:在switch的default代码块中增加AssertError错误

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

我们知道每个枚举项都是java.lang.Enum的子类,都可以访问Enum类提供的方法,比如hashCode、name、valueOf等,其中valueOf方法会把一个String类型的名称转换为枚举项,也就是在枚举项中查找出字面值与参数相等的枚举项。虽然这个方法简单,但是JDK却做了一个对于开发人员来说并不简单的处理,我们来看代码:  

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        for (String name:params){
            Season s = Season.valueOf(name);
            if(null!=s){
                System.out.println(s);
            }else {
                System.out.println("无相关枚举项");
            }
        }
    }
}

看着没问题啊,summer不在Season里,就输出无相关枚举项就完事了嘛。。。

60426cd62c7ed0439a3981f9862ebb73a0f.jpg

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

解决此问题的方法:

1、抛异常

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        try{
            for (String name:params){
                Season s = Season.valueOf(name);
                System.out.println(s);
            }
        }catch (IllegalArgumentException e){
            e.printStackTrace();
            System.out.println("无相关枚举项");
        }

    }
}

2、扩展枚举类:

枚举中是可以定义方法的,那就在枚举项中自定义一个contains方法就可以。

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client15 {
    enum Season{
        SPRING,SUMMER,AUTUMN,WINTER;
        public static boolean contains(String name){
            for (Season s:Season.values()){
                if(s.name().equals(name)){
                    return true;
                }
            }
            return false;
        }
    }
    public static void main(String[] args) {
        List<String> params = Arrays.asList("SPRING","summer");
        for (String name:params){
            if(Season.contains(name)){
                Season s = Season.valueOf(name);
                System.out.println(s);
            }else {
                System.out.println("无相关枚举项");
            }
        }
    }
}

个人感觉第二种方法更好一些!

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

工厂方法模式是“创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其它子类”。工厂方法模式在我们的开发中经常会用到。下面以汽车制造为例,看看一般的工厂方法模式是如何实现的。

//抽象产品
interface Car{
    
}
//具体产品类
class FordCar implements Car{
    
}
//具体产品类
class BuickCar implements Car{
    
}
//工厂类
class CarFactory{
    //生产汽车
    public static Car createCar(Class<? extends Car> c){
        try {
            return c.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

这是最原始的工厂方法模式,有两个产品:福特汽车和别克汽车,然后通过工厂方法模式来生产。有了工厂方法模式,我们就不用关心一辆车具体是怎么生成的了,只要告诉工厂" 给我生产一辆福特汽车 "就可以了,下面是产出一辆福特汽车时客户端的代码: 

public static void main(String[] args) {
    //生产车辆
    Car car = CarFactory.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、避免错误调用的发生:一般工厂方法模式中的生产方法,可以接收三种类型的参数:类型参数、String参数、int参数,这三种参数都是宽泛的数据类型,很容易发生错误(比如边界问题、null值问题),而且出现这类错误编辑器还不会报警,例如:

public static void main(String[] args) {
    // 生产车辆
    Car car = CarFactory.createCar(Car.class);
}

Car是一个接口,完全合乎createCar的要求,所以它在编译时不会报任何错误,但一运行就会报出InstantiationException异常,而使用枚举类型的工厂方法模式就不存在该问题了,不需要传递任何参数,只需要选择好生产什么类型的产品即可。

2、性能好,使用简洁:枚举类型的计算时以int类型的计算为基础,这是最基本的操作,性能当然会快,至于使用便捷,注意看客户端的调用,代码的字面意思是“汽车工厂,我们要一辆别克汽车,赶快生产”。

3、降低类间耦合:不管生产方法接收的是class、string还是int的参数,都会成为客户端类的负担,这些类并不是客户端需要的,而是因为工厂方法的限制必须输入,例如class参数,对客户端main方法来说,它需要传递一个fordCar.class参数才能生产一台福特汽车,除了在create方法中传递参数外,业务类不需要改car的实现类。这严重违反了迪克特原则,也就是最少知识原则:一个对象该对其它对象有最少的了解。

而枚举类型的工厂方法就没有这种问题,它只需要依赖工厂类就可以生产一辆符合接口的汽车,完全可以无视具体汽车类的存在。

建议89:枚举类的数量限制在64个以内

为了更好地使用枚举,Java提供了两个枚举集合:EnumSet和EnumMap,这两个集合使用的方法都比较简单,EnumSet表示其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项,由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其它Set和Map要高。

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

建议90:小心继承注解

Java从1.5版本开始引入注解(Annotation),@Inheruted,它表示一个注解是否可以自动继承。

浅谈java注解

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

注解的写法和接口很相似,都采用关键字interface,而且都不能有实现代码,常量定义默认都是public static final类型的,它们的主要不同点是:注解要在interface前加@字符,而且不能继承,不能实现。

我们举例说明一下,以访问权限列表为例:

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

public enum CommonIdentifier implements Identifier {
    // 权限级别
    Reader, Author, Admin;
    @Override
    public boolean identify() {
        return false;
    }
}
package OSChina.reflect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

@Access(level=CommonIdentifier.Author)
public class Foo {
}
package OSChina.reflect;

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

0b1c0904e1ec26eb288db4e67d48ae2ea18.jpg

看看上面的代码,简单易懂,所有的开发人员只要增加注解即可解决访问控制问题。

建议92:注意@override不同版本的区别

@Override注解用于方法的覆写上,它是在编译器有效,也就是Java编译器在编译时会根据注解检查方法是否真的是覆写,如果不是就报错,拒绝编译。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
编写高质量代码改善java程序151建议》内容简介:在通往“java技术殿堂”的路上,本书将为你指点迷津!内容全部由java编码的最佳实践组成,从语法、程序设计和架构、工具和框架、编码风格和编程思想等五大方面,对java程序员遇到的各种棘手的疑难问题给出了经验性的解决方案,为java程序员如何编写高质量java代码提出了151条极为宝贵的建议。对于每一个问题,不仅以建议的方式从正反两面给出了被实践证明为十分优秀的解决方案和非常糟糕的解决方案,而且还分析了问题产生的根源,犹如醍醐灌顶,让人豁然开朗。 《编写高质量代码改善java程序151建议》一共12,第1~3针对java语法本身提出了51条建议,例如覆写变长方法时应该注意哪些事项、final修饰的常量不要在运行期修改、匿名类的构造函数特殊在什么地方等;第4~9重点针对jdk api的使用提出了80条建议,例如字符串的拼接方法该如何选择、枚举使用时有哪些注意事项、出现nullpointerexception该如何处理、泛型的多重界限该如何使用、多线程编程如何预防死锁,等等;第10~12针对程序性能、开源的工具和框架、编码风格和编程思想等方面提出了20条建议。 《编写高质量代码改善java程序151建议》针对每个问题所设计的应用场景都非常典型,给出的建议也都与实践紧密结合。书中的每一条建议都可能在你的下一行代码、下一个应用或下一个项目中崭露头角,建议你将此书搁置在手边,随时查阅,一定能使你的学习和开发工作事半功倍。 《编写高质量代码:改善javascript程序的188个建议》内容简介:本书是web前端工程师进阶修炼的必读之作,将为你通往“javascript技术殿堂”指点迷津!内容全部由编写高质量javascript代码的最佳实践组成,从基本语法、应用架构、工具框架、编码风格、编程思想等5大方面对web前端工程师遇到的疑难问题给出了经验性的解决方案,为web前端工程师如何编写高质量javascript代码提供了188条极为宝贵的建议。对于每一个问题,不仅以建议的方式给出了被实践证明为十分优秀的解决方案,而且还给出了经常被误用或被错误理解的不好的解决方案,从正反两个方面进行了分析和对比,犹如醍醐灌顶,让人豁然开朗。 《编写高质量代码:改善javascript程序的188个建议》针对每个问题所设计的应用场景都非常典型,给出的建议也都与实践紧密结合。书中的每一条建议都可能在你的下一行代码、下一个应用或下一个项目中被用到,建议你将此书放置在手边,随时查阅,一定能使你的学习和开发工作事半功倍。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值