34 使用枚举类型替代整型常量
有时会遇到这样一些场景:要表示一组固定的常量值。如春夏秋冬、八大行星等等。我们最容易想到的是用几个int类型的常量表示,但是这样有许多问题要考虑:
(1)一个数字没有直观的信息,特别是调试时打印出来用处不大。
(2)数字没有类型区分,容易有类型安全问题。如一个函数本来是接收春夏秋冬的某个,假设分别1 2 3 4表示,这时我们传参为5的话就会出现逻辑错误,但是程序不会报错增加隐患。
(3)不易拓展修改,一旦整型值表示的意义变化就需要较大改动,重新编译所有使用过它的代码。比如之前是1表示春天,现在需要变为0表示春天,则我们需要去程序里把所有的1改成0并重新编译。
不要想着用字符串代替数字,字符串也有上述问题,而且这样硬编码字符串写错了也是完全能编译成功从而导致运行时出错,且难以排查。
所以,这种场景就要用枚举类型了,枚举的好处如下:
(1)Java的枚举很强大,是一个完整的类,可以有属性、构造函数、方法等,还有几个enum类型自带的方法,携带的信息全面,非常方便
(2)利于修改拓展。程序中使用春天这个枚举量都是直接使用SPRING,当春天的表示从1变为0时,只需要将枚举类SPRING枚举量的值改为0就行,不会影响使用SPRING枚举量的其他代码。
(3)枚举极为可靠,前面也说了它是单例模式的最佳实现。反射都不能破坏。因此,我们例举几个实例,这个枚举类就会有几个实例,不会出错。
35 用实例属性代替序数
我们知道枚举类有一些自带的方法,其中就有 ordinal()方法,返回这个枚举常量的序数。即第一个枚举常量的序数为0,第二个为1...依次类推。但是我们不要想着图省事去用这个序数表示一些业务上的属性,序数就应该只是序数,乱用会有不好的后果。比如用序数代替表示某个属性A,当前枚举量的序数为6,但我希望添加一个枚举量它的属性A我想要它为10,则不能实现。因为序数只能是一个一个递增,我们还不得不在6到10中间增加几个无意义的枚举量,才能定义我们想要的枚举量。
大多数程序员没有需要用ordinal()方法。它被设计用于基于枚举的通用数据结构,如EnumSet和EnumMap。除非在编写这样数据结构的代码,否则最好避免使用ordinal方法
36 使用EnumSet替代位属性
如果我们使用int表示一组固定的常量时,我们则可以在传参时使用或操作 ‘|’传入多个状态。如书中:
public class Text { //这个类表示文本风格 public static final int STYLE_BOLD = 1 << 0; // 1 表示某种风格 public static final int STYLE_ITALIC = 1 << 1; // 2 public void applyStyles(int styles) { ... } } ================ text.applyStyles(STYLE_BOLD | STYLE_ITALIC); //01 | 10 = 11,可以表明此处传入两种风格,即一个int值的集合
34条也说了: 尽量不要使用int来表示一组常量,而是要用枚举来表示。那么使用枚举可以方便的传入几个常量的集合吗?这是完全可以的,我们有EnumSet这个专门用于枚举的工具类,它可以将我们的若干个枚举常量转换为一个集合,而且它内部是用位矢量进行存储的,非常高效。简单使用示例:
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } public void applyStyles(Set<Style> styles) { ... } } =============== text.applyStyles(EnumSet.of(BOLD, ITALIC)); //简简单单就将两个元素的集合传了进去
EnumSet的其他方法作用就不表了。总之,不要害怕枚举常量不好一次传入多个,而去使用整型常量,从而用位属性运算实现传入多个常量的目的。
37 使用EnumMap代替序数索引
35条说了,最好不要用ordinal()方法拿枚举常量的序数做什么业务上的事情。当需要某个枚举常量做关联时使用EnumMap而不是序数。如:
//现在又枚举类Seasons 表示春夏秋冬,现在需要将每个季节开花的植物分别统计 Set<Plant>[] plants = (Set<Plant>[]) new Set[Seasons.values().length]; //建立四个数组,每个数组指向一个存放植物的Set Set<Plant> springPlants ... //假设springPlants存放了春天开花的植物 plants[SPRING.ordinal()] = springPlants; //则以春天这个枚举常量的序数作为数组的下标,将春天开花的植物集合赋给这个下标的元素
上面就是用的枚举常量的序数作为数组的下标,从而区分各个下标元素代表的季节为元素赋不同的集合。这种做法不好,不仅是35条中所说不易于拓展,而且数组与泛型结合使用也是非常不好的,有类型转换的隐患。推荐的做法是通过map做到枚举常量与其他量的关联,其中EnumMap就是专门为其准备的:
Map<Seasons, Set<Plant>> seasonPlantMap = new EnumMap<>(Seasons.class);//新建一个map,这个map以Seasons的各个枚举常量为key,以Set集合为val seasonPlantMap.put(SPRING,new HashSet<>()); //向map中加上 春天 常量,及其所对应的Set seasonPlantMap.get(SPRING).add(plants); //通过key:春天拿到它对应的Set,再往Set中加植物
总之就是想用枚举常量的序数做点什么时,比如:通过枚举常量序数作为数组下标,再去给这个下标的元素赋值从而关联一个值;先想想能不能用EnumMap做到。
38 使用接口模拟可扩展的枚举
前面说到了,枚举类十分难以拓展,这是为了保证它的安全性,枚举是最安全的单例模式实现,反射都无法破坏。那么我们要怎么让枚举类变的可拓展呢?一般来说要拓展某个类就需要继承他生成子类来拓展,但枚举是不可能让你继承的,于是我们想到让枚举类实现某个接口,想要对它进行拓展时只需新建一个枚举类但要实现相同的接口,这样两个枚举类的常量作为参数传递时都可以由这个共同的接口接收。
public class Main{ public static void main(String[] args) { Main mian = new Main(); main.funcB(BASE.AUTUMN); //既可以传递BASE类型的枚举量 main.funcB(EXPAND.AUTUMN); //也可以传递EXPAND类型的枚举量 } public void funcB(Intfs s) {...} //用两个枚举量都实现了的接口做形参类型 } enum BASE implements Intfs{ SPRING } enum EXPAND implements Intfs { AUTUMN }
39 注解优于命名模式
注解和命名模式都是通过装饰的方式让程序执行到某个地方时,经过一些特殊的处理。当然,现在基本没人使用命名模式了都是用的注解(方便且不易出错),如无必要也不用去了解命名模式了。(没什么好总结的)
40 始终使用@Override注解
重写父类方法时,始终使用@Override注解标记子类的重写方法。这是一个好习惯,因为这样重写方法不合法时就不能正常编译,这样就可以防止运行时由于重写方法出错的原因造成与我们预料不符的结果。
41 使用标记接口定义类型
前面说注解优于命名模式,但是也不是所有的标记工作都要使用注解。例如,JDK标记一个类是否可以被序列化是看它实现了Serializable接口,为什么要使用接口而不用一个注解来标记呢?
(1) 最重要的优势就是,标记接口定义了一个被标记类的类型,利用这点可以在编译时进行一些错误检测。例如:序列化方法的接收参数可以为Serializable类型
writeObject(Serializable object) { } 接收没有实现Serializable接口的类的对象传递时就会出现编译不通过错误。(JDK实际用Object接收,不完美)
(2)另一个优势是标记接口更精确,被标记接口标记的类必须实现了接口的所有方法,而注解是可以随意使用的没有对类进行约束。
什么时候用标记接口:如果标记仅用于类或接口,而我想编写一个方法,这个方法的参数我只想接收有此标记的类的对象(如 writeObject(Serializable object) { }),那么就编写一个标记接口让他们实现。其他的情况例如标记还会用于其他程序中的元素,则使用注解。
42 lambda表达式优于匿名类
lambda表达式其实就是通过匿名类实现的。就像以前一个线程可以通过创建一个实现了Runnable接口的匿名类,也就是实现了un方法来完成:
Thread t = new Thread(new Runnable() { @Override public void run() { ... } }); //仔细想想,似乎就只有这个run方法的内容有写的必要。
如上,既然实现了Runnable接口的类没名字,那么前面那些代码我们也完全可以省去,只保留方法体就行:
Thread t = new Thread( () -> { .... });
这就是lambda表达式,对于使用匿名类实现一个接口的方法时,接口中又只有一个方法我们完全可以只写方法的参数列表和方法体。虽然lambda表达式写法简便了许多,看起来就像是传入了一个方法一样,但实际上编译后再反编译看字节码就知道: 还是使用匿名类实现的,lambda表达式本质上就是颗语法糖而已。
这颗语法糖使代码确实简洁了许多,所以推荐使用lambda代替匿名类的写法。但也要注意也不是无条件使用:首先lambda表达式不便阅读且不方便调试,超过3行就不推荐使用了;然后使用lambda我们就无法获得匿名类的实例了;所以当需要匿名类实例或要用到this指针操作匿名类实例时,还是要编写匿名类实现。
43 方法引用优于lambda表达式
lambda表达式是为了代码简洁,方法引用则一般比lambda更简洁,当然如果lambda本身就很短方法引用就没办法更简洁了。一般来说,能用方法引用都建议使用方法引用。但是lambda表达式有个好一点的地方就是,可以为方法的参数起名字,并可以为参数提供说明文档从而使lambda表达式更具可读性可维护性。使用时需要基于以上的考虑,灵活选择。
44 优先使用标准的函数式接口
以前编写方法很多时候用模板方法:编写一个一个父类(或接口),然后由子类重写(或实现)它来专门化这个方法的行为。例如,编写一个操作两个整数的方法:
class Father{ public int func(int a,int b) { //父类定义了操作两个整数的方法 ........ return 0; } } ============ public static void main(String []args) { PlusSon ps = new PlusSon(); //Father类的子类,重写func函数,将两个参数相加后返回 int plusResult = ps.func(1,2); //二者相加的结果 MinusSon ms = new MinusSon(); //Father类的子类,重写func函数,将两个参数相减后返回 int minusResult = ms.func(1,2); ..... }
可以看出,这样很麻烦,只要新增个场景就要写新子类、实现类重写或实现方法。这时候函数式接口就派上用场了,它处理参数的方法体是在我们传递时指定的。如:
@FunctionalInterface //这个注解说明这是个函数式接口 public interface IntBinaryOperator { int operate(int left, int right); //函数式接口只有单独一个方法,操作两个整数 } =========== class Father{ public int func(int a,int b,IntBinaryOperator ibo) { //ibo接收一个实现了那个唯一函数的类,传递时可以简写为方法体即可 int result = ibo.operate(a,b); //operate的方法体在调用时才传入 return 0; } } ============ public static void main(String []args) { Father f = new Father(); int plusResult = f.func(1,2,(a,b) -> a + b); //想算加法时,传的方法体 int minusResult = f.func(1,2,(a,b) -> a - b); //想算减法时,传另一个方法体 int plusResult = f.func(1,2,Integer::sum); //也可以直接传入方法引用,用这个方法的方法体,不用自己写 ..... }
可以看出,说来说去还是因为lambda省去了匿名类实现有唯一方法的接口那一步,而是可以直接传入方法体。因此我们在编写供他人调用的方法时,可以在方法中设置一个参数为函数式接口,这样可以接收方法体,灵活的决定如何处理参数。
可以用@FunctionalInterface定义自己的函数式接口,也可以使用java.util.Function包中现成的函数式接口,一般来说使用现成的可以应付绝大多数场景。
45 明智审慎地使用Stream
流时Java8有的,可以很方便的处理一各序列的元素,这些元素的类型可以是引用和几种基础数据类型分别是int、double、long。流的特点就是独立于源数据,在流中修改元素的内容不会影响源数据;还有个特点就是顺序不能倒退,就是一个元素经过处理后就轮到下一个了不能再去处理上个了(流的具体特点和概念不表)。
流配合几个函数的用法也不表,而且要注意流并不是能完全替代传统的迭代处理元素的方法。具体局限有:
(1)在流的代码块不能修改局部变量,这时因为流独立于数据源决定的。不能让一个独立的、用完就消失的东西(因为不能后退)来操作局部变量。
(2) 流代码块里不够灵活,不能中断、继续循环,不能抛出已检查异常。
因此,使用流也要仔细评估它能否带来好处。当需要:统一转换元素序列、过滤元素序列、使用单个操作组合元素序列、将元素序列累计到集合或通过属性将其分组 等等,就要考虑用stream方法。
(小提示:stream中的参数不要用单个字母,难以阅读。)