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

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

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

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

建议85:当心switch带来的空指针异常编辑器

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

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

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

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

建议90:当心继承注解this

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

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

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

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

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

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

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

一、枚举常量简单

二、枚举常量属于稳态型

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枚举。

三、枚举具备内置方法

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

经过values方法得到全部的枚举项。

四、枚举能够自定义方法

关键是枚举常量不只能够定义静态方法,还能够定义非静态方法。

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

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

建议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 {
}

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

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

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

三、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里,就输出无相关枚举项就完事了嘛。。。

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

解决此问题的方法:

一、抛异常

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("无相关枚举项");
        }

    }
}

二、扩展枚举类:

枚举中是能够定义方法的,那就在枚举项中自定义一个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);
}

这就是咱们常用的工厂方法模式,但常用并不表明就是最优秀、最简洁的。此处再介绍一种经过枚举实现工厂方法模式的方案,谁优谁劣你自行评价。枚举实现工厂方法模式有两种方法:

一、枚举非静态方法实现工厂方法模式

咱们知道每一个枚举项都是该枚举的实例对象,那是否是定义一个方法能够生成每一个枚举项对应产品来实现此模式呢?代码以下:

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

二、经过抽象方法生成产品

枚举类型虽然不能继承,可是能够用abstract修饰其方法,此时就表示该枚举是一个抽象枚举,须要每一个枚举项自行实现该方法,也就是说枚举项的类型是该枚举的一个子类,咱们来看代码:

enum CarFactory {
    // 定义生产类能生产汽车的类型
    FordCar{
        public Car create(){
            return new FordCar();
        }
    },
    BuickCar{
        public Car create(){
            return new BuickCar();
        }
    };
    //抽象生产方法
    public abstract Car create();
}

首先定义一个抽象制造方法create,而后每一个枚举项自行实现,这种方式编译后会产生CarFactory的匿名子类,由于每一个枚举项都要实现create抽象方法。客户端调用与上一个方案相同,再也不赘述。

你们可能会问,为何要使用枚举类型的工厂方法模式呢?那是由于使用枚举类型的工厂方法模式有如下三个优势:

一、避免错误调用的发生:通常工厂方法模式中的生产方法,能够接收三种类型的参数:类型参数、String参数、int参数,这三种参数都是宽泛的数据类型,很容易发生错误(好比边界问题、null值问题),并且出现这类错误编辑器还不会报警,例如:

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

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

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

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

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

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

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

Java集合之EnumSet

注意:枚举项数量不要超过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编译器在编译时会根据注解检查方法是否真的是覆写,若是不是就报错,拒绝编译。

 

编写高质量代码:改善Java程序的151个建议@目录

相关文章
相关标签/搜索