8条枚举与注解技巧,提升代码质量与设计美学

简介: 8条枚举与注解技巧,提升代码质量与设计美学

8条枚举与注解技巧,提升代码质量与设计美学

Java支持两种特殊用途的引用类型:

  1. 类实现的枚举类型
  2. 接口实现的注解类型

枚举与注解作为Java语言的重要特性,如同艺术家手中的画笔和调色板,赋予代码独特的语义与生命力

本文基于 Effective Java 枚举与注解 章节总结8条相关技巧(文末附案例地址)

思维导图如下:

image.png

使用枚举取代部分常量

在早起没有枚举时,会使用int、String等定义常量,会存在他们无法关联、无法遍历获取所有常量等缺点

枚举的出现解决这些问题并提升类型安全、代码可读性、扩展性等

使用枚举类时,实际上会去实现抽象类Enum,其有两个字段:name和ordinal,ordinal用于实现Comparable的排序

    public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        private final int ordinal;
    }

枚举类常用来定义常量,该常量可以由多个字段组成

比如以下枚举类,有重量、半径字段,提供构造,其中每个常量(星球)MERCURY、VENUS..由重量、半径字段组成

    public enum Planet {
        MERCURY(3.302e+23, 2.439e6),
        VENUS  (4.869e+24, 6.052e6),
        NEPTUNE(1.024e+26, 2.477e7);

        //重量
        private final double mass;
        //半径
        private final double radius;

        //构造
        Planet(double mass, double radius) {
            this.mass = mass;
            this.radius = radius;
        }
    }

通过方法能够获取常量的信息,如果需要遍历所有常量,枚举还提供values方法

    double mass = Planet.EARTH.mass();

    Planet[] values = Planet.values();
    for (Planet planet : values) {
       System.out.println(planet);
    }

当枚举对应不同类型时可以使用策略枚举,策略枚举生成多种枚举类型提供给外界

使用抽象方法让不同的策略枚举实现具体细节

    enum PayrollDay {
        MONDAY(PayType.WEEKDAY), 
        FRIDAY(PayType.WEEKDAY),
        SATURDAY(PayType.WEEKEND), 
        SUNDAY(PayType.WEEKEND);

        private final PayType payType;

        PayrollDay(PayType payType) {
            this.payType = payType;
        }

        int pay(int minutesWorked, int payRate) {
            return payType.pay(minutesWorked, payRate);
        }

        // 工作日和周末的加班费计算策略枚举
        enum PayType {
            WEEKDAY {
                int overtimePay(int minsWorked, int payRate) {
                    return minsWorked <= MINS_PER_SHIFT ? 0 :
                            (minsWorked - MINS_PER_SHIFT) * payRate / 2;
                }
            },
            WEEKEND {
                int overtimePay(int minsWorked, int payRate) {
                    return minsWorked * payRate / 2;
                }
            };
            //抽象计算加班费由每个枚举常量实现
            abstract int overtimePay(int mins, int payRate);

            private static final int MINS_PER_SHIFT = 8 * 60;

            //计算工资
            int pay(int minsWorked, int payRate) {
                int basePay = minsWorked * payRate;
                //基本工资 + 加班费
                return basePay + overtimePay(minsWorked, payRate);
            }
        }
    }

策略枚举分为两种策略:工作日、周末的加班费

即使后续需要扩展(删减、增加)枚举常量也十分方便

用字段代替ordinal

ordinal用于标识常量在枚举中的顺序,当位置发生改变时其值也会发生改变

如果需要记录顺序最好使用字段记录(不要使用ordinal),避免位置发生改变时ordinal值变动影响业务

    public enum Ensemble {
        DUET(2),
        SOLO(1),
        TRIO(3);

        //使用字段代替ordinal
        private final int numberOfMusicians;

        Ensemble(int size) {
            this.numberOfMusicians = size;
        }
    }

善用EnumSet

位域指的是通过位运算用少量的空间高效的记录集合中存储的常量内容

如果枚举常量都存在一个集合中,可以使用EnumSet取代位域

    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

将枚举常量存入集合

    Text text = new Text();
    EnumSet<Style> enumSet = EnumSet.of(Style.BOLD, Style.ITALIC);
    enumSet.add(Style.UNDERLINE);
    //打印集合内容 [BOLD, ITALIC, UNDERLINE] 
    text.applyStyles(enumSet);

EnumSet.of 方法能够使用位运算记录枚举加入集合

注意它返回的结果并不是不可变对象

    //返回的集合可以继续添加对象
    public static <E extends Enum<E>> EnumSet<E> of(E e1, E e2) {
        EnumSet<E> result = noneOf(e1.getDeclaringClass());
        result.add(e1);
        result.add(e2);
        return result;
    }
    
    //加入集合时位运算
    public boolean add(E e) {
        typeCheck(e);

        long oldElements = elements;
        elements |= (1L << ((Enum<?>)e).ordinal());
        return elements != oldElements;
    }

