Effective JAVA
第1条:考虑用静态工厂方法代替构造器
第2条:遇到多个构造器参数时要考虑使用构建器
- 使用lombok的@Builder注解
- 配合使用@NoArgsConstructor 提供无参构造函数
第3条:用私有构造器或者枚举类型强化Singleton属性
- 用饥饿模式创建单例
- 使用枚举类型实现singletons是最佳方法
第4条:使用私有化构造器强化不可实例化的能力
- 强迫使用工厂方法获取实例对象
- 坏处是导致子类化。继承
第5条:避免创建不必要的对象
- 典型反例: String sss = new String(“now”),字符串的字面常量在jvm只有一份的,能重复利用。new String(“now”)操作如果还没有该字面常量则会创建并再创建一个String对象
- jdk:map的keySet方法返回set视图时会缓存一份
- 小心自动装箱和自动拆箱
- 提倡保护性拷贝
第6条:清除过期的对象引用
- jdk中有很多help gc的注释
- 常常是remove调一个对象时需要释放对象的引用。
- 想到了ThreadLocal的内存泄漏。弱引用导致的(下一次GC)。
- 使用weakHashMap保持弱引用
- 使用内存分析工具查找泄漏的地方
第7条:避免使用终结方法
- Object的finalize方法如果子类有覆盖jvm会在gc时调用,放到调用finalize队列中,执行改队列的优先级很低,jvm不保证一定会执行完finalize
- 子类覆盖了finalize后需要手动调用父类得到finalize方法
- 不推荐使用finalize来处理一些资源的关闭。推荐使用try cache finally
- 必须用到finalize时可以使用终结方法守卫者来执行。(此方法,子类无需手动调用finalize)
第8条:覆盖equals时请遵守通用约定
- 要满足自反性(x.equals(x) == true),对称性(x.equals(y) == y.equals(x)),传递性(x.equals(y),y.equals(z),则x.equals(z)),一致性(比较的值未修改时,多次调用equals时需要返回一致的值)
- 推荐使用 == 检查引用是否相等,instanceof检查参数的正确类型,转换成正确类型,对每个关键值比较
- 关注null的情况
- 覆盖equals时要覆盖hashcode
第9条:覆盖equals时总要覆盖hashCode
- 字段是boolean 则计算f?1:0
- 字段是byte,char,short,int 则(int) f
- 字段是long 则计算 (int)(f^(f>>>32)
- float则Float.floatToIntBits(f)
- double则Double.doubleToLongBits(f),再计算 (int)(f^(f>>>32)
- 引用对象则调用引用对象的hashCode
- 如果时数组Arrays.hashCode
- 写完单元测试检查
- 可以使用lombok的@EqualsAndHashCode
第10条:始终覆盖toString
- 使用lombok的@ToString
第11条:谨慎的覆盖clone
- clone方法会递归调用字段的clone并赋值给新的对象
- 最后不要覆盖clone方法
第12条:考虑实现comparable接口
- 当需要用到排序时,考虑实现comparable接口替代创建Comparator的实现类
满足以下条件
第13条:使类和成员的可访问性最小化
- public、package-private、protected、private四种类型
第14条:在公有类中使用访问方法而非公有域
- 这点比较常用
第15条:使可变性最小化
- 就是增加各种限制,缩小访问权限。(final修饰、private修饰)
第16条:复合优先于继承
- 当然啦,合理使用,复合继承都可以。
第17条:要么为继承而设计,并提供文档说明,要么就禁止继承
第18条:接口优于抽象类
- 现有的类容易更改,可以实现新的接口
- 接口是定义mix的理想选择
- 接口允许我们构造非层次结构的类型框架
第19条:接口只用于定义类型
- 接口是来定义方法的,其他使用都不恰当
- 常量接口是不可取的
第20条:类层次优于标签类
- 在一个类中增加标签字段,既可以表示这个也可以别是其他类型,这是不可取的
- 使用定义抽象父类+多个类型的子类。类层次更加明确
第21条:用函数对象表示策略
- 定义一个对象,它的方法执行其他对象上的操作,一个类仅仅导出这样的一个方法。则称为函数对象
- 定义一个类,单例类,让具体的策略类成为宿主类的私有嵌套类
class Host{
private static class StrLenCmp implements Comparator<String>,Serializable{
public int compare(String s1,String s2){
return s1.length()-s2.length();
}
}
public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}
第22条:优先考虑静态成员类
- 嵌套类是定义在另一个类的内部的类。有四种:静态成员类,非静态成员类,匿名类,局部类
- 静态成员类,最后把它看作是普通的类,恰巧被声明在另一个类的内部。可以访问外围类的所有成员
- 非静态成员类,实例化非静态成员类会关联外部类的对象,如map中的keySet
- 匿名内部类往往是lambda表达式实现一个接口
- 局部类,只有当局部类是非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员
第23条:请不要在新代码中使用原生态类型
- 声明具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型类或者接口。
- 泛型类和接口统称为泛型(generic type)
- 每个泛型都定义一个原生态类型。如List的原生态类型就是List。
- 可以使用无限制的通配符类型(unbounded wildcard type)替代原生态来兴
- 在类文字中必须使用原生态类型
- 在使用instanceof判断类型中不能使用参数化的类型,可以是无限制通配符类型
合理使用方法
if(o instanceof Set){
Set<?> m = (Set<?>)o;
}
- 黑话介绍
//递归类型限制
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
第24条:消除非受检警告
- 无法消除的警告,但是类型是安全的使用@SuppressWarnings(“unchecked”)
- 有非受检异常时,且确认是类型安全的,必须使用注解消除,并说明原因
第25条:列表优先于数组
- 数组是协变的。子类型数组可以向上转换成父类型数组
- List中的type parameter,如果是父子类型,转换时会报编译错误
- 创建泛型数组是非法的
- 禁止在可变长参数中使用泛型
- 数组是协变且可以具体化的,泛型是不可变的且可以被擦除的
- 数组提供了运行时的类型安全(不会抛出ClassCaseException),但是没有编译时的类型安全。泛型反之。
// 元素是父子类型那么数组也是父子类型,要是可以向上转换的
// 存储时会报ArrayStroyException
public static void main(String[] args) {
Object[] objects = new Long[10];
objects[0] = "123123";
}
// 虽然编译能过,但是运行时E是不确定,运行时E[]是Object[],则在运行时会存在上述问题。
E[] objects = (E[]) list.toArray();
//以下是正确使用方法,提供了类型安全的转换
ArrayList<E> es = new ArrayList<>(list);
第26条:优先考虑泛型
- 在自己的类中使用泛型替代object
第27条:优先考虑泛型方法
- 使用泛型方法可以消除类型警告
- 使用泛型方法的静态工厂方法创建HashMap等对象实例
第28条:利用有限制通配符来提升API的灵活性
- ? extends T, ? super T。称为有限制的通配符
- 参数化类型是不可变的,通配符类型比较灵活
- PECS。Product extends。Consumer super。 入参是product。出参是consumer
- 无限制通配符类型取值为Object,无法放入值
- 这条不是特别懂
//解决无限制通配符类型无法放入值的问题
public void swap(List<?> list,int i,int j){
swapInner(list,i,j);
}
private <E> void swapInner(List<E> list,int i,int j){
list.set(i,list.set(j,list.get(i)));
}
第29条:优先考虑类型安全的异构容器
- CheckedSet,CheckedList,CheckedMap.
枚举和注解
第30条:用enum代替int常量
- enum各种好,多用
第31条:用实例域替代序数、、、、、、、、、、、、、
- 不要使用枚举的ordinal(),表示枚举实例的位置顺序
第32条:用EnumSet代替位域
- EnumSet.of 替代 比如selector轮询时care的类型
第33条:用EnumMap代替序数索引
- 使用enum作为key。enum的生态
第34条:用接口模拟可伸缩的枚举
- 枚举不允许继承
- 通过实现接口来扩展实现类
//参数类型多限制
public <E extends Enum<E> & Comparable<E>> void swap(List<E> list,int i,int j){
swapInner(list,i,j);
}
第35条:注解优先于命名模式
- 使用注解,而不是规定命名
第36条:坚持使用Override注解
- 继承方法和实现方法都要加上Override注解,编译器会检查是否正常覆盖
第37条:用标记接口定义类型
- Serializable标记接口 clone标记接口
方法
从处理参数、返回值、设计方法签名、编写文档方面讨论可用性、健壮性和灵活性
第38条:检查参数的有效性
- 检验参数的有效性,并在javadoc中说明可能会抛出的异常和情况
第39条:必要时进行保护性拷贝
- 总之一条,对应可能存在变化的对象进行拷贝。
- 对于入参必要时进行拷贝,因为入参有可能变化
第40条:谨慎设计方法前面
- 名字要做到顾名思义
- 不要提供大而全的方法,提供基础的方法
- 避免过长的参数列表,超过4个建议拆分方法
- 入参优先使用类
第41条:慎用重载
- 对于重载的方法,java是编译时决定调用哪个方法。注意不根据运行时实际类型决定
第42条:慎用可变参数
- 不要为了方便而使用,要因为设计而使用
第43条:返回零长度的数组或者集合,而不是null
- 空集合和数组,可以不需要判断就能使用
- 为了避免重复创建可以定义一个全局的空数组和空集合
第44条:为所有导出的API元素编写文档注释
- @throws @param @return @ @{code}
- 使用swagger为外部接口写文档,可以配合yapi一键导入
第45条:将局部变量的作用域最小化
- 保护变量,减少内存使用
第46条:使用for-each循环优于传统的for循环
- 减少错误
- 对数组索引的边界值只计算一次
第47条:了解和使用类库
- Random.nextInt
- juc包
- collection utils
- 去了解类库和使用类库
第48条:如果需要精确的答案,避免使用float和double
- 使用BigDecimal、int、long进行货币计算
第49条:基本类型优先于装箱类型
- 熟悉自动装箱和自己拆箱的情况
- 在外部接口入参中还是使用包装类型为好
第50条:如果其他类型更合适,则避免使用字符串
- 不要用字符串替代基本类型、枚举类型和聚集类型
- 但是外部接口传入时,字符串是真的好用
第51条:注意字符串连接的性能
- 使用stringBuilder和StringBuffer替代字符串连接
- 字符串连接在java编译后,有一部分会自动使用StringBuilder进行连接
第52条:通过接口引用对象
- 方便以后提交对象引用的实例类型
第53条:接口优先于反射机制
- 执行反射访问所需要的代码非常笨拙和冗长
- 性能损失
- 最好仅仅使用反射机制来实例化对象,访问对象使用超类或者接口
第54条:谨慎的使用本地方法
- jni允许应用程序可以调用本地方法
- 我还没用过,写完试一下
第55条:谨慎地进行优化
- 在设计时对模块之间的交互,模块和外部交互的API、线路层协议和永久数据格式合理设计
第56条:遵守普遍接受的命名管理
- 常量全部大写 + 下划线
- 变量用驼峰
- 方法名驼峰
- 类型参数 T,E,K,V,T1,T2
第57条:只针对异常的情况才使用异常
- 在确实存在异常的情况捕获异常。避免滥用
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
- checked exp,run-time exp,error
- 对于由于错误使用方法,比如入参不规范的使用运行时异常。
- 受检异常时可恢复的,受检异常需要cache住。希望用户对异常处理时使用checked exp
- run-time则不需要
第59条:避免不必要地使用受检的异常
- 往往受检的异常只能cache住打日志,并做合理提示,不如直接抛run-time的
第60条:优先使用标准的异常
第61条:抛出与抽象相对应的异常
- 底层抛出异常时捕获,并封装成与本方法相符合的异常,虽然很麻烦。
第62条:抛出的异常要有文档
- 不然也看不懂你为啥抛异常,还要看一堆逻辑
第63条:在细节消息中包含能捕获失败的信息
- 打印堆栈时把错误的信息打的更详细,排查定位问题更快,谁用谁知道。
第64条:努力使失败保持原子性
- 类似事务,失败后要还原状态。但是怎么还原呢,当然是提前校验参数的准确性
- 写恢复的代码,比如mysql的undo.log
- 拷贝一份对象,失败时恢复。 应该不多用吧
第65条:不要忽略异常
- 至少解释下为啥忽略吧,好歹打个日志
并发
第66条:同步访问共享的可变数据
- volatile和synchronized
- 上面两个关键字的区别
- 单个变量safe thread, 代码块safe thread
第67条:避免过度同步
- 史诗级难题–要使用但是不能过度使用
第68条:多多使用juc下的包
- 举个例子 ThreadPoolExecutor和ExecutorService,CompletableFuture
//优雅的退出ExecutorService
public void shutdown(ExecutorService executor){
executor.shudown();
while(!executor.isTerminated()){
try{
executor.awaitTermination(600L,TimeUnit.SECONDS);
}cache(InterruptedExecption ignore){
Thread.currentThread().interrupt();
}
}
}
第69条:并发工具优先于wait和notify
- 多用juc下的并发控制工具
- ReentrantReadWriteLock,ReentrantLock,StampedLock
第70条:线程安全性的文档化
- 线程安全级别:
- 不可变的
- 无条件的线程安全
- 有条件的线程安全
- 非线程安全
- 线程对立
第71条:慎用延迟初始化
- 不建议使用lazy initializtion holder
- 推荐使用双重校验初始化
第72条:不要依赖线程调度器
- 使用并发控制工具-JUC
- 尽量少使用Thread.yield,Thread.Sleep
第73条:避免使用线程组
- 使用线程池替代Thread Group
序列化
第74条:谨慎地实现Serializable接口
- 实现了Serializable接口就要有流得到唯一标识符–serialVersionUID
- 可以不通过构造器实例化。对象的约束性遭到破坏
- 测试成本增加。需要保证序列化和反序列化没问题
第75条:考虑使用自定义的序列化形式
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
BB bb = new BB();
bb.getAa().age = 7;
bb.setAge(7);
out.writeObject(bb);
ByteArrayInputStream byteArrayInputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
BB o = (BB)objectInputStream.readObject();
System.out.println(o.getAge());
System.out.println(o.getAa());
o.getAa().age = 8;
System.out.println(bb.getAa().age);
第76条:保护性地编写readObject方法
- 不知道
第77条:对于实例控制,枚举类型优先于readResolve
- 感觉是用来防范客户端被篡改的
第78条:考虑用序列化代理代替序列化实例
- 感觉是用来防范客户端被篡改的