其他
第N次读《Effective Java》,肝了一篇到底那些技巧需要你花功夫仔细揣摩~
来源:https://blog.dogchao.cn/?p=70
哈喽,各位新来的小伙伴们,大家好!由于公众号做了改版,为了保证公众号的资源能准时推送到你手里,大家记得将咱们的公众号 加星标置顶 ,在此真诚的表示感谢~
《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。
以下内容只记录了我自己整理的东西,还是建议读原文。为了聚焦知识点,一些说明故意忽略掉了。相当于是一篇摘要。
1、考虑用静态工厂方法替代构造函数
例子:
Integer.valueOf(“1”)、Boolean.valueOf(“true”)等。
优势:
可读性高(方法名)
性能(不一定创建对象)
灵活性高
下面针对三个优势进行一些解读。
可读性高
性能
public final class Boolean implements Serializable, Comparable<Boolean> {
// 预先设置两个对象
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
public Boolean(boolean var1) {
this.value = var1;
}
public Boolean(String var1) {
this(parseBoolean(var1));
}
// 工厂方法
public static Boolean valueOf(boolean var0) {
return var0?TRUE:FALSE; // 返回预先设置的对象,而不是创建对象
}
// 工厂方法
public static Boolean valueOf(String var0) {
return parseBoolean(var0)?TRUE:FALSE;
}
// ... other code
}
灵活性高
public class Collections {
// 私有,典型工厂
private Collections() {
}
public static final List EMPTY_LIST = new EmptyList<>();
// 工厂方法
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {
// code
}
// 工厂方法
public static <E> List<E> checkedList(List<E> list, Class<E> type) {
// 根据具体情况,获取相应子类
return (list instanceof RandomAccess ?
new CheckedRandomAccessList<>(list, type) :
new CheckedList<>(list, type));
}
// 子类1
static class CheckedRandomAccessList<E> extends CheckedList<E> implements RandomAccess {
CheckedRandomAccessList(List<E> list, Class<E> type) {
super(list, type);
}
public List<E> subList(int fromIndex, int toIndex) {
return new CheckedRandomAccessList<>(
list.subList(fromIndex, toIndex), type);
}
}
// 子类2
static class CheckedList<E> extends CheckedCollection<E> implements List<E> {
// code
}
}
2、多个构造函数时,考虑使用构造器
// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
private int width;
private int height;
private String title;
private String confirmText;
private String denyText;
private AlertDialog(){}
public AlertDialog(int width, int height){ // 空白的警告框
AlertDialog(width,height,null);
}
// 带标题的警告框
public AlertDialog(int width, int height, String title){ // 带标题的警告框
AlertDialog(width, height, title, "确定");
}
// 带标题的警告框,有确定按钮
public AlertDialog(int width, int height, String title, String confirm){
AlertDialog(width, height, title, confirm, null);
}
// 带标题的警告框,有确定按钮,取消按钮
public AlertDialog(int width, int height, String title, String confirm, String denyText){
// set every thing.
}
}
有多种样式的警告框,为了调用方便,必须提供多个构造函数。否则用户在调用时,只能使用完整构造函数,容易犯错且无法进行阅读。极不灵活。如果采用另外一种方式,则可以解决,但会花费很多经历处理并发的情况:
// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
private int width;
private int height;
private String title;
private String confirmText;
private String denyText;
public AlertDialog(){}// 空白的构造函数
public void setWidth(int width){
this.width = width;
}
// 其他set方法
}
调用时,通过调用各个参数的set方法进行设置。问题来了:
并发
无法进行参数校验。
例如,只创建了对象,设置了标题,却没有尺寸,相当于创建了一个没有尺寸的警告框。
在Android中,大量的控件都使用了构造器Builder。
// 非Android中的AlertDialog,便于说明问题,举个例子
public class AlertDialog {
private int width;
private int height;
private String title;
private String confirmText;
private String denyText;
// private
private AlertDialog(){}
// Builder中使用
protected AlertDialog(Builder b){
width = b.width;
height = b.height;
// .....
if(width==0||height==0) throws new Exception("size must be set");
}
// 构造器
public static class Builder {
private int width;
private int height;
private String title;
private String confirmText;
private String denyText;
// 注意:返回的Builder。
public Builder setTitle(String title) {
this.title = title;
return this;
}
// 其他set...
public AlertDialog build(){
return AlertDialog(this);
}
}
}
于是,可以根据相应需求,进行相应设置,并在AlertDialog真正构造时,进行参数校验。就像这样:上述例子,会成功抛出异常。
3、用私有化构造器或者枚举型强化Singleton。
public class Elvis{
// 注意,公有final对象
public static final Elvis INSTANCE = new Elvis();
private Elvis(){}
}
另一种情况,在序列化的过程中,反序列化得到的对象已经不再是以前的对象(破坏了Singleton),这种情况下,可以通过单元素枚举型处理。
public enum Elvis{
INSTANCE;
// some methods
}
4、通过私有化构造器强化不可实例化的能力
public class Util{
private Util(){
// 抛出异常,防止内部误调用
throw new AssertionError();
}
}
弊端是无法对该类进行继承(子类会调用super())。5、避免创建不必要的对象
对象的重用 昂贵的对象,使用对象池 廉价的对象,慎用对象池。
现代JVM对廉价对象的创建和销毁非常快,此时不适于使用对象池。
6、消除过期的对象引用
自己管理的内存(数组长度减小后,pop出的对象容易导致内存泄漏)
缓存 监听和回调
自己管理的内存
public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++]=e; // allocate新的堆内存和栈内存
}
public Object pop(){
if(size==0) throw new EmptyStackException();
return element[--size]; // pop出element[size],该对象不再有效。内存泄漏原因。
}
private void ensureCapacity(){
if(elements.length==size)
elements = Arrays.copyOf(elements, 2*size+1);
}
}
弹出的对象不再有效,但JVM不知道,所以会一直保持该对象,造成内存泄露。解决:
public Object pop(){
if(size==0) throw new EmptyStackException();
elements[size] = null; // 等待回收
return element[--size];
}
缓存
监听或回调
7、避免显示调用GC
jvm是针对具体的硬件设计的,然而程序却不是针对具体硬件设计的,所以,java代码无法很好的解决gc问题(因为他具有平台差异化)。另外,finalizer的性能开销也非常大,从这个角度上考虑也不应该使用它。
8、覆盖equals方法请遵守通用约定
自反性。 x.equals(x) == true
对称性。 当前仅当y.equals(x)==true时,x.equals(y)==true
传递性。 if(x.equals(y)&&y.equals(z)),y.equals(z)==true
一致性。
非空性。 x.equals(null)==false
9、覆盖equals方法时总要覆盖hashCode
10、始终覆盖toString
11、谨慎覆盖clone
12、考虑实现Comparable接口
13、使类和成员的可访问性最小化
子类覆盖超类,不允许访问级别低于超类的访问级别。(超类的protected,子类覆盖后不能改为default)。
成员变量决不允许是公有的。一旦设置为公有,则放弃了对他处理的能力。这种类并不是线程安全的。即使是final的,也不允许。除非希望通过public static final来暴露常量。成员变量总是需要使用setter和getter来维护。有一个例外:长度非零的数组。这是安全漏洞的一个根源。
public Object pop(){
if(size==0) throw new EmptyStackException();
elements[size] = null; // 等待回收
return element[--size];
}
改进:
private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final List<Thing> VALUS =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))
另一种:
private static final Thing[] PRIVATE_VALUES = {...}
// 此时获取到的才是“常量”
public static final Thing[] values(){
return PRIVATE_VALUES.clone();
}
14、在公有类中使用访问方法而非公有成员变量(类似13)
15、使可变性最小化
16、复合优先于继承
复合,即不扩展已有的类,而是在的类中新增一个现有类的。相当于现有类作为一个组建存在于新类中。如此,将只会用到需要用到的东西,而不表现现有类所有的方法和成员变量。新类也可以称为“包装类”,也就是设计模式中的Decorate模式。
17、要么就为继承而设计,并提供文档说明,要么就禁止继承
18、接口优于抽象类
19、接口只用于定义类型
20、类层次优先于标签类
21、用函数对象表示策略
22、优先考虑静态类成员
如果成员类不需要访问外围类,则需要添加static,是他成为静态成员类,否则每个实例都将包含一个额外指向外围对象的引用。将会影响垃圾回收机制。
23、应指定泛型的具体类型,而不是直接使用原生类型。
24、消除非首检警告
25、列表优先于数组
// Fails at runtime
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // throw exception
// won't compile
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
从代码中可以看到,使用泛型,会提前发现错误。
26、优先考虑泛型
27、优先考虑泛型方法
28、利用有限制通配符来提升API的灵活性
//public class Stack<E>{
// public Stack();
// public void push(E e);
// public E pop();
// public boolean isEmpty();
//}
public void pushAll(Iterator<? extends E> src){
for(E e : src)
push(e);
}
public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
// Get and Put Principle
所有comparable和comparator都是消费者(Consumer)。
29、优先考虑类型安全的异构容器
30、用enum代替int常量
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
枚举型在java中非常强大,当需要一组固定常量时,使用enum比int好很多。比如代码可读性,安全性等。
31、enum用实例域代替序数
// bad solution
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
//
// improvement
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
永远不要像第一种的方式,利用序数访问enum,需要在构造函数中使用参数来初始化。
32、用EnumSet代替位域
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
public void applyStyles(int styles){
// ...
}
}
//
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);以上叫做位图法,但是有更好的方案来传递多组常量——EnumSet。public class Text{
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// 注意此处,使用的是Set而不是EnumSet
public void applyStyles(Set<Style> styles){
// ...
}
}
//
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
33、用EnumMap代替序数索引
34、用接口模拟可伸缩的枚举
35、注解优先于命名模式
36、坚持使用Override注解
38、检查参数的有效性
39、必要时进行保护性拷贝
public Period(Date start, Date end){
this.start = new Date(start); // 使用了值的拷贝,没有使用原对象(指针)
this.end = new Date(end);
if(this.start.compareTo(this.end)>0)
throw new IllegalArgumentException(start + " after " + end)
}
注意:保护性拷贝是在检查参数之前进行的,防止多线程的影响。不要使用clone方法进行保护性拷贝。
以上方法防御了传入参数的修改,但是对于get方法获取到的对象,仍然可以被修改,通过以下方法可以防止这种攻击。
public Date start(){
return new Date(start);
}
public Date end(){
return new Date(end);
}
40、谨慎设计方法签名
41、慎用重载
42、慎用可变参数
43、返回0长度的数组或者集合,而不是null
44、为所有导出的API元素编写文档注释
45、将局部变量的作用域最小化
46、for-each优先于for循环
过滤 转换 平行迭代
48、如果需要精确的答案,请避免使用float和double
为了解决这个问题,需要使用BigDecimal。然而这也有一些问题,相对于普通的运算,它显得更加麻烦,而且也更慢。通常来说后一个缺点可以忽略,但是前者可能会让人很不舒服。有一种做法是将需要处理的数值*10(或更多),使用int进行计算,不过需要你自己处理四舍五入等操作。
49、基本类型优先于装箱基本类型
基本类型只有值,装箱类具有与他们值不同的同一性。
基本类型只有功能完备的值,装箱类还具有非功能值:
null。
所以你可能会碰到NPE
基本类型省空间省时间
50、如果有更精确的类型,请避免使用字符串
字符串不适合代替其他值的类型。
例如:int,boolean等不适合代替枚举类型(第30条) 不适合聚集类型
51、当心字符串连接的性能
52、通过接口引用对象
53、接口优先于反射机制
丧失了编译期类型检查
代码笨拙冗长 性能损失
反射基本上只适合用在编写组件时、代码分析器、RPC等场景下使用。在使用反射机制时,如果可能,尽可能只通过反射机制实例化对象,而访问方法时,使用已知的接口或者超类。
54、谨慎使用JNI
55、谨慎进行优化
不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。
——Donald E. Knuth
在优化方面,我们应该遵守两条规则:
规则1:不要进行优化。
规则2(仅针对专家):还是不要进行优化——也就是说,在你还没有绝对清晰的优化方案前,请不要进行优化。
——M. A. Jackson
这些格言比java的出现还要早20年。他们讲述了一个关于优化的深刻事实:优化的弊大于利。
要努力编写好的程序,而不是快的程序。低耦合的重要性远远大于性能。当程序编写得足够低耦合后,通过工具发现了性能瓶颈的代码块,才可以保证对其的修改不影响任何外部环境。
56、遵守普遍的命名规则
57、只针对异常情况才使用异常
jvm很少对异常进行优化,因为它只用于不正常的情况。并且,如果你将代码放入try-catch代码块,jvm就丧失了本来可以对它进行的优化。
58、对于可恢复的情况使用受检异常,对于编程错误的情况使用运行时异常
如果期望调用者适当的恢复,则需要使用受检异常,强迫调用者食用try-catch代码块,或者将他们抛出去
当调用发生前提违例——违反约定的情况时,使用运行时异常,这个时候程序已经无法再执行下去了。
例如调用数组的-1索引。
如果你还有补充的话,欢迎在留言区补充,或者给咱们提建议哈~
还需要什么ProcessOn、PlantUML,IDEA自己就可以把项目一键生成UML类图~ 11月全国程序员平均工资出炉,网友:我丢了同行的脸~ MySQL 8.0 新特性:哈希连接(Hash Join)实战,效率如何?