当枚举常量都在同一集合时,使用EnumSet存储会更简单、高效

善用EnumMap

当需要为不同的枚举进行分组时可以考虑使用EnumMap

定义枚举类型为植物的成熟周期

    class Plant {
        //植物成熟周期
        enum LifeCycle {
            ANNUAL,
            PERENNIAL,
            BIENNIAL
        }
        final String name;
        final LifeCycle lifeCycle;

        Plant(String name, LifeCycle lifeCycle) {
            this.name = name;
            this.lifeCycle = lifeCycle;
        }
    }

不同的植物有不同的成熟周期

    //不同植物成熟周期不同
    Plant[] garden = {
            new Plant("Basil", LifeCycle.ANNUAL),
            new Plant("Carroway", LifeCycle.BIENNIAL),
            new Plant("Dill", LifeCycle.ANNUAL),
            new Plant("Lavendar", LifeCycle.PERENNIAL),
            new Plant("Parsley", LifeCycle.BIENNIAL),
            new Plant("Rosemary", LifeCycle.PERENNIAL),
            new Plant("Rosemary", LifeCycle.PERENNIAL)
    };

如果要以成熟周期进行分组,每个成熟周期下可能有多个植物,又有多个成熟周期

使用stream流将其转化为EnumMap

EnumMap<LifeCycle, Set<Plant>> enumMap = Arrays.stream(garden)
            .collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()));
//{ANNUAL=[Basil, Dill], PERENNIAL=[Lavendar, Rosemary, Rosemary], BIENNIAL=[Parsley, Carroway]}
System.out.println(enumMap);

EnumMap基于序数(ordinal)进行索引下标,这样特点就是高效、线性、空间紧凑,只为枚举服务

public V put(K key, V value) {
    typeCheck(key);
    //基于ordinal线性存储
    int index = key.ordinal();
    Object oldValue = vals[index];
    vals[index] = maskNull(value);
    if (oldValue == null)
        size++;
    return unmaskNull(oldValue);
}

如果要根据枚举类型分组,考虑使用EnumMap

使用接口扩展枚举

如果想像添加新类那样扩展枚举值,枚举虽然无法实现,但可以通过接口来进行扩展

使用接口定义抽象方法由枚举类型实现

//计算
public interface Operation {
    double apply(double x, double y);
}

基础拥有加减乘除的枚举

//基础枚举 加
public enum BasicOperation implements Operation {
    PLUS("+") {
        public double apply(double x, double y) {
            return x + y;
        }
    };

    private final String symbol;

    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
}

可以使用新的枚举类实现接口从而完成新增功能

//扩展枚举 模
public enum ExtendedOperation implements Operation {
    REMAINDER("%") {
        public double apply(double x, double y) {
            return x % y;
        }
    };
    private final String symbol;

    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }

    public static void main(String[] args) {
        double x = Double.parseDouble("9.0");
        double y = Double.parseDouble("1.0");
        //打印:9.000000 + 1.000000 = 10.000000
        test(Arrays.asList(BasicOperation.values()), x, y);
        //打印:9.000000 % 1.000000 = 0.000000
        test(Arrays.asList(ExtendedOperation.values()), x, y);
    }
}

可以使用接口模拟增加新枚举类的方式进行扩展枚举值

标记注解优于命名模式

命名模式指的是在早期开发中,人员想要标记一些代码(类、方法、字段)时,会约定一些标记的方式

比如:需要测试的方法以test开头,后续通过判断方法名是否以test开头来进行判断是否处理标记的代码

这种命名模式一不小心就会出现问题,比如忘记遵守约定

使用注解时,则需要先定义注解,再标记时使用注解,最后编写处理标记的流程

