
Java学习:Java从入门到精通总结
Spring系列推荐:Spring源码解析
最近更新:2021年12月16日
个人简介:通信工程本硕、阿里新晋猿同学。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤
点赞 收藏 ⭐留言 都是我最大的动力!
豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要外在的武功招式(编程语言、工具、中间件等等),也需要修炼心法(设计模式、源码等等)学霸、学神OR开挂
我个人在Java领域也已经学习了近5年,在修炼“内功”的方面也通过各种途径接触到了一些编程规约,例如阿里巴巴的泰山版规约,在此基础下读这本书的时候仍是让我受到了很大的冲激,学习到了很多约定背后的细节问题,还有一些让我欣赏此书的点是,书中对于编程规约的解释让我感到十分受用,并愿意将他们应用在我的工作中,也提醒了我要把阅读JDK源码的任务提上日程。
最后想分享一下我个人目前的看法,内功修炼不像学习一个新的工具那么简单,其主旨在于踏实,深入探索底层原理的过程很缓慢并且是艰辛的,但一旦开悟,修为一定会突破瓶颈,达到更高的境界,这远远不是我通过一两篇博客就能学到的东西。
接下来就针对此书列举一下我的收获与思考。
不过还是要吐槽一下的是翻译版属实让人一言难尽,有些地方会有误导的效果,你比如java语言里extends是继承的关键字,书本中全部翻译成了扩展 就完全不是原来的意思了。所以建议有问题的地方对照英文原版进行语义上的理解。
没有时间读原作的同学可以参考我这篇文章。
文章目录
- 34 用enum代替int常量
- 35 用实际属性代替序数
- 36 使用 EnumSet 替代位属性
- 37 使用EnumMap 替代序数索引
- 38 用接口实现可继承的枚举
- 39 注解优先于命名模式
- 40 坚持使用Override注解
- 41 用标记接口定义类型
每当需要一组固定常量,并且在编译时就知道常量分别都是什么时,就要用枚举。
在枚举出现之前,大家都是用int常量来表示枚举类型:
public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY_SMITH = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2;
上述做法有很多不足:
1. 不安全且没有任何描述性
如果将apple传入了想要orange的方法里,不会报错,还会用==运算符比较 Apple 与 Orange
2. 使用int 枚举的程序很脆弱
因为 int 枚举是编译时常量,所以它们的int 值被编译到使用它们的客户端中,如果与int 枚举关联的值发生更改,则必须重新编译其客户端
3. 很难将 int 枚举常量转换为可打印的字符串
就算将其打印出来了,所看到的也只是一个数字,没什么意义
4. 没有可靠的方法来遍历所有 int 枚举常量
除了int枚举模式之外,还有String枚举模式(String enum pattern),它同样也有很多缺点:
1. 导致初级用户将字符串常量硬编码到客户端代码中,就是常说的魔法值
2. 会依赖字符串的比较 *** 作故有很大的性能问题
Java提供的枚举类型可以很好的解决上面的问题:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
Java的枚举本质上是int值,思想非常简单:通过public static final属性为每个枚举常量导出一个实例。由于没有可访问的构造方法,枚举类型实际上是 final 的。客户既不能创建枚举类型的实例也不能继承它,除了声明的枚举常量外,不能有任何实例。
有以下几条优点:
1. 保证了编译时的类型安全
如果声明参数的类型为Apple,它就能保证传到该参数上的任何非空的对象引用一定是FUJI,PIPPIN,GRANNY_SMITH之一
2. 具有相同名称常量的多个枚举类型可以共存
因为每个类都有其自己的名称空间
3. 枚举类型还允许添加任意方法和属性并实现任意接口
提供了所有 Object 方法,实现了Comparable 和 Serializable 接口
比如太阳系的八颗行星,每个行星都有质量和半径,从这两个属性可以计算出它的表面重力
每个枚举常量之后的括号中的数字是传递给其构造方法的参数
package com.wjw.effectivejava1;
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6),
MARS(6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN(5.685e+26, 6.027e7),
URANUS(8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() {
return mass;
}
public double radius() {
return radius;
}
public double surfaceGravity() {
return surfaceGravity;
}
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
可以根据物体在地球上的重量,打印出该物体在所有8颗行星上的重量:
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
运行结果:
如果一个枚举具有普适性,他就应该成为一个顶层类,如果他只是被用在一个特定的顶层类里,他就应该成为该顶级类的
成员类,例如java.math.RoundingMode表示小数部分的舍入模式。BigDecimal类使用了这些舍入模式,但他们却不属于BigDecimal类的一个抽象。让RoundingMode成为一个顶层类,以鼓励让任何需要舍入模式的程序员重用。
有时我们需要更多的方法,加入正在编写一个枚举类,表示计算器的加减乘除 *** 作,还需要提供一个方法来执行每个常量所表示的运算:
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
public double apply(double x, double y) {
switch (this) {
case PLUS:
return x + y;
case MINUS:
return x - y;
case TIMES:
return x * y;
case DIVIDE:
return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
这段代码能用,但不优雅,如果添加了新的枚举常量,却忘记给switch添加相应的条件就会失败。
有一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体(constant-specific class body)。这种方法被称为特定于常量的方法实现(constant-specific method implementation):
public enum Operation {
PLUS {
public double apply(double x, double y) {
return x + y;
}
},
MINUS {
public double apply(double x, double y) {
return x - y;
}
},
TIMES {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE {
public double apply(double x, double y) {
return x / y;
}
};
public abstract double apply(double x, double y);
}
特定于常量的方法实现可以与特定于常量的数据结合使用。toString 方法返回与 *** 作关联的符号:
public enum Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
上述代码可以很容易地打印算术表达式:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难。例如,考虑用一个枚举代表工资包中的工作天数,根据给定的某工人的基本工资(每小时)和当天工作的时间计算当天工人的工资。
enum PayrollDay {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate; // 计算基本工资
// 计算加班工资
int overtimePay;
switch (this) {
case SATURDAY:
case SUNDAY: // Weekend
overtimePay = basePay / 2;
break;
default: // Weekday
overtimePay = minutesWorked <= MINS_PER_SHIFT ?
0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
假设你给枚举添加了一个元素,可能是一个特殊的值来表示一个假期,但忘记在switch 语句中添加一个相应的case 条件。该程序仍然会编译,但付费方法会将节假日的工资算成工作日的工资,原因是走了上面default里的逻辑。
我们真正想要的是每次添加枚举常量时,就自动选择加班费策略:再定义一个嵌套枚举类PayType,并将PayType实例传递给·PayrollDay·枚举的构造方法里。
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) {
this.payType = payType;
}
PayrollDay() {
this(PayType.WEEKDAY);
} // Default
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
private 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);
}
}
}
枚举的switch语句适合于给外部的枚举类型增加和常量值对应行为。
假设希望 Operation 枚举有一个实例方法来返回每个相反的 *** 作。
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op: " + op);
}
}
35 用实际属性代替序数
许多枚举天生就与某个 int 值关联。所以枚举都有一个ordinal方法,返回每个枚举常量在类型中的数字位置。
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET, SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1;
}
}
这段代码维护起来就是一场噩梦,如果上面常量的顺序变了,所有用到numberOfMusicians的地方都会返回一个不同的值。所以永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例属性中:
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
实际上,ordinal的目的是用于基于枚举的通用数据结构,如 EnumSet 和 EnumMap,除了在编写这种数据结构时可以用,其他时候都不要用。
36 使用 EnumSet 替代位属性
EnumSet 类集位属性的简介和性能优势及枚举类型的所有优点于一身。
如果枚举类型的元素主要用于集合中,一般就使用int枚举模式,例如将2的不同倍数设置成常量:
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Parameter is bitwise OR of zero or more STYLE_ constants
public void applyStyles(int styles) { ... }
}
这种int型表示方式允许使用按位或(or)运算,将几个常量合并到一个称为位属性(bit field)的集合中:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
除了使用 OR 运算符之外,位属性表示法还可以支持求交集、异或求加法等很方便的 *** 作,但是它本质上还是int型枚举常量,所以继承了int枚举常量的所有缺点:
1. 打印位属性时,翻译位域要难得多
就好比让你直接用二进制编程,酸爽程度可想而知
2. 没有一个好的方法可以遍历所有位属性表示的元素
3. 写API之前就要确定好需要多少位,选择相应的类型(int、long)
java.util包提供了 EnumSet 类来有效地表示从单个枚举类型中提取的值集合。
在内部具体的实现上,每个EnumSet表示为位矢量。
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set