/**
 * 定义注解
 * 只在无参静态方法上使用
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

使用注解标记需要处理的方法

public class Sample {

    //满足无参静态能通过
    @Test
    public static void m1() {
    }

    //抛出异常 不能通过
    @Test
    public static void m2() {
        throw new RuntimeException("Boom");
    }

    //不能通过 不是静态
    @Test
    public void m3() {
    }

    // Test should fail
    @Test
    public static void m4() {
        throw new RuntimeException("Crash");
    }

}

编写处理标记的流程

public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Sample.class;
        for (Method m : testClass.getDeclaredMethods()) {
            //方法有注解则进行处理
            if (m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    //非静态 空指针
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

结果

public static void _6枚举和注解.F注解优于命名模式.Sample.m4() failed: java.lang.RuntimeException: Crash
public static void _6枚举和注解.F注解优于命名模式.Sample.m2() failed: java.lang.RuntimeException: Boom
Invalid @Test: public void _6枚举和注解.F注解优于命名模式.Sample.m3()
Passed: 1, Failed: 3

不要使用约定的命名模式标记代码,而是使用注解处理更靠谱

坚持使用Override注解

@Override 注解用于覆写父类方法或抽象方法

如果想要对方法进行覆写(重写)时,不小心对其进行重载,那么编译器不会报错,反而运行时才出现错误,导致排查浪费时间

需要覆写方法时使用@Override注解,如果发生这种情况编译器会提前报错,提示进行修改

好在现在的IDE工具基本上在覆写时都会自动生成Override注解

善用标记接口

标记接口指的是没有抽象方法,只用于定义类型的接口,如:序列化 Serializable、克隆 Cloneable、随机访问 RandomAccess

标记接口只能由接口继承或类实现,所以只适用于类和接口上

当标记需要在其他地方(方法、字段)上时优先使用标记注解

当使用标记接口时,能够得到编译期间检查类型的好处,尽早暴露问题

比如反序列化 ObjectOutputStream.writeObject(Object) 并没有使用标记接口的好处

如果申明参数为Serializable,传入参数未实现序列化接口则可以在编译期间就提前暴露问题

总结

枚举类继承抽象类Enum,用于定义常量,可由多个字段组成,并提供name\ordinal字段、values遍历方法等,使用枚举代替常量提升类型安全、可读性、扩展性

ordinal用于标识枚举类型顺序,位置变动会发生改变,如果要依赖顺序性,最好使用字段记录

EnumSet 使用位运算,在少量的空间高效的记录存储在同一集合的枚举常量

EnumMap 使用ordinal索引下标,能够更高效、空间紧凑线性的对枚举常量类型进行分组

如果想像新增类一样扩展枚举,可以定义接口类型由新增枚举实现

命名模式需要约定并且容易遗忘,使用标记注解,标记代码,特殊处理

覆写方法始终使用override注解,如果写成重载能够在编译期间暴露问题

标记接口用于定义类型,能够在编译期间检查类型,但只能用于类或接口,若用于方法、字段只能使用标记注解

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

相关文章
|
29天前
|
设计模式 JavaScript 安全
TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等
本文深入探讨了TypeScript性能优化及代码质量提升的重要性、方法与策略,包括合理使用类型注解、减少类型断言、优化模块导入导出、遵循编码规范、加强代码注释等,旨在帮助开发者在保证代码质量的同时,实现高效的性能优化,提升用户体验和项目稳定性。
42 6
|
1月前
|
设计模式 安全 测试技术
Swift代码审查的关键点及最佳实践,涵盖代码风格一致性、变量使用合理性、函数设计、错误处理、性能优化、安全性、代码注释等方面,旨在提升代码质量和项目管理水平
本文深入探讨了Swift代码审查的关键点及最佳实践,涵盖代码风格一致性、变量使用合理性、函数设计、错误处理、性能优化、安全性、代码注释等方面,旨在提升代码质量和项目管理水平。通过实际案例分析,展示了如何有效应用这些原则,确保代码的高可读性、可维护性和可靠性。
29 2
|
7月前
|
存储 安全 Java
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
|
7月前
|
存储 Java API
掌握8条方法设计规则,设计优雅健壮的Java方法
掌握8条方法设计规则,设计优雅健壮的Java方法
|
程序员 C++
代码规范:类的继承与组合
【规则 10-1-2】若在逻辑上 B 是 A 的“一种”(a kind of ),则允许 B 继承 A 的功能和属性。例如男人(Man)是人(Human)的一种,男孩(Boy)是男人的一种。那么类 Man 可以从类 Human 派生,类 Boy 可以从类 Man 派生。
34 0
|
存储
十种高级的代码书写方式,提高代码质量和工作效率
十种高级的代码书写方式,提高代码质量和工作效率
78 0
|
人工智能 自然语言处理 Java
提高代码可读性的秘诀:注释的重要性
A:你写代码怎么连注释都不加? B:老大为什么要加注释? A:你不加注释,你怎么知道我能看懂你的代码? B:遇到问题你找到就可以了啊? A:那你哪天生病了请假了闹情绪了离职了,公司怎么办? B:我现在反正没觉得有什么问题,我对公司也很满意,安心啦! 又是00后整顿职场的一段精彩演绎。不可置否,在实际的软件开发过程中,确实有很多开发人员依然不愿意写注释认为这会浪费时间,或者自认为他们的代码足够清晰,不需要额外的解释。但这种想法too young too simple,代码注释对于项目的质量和效率有着深远的影响,在软件开发中的重要性不容小觑。
《重构2》第十二章-继承
《重构2》第十二章-继承
129 0
|
存储 监控 安全
软件架构的10个质量属性
软件架构的10个质量属性
452 0
|
自然语言处理 算法 JavaScript
重构的秘诀:消除重复,清晰意图
  11年前有幸阅读了《重构——改善既有代码的设计》第一版,当时是一口气读完的,书中的内容直接惊艳到我了。   今年读了该书的第二版,再次震撼到我了,并且这次的示例代码用的JavaScript,让我更有亲切感。   全书共有12章,前面5章是在讲解重构的原则、测试、代码的坏味道等内容,后面7章是各种经验和实践,全书的精髓所在。