读书笔记——《Effective Java》


作者: 康凯森

日期: 2016-11-05

分类: Java


服务提供者框架

多个服务提供者实现一个服务,系统为多个服务提供者的客户端提供多个实现,并把他们从多个实现中解耦出来。

服务提供者框架有3个重要组件:

  1. 服务接口 : 服务提供者实现
  2. 提供者注册API : 系统用来注册实现,让客户端访问它们的
  3. 服务访问API :客户端用来获取服务实例的
  4. 服务提供者接口(可选):服务提供者负责创建其服务实现的实例

对于JDBC 来说:

  • connection 就是服务接口
  • drivermanager.registerdriver 是提供者注册API
  • drivermanager.getconnection 服务访问API
  • driver 服务提供者接口

1 学习一门语言必须掌握3件事情

  • 语法
  • 库和API
  • 用法

2 静态工厂方法代替构造器

静态工厂相比构造器的优势1 :有名称

静态工厂相比构造器的优势2 :不必每次调用时都创建一个新对象

静态工厂相比构造器的优势3 :可以返回原返回类型的任何子类型的对象

静态工厂相比构造器的优势4 : 在创建参数化类型实例的时候,可以使代码更加简洁

静态工厂方法与其他的静态方法实际上没有任何区别

3 遇到多个构造器参数时要考虑用构建器

静态工厂和构造器的共同缺陷:不能很好的扩展到大量的可选参数

重叠构造器模式 不适合参数很多的情景

JavaBeans模式 线程安全问题

Builder模式 线程安全,易读,适合大量参数的场景,生成的对象是不可变的

4 通过私有构造器强化类不可实例化的能力

5 避免创造不必要的对象

如果对象是不可变的,它就始终可以被重用

优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱

小对象的创建和回收动作是十分廉价的

真正正确使用对象池的典型对象示例就是数据库连接池

6 消除过期的对象引用

所谓的过期引用,是指永远也不会再被解除的引用。

如果一个对象引用被无意识地保留起来了, 那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。

一旦对象引用已经过期,只需清空引用即可。即赋值为null。

只要类是自己管理内存,就应该警惕内存泄露问题

内存泄露的另一个常见来源是缓存

内存泄露的第三个来源是监听器和回调

7 应该避免使用终结方法

终结方法的缺点是不能保证会被及时执行。

8 覆盖 equal 时请遵循通用约定

自反性 对称性 传递性 一致性 如果俩个对象相等,它们必须始终保证相等

  1. 用 == 操作符 检查 参数是否是这个对象的引用;
  2. 使用 instanceof 操作符 检查 参数是否为正确的类型;
  3. 把参数转换为正确的类型;
  4. 对于参数中的关键域,检查参数中的域是否与该对象中对应的域相匹配;
  5. 当编写完 equal方法时。应该检查它是否是对称的,传递的,一致的。
public boolean equals(Object obj){ 
  //判断obj是否和this相等,保证自反性 
  if (obj == this) return true; 
  //判断obj是否为null,保证最后一条准则 
  if (obj == null) return false; 
  //判断两个对象的实际类型是否相等, 
//如果不相等,则说明比较的是两个不同种类的对象,应当返回false 
if (obj.getClass() != this.getClass()) return false; 
//强制类型转换 
//由于之前已经使用getClass判断过实际类型,因此这里强转是安全的 
Student stu = (Student) obj; 
//判断每个属性是否相等 
//  对于基本类型的属性用“ == ”比较,对象类型的属性用 equals 比较  
if (this.age == stu.age && this.name.equals(stu.name) )  

  return true; 
else return false; 
}

9 覆盖equal时总要覆盖hashcode

相等的对象必须具有相等的 hashcode

10 始终覆盖 toString

11 谨慎覆盖 clone

永远不要让客户去做任何类库可以替客户完成的事情。

12 考虑实现 Comparable 接口

13 使类和成员的可访问性最小化

设计良好的模块会隐藏所有的实现细节,把它的API和实现清晰地隔离开来。然后,模块之间只通过API进行通信。

包含共有可变域的类并不是线程安全的。

14 在共有类中使用访问方法而非共有域

15 使可变性最小化

不可变类只是实例不能被修改的类。

1 不要提供任何修改对象的方法。

2 保证类不会被扩展。

3 使得所有域都是 final 和私有的。

4 确保对于任何可变组件的互斥访问。

不可变对象本质上是线程安全的,不要求同步,可以自由共享。

不可变类的真正唯一缺点,对于每个不同的值都需要一个单独的对象。

16 复合优先于继承

17 要么为继承而设计并提供文档说明,要么就禁止继承

好的API文档应该描述了一个给定的方法做了什么工作,而不是如何做到的。

18 接口优于抽象类

抽象类允许包含某些方法的实现,但是接口不可以。

为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。

接口的优点:

  1. 现有的类可以很容易被更新,以实现新的接口。
  2. 接口是定义mixin(混合类型的)理想选择。
  3. 接口允许我们构造非层次结构的类型框架。

抽象类的演变比接口的演变要容易得多。

19 接口只用于定义类型

一般不定义常量。

常量的定义可以放在本身类,枚举类,工具类中。

20 类层次优于标签类

代码

21 用函数对象表示策略

这类对象的方法执行其他对象上的操作,如果一个类仅仅导出这样一个方法,它的实例实际上就等同于一个指向该方法的指针。这样的实例被称为函数对象。

函数指针的主要用途就是实现策略模式,在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该借口的类。

代码

具体的策略类只使用一次时,往往使用匿名类实现。

重复使用时 使用私有的静态成员类。

22 优先考虑静态成员类

嵌套类存在的目的只是为了它的外围类服务。

静态成员类 作为共有类的辅助类,仅当与它的外部类一起使用时才有意义。

非静态成员类 每个实例都与一个外部类的外部实例相关联 。

匿名类:动态创建函数对象 创建过程对象 静态工厂方法的内部

局部类 :在任何“可以声明局部变量的地方” 都可以声明局部类

后3个称为内部类。

23 新代码中要使用泛型而不是原生态类型

无限制的通配符类型

24 消除非受检警告

25 列表优先于数组

26 优先考虑泛型方法

28 利用有限通配符来提升API灵活性

list<type1> 既不是 list<type2>的子类型,也不是它的超类型

有限的通配符类型 List<? extend E>

Collection<? super E>

PECS producer-extends consumer-super

29 优先考虑类型安全的异构容器 ??

30 用枚举代替int常量

31 用EnumSet 代理位域

32 用EnumMap 代替序数索引

35 注解优先于命名模式

36 坚持使用Override注解

38 检查参数的有效性

39 必要时进行保护性拷贝

保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝后的对象,而不是针对原始的对象。

40 谨慎的设计方法签名

谨慎地选择方法的名称。

避免过长的参数列表:

  • 创建辅助类
  • build模式

对于参数类型, 优先使用接口而不是类。

41 慎用重载

覆盖方法 代码例子

42 慎用可变参数

可变参数的每次调用都会导致进行一次数组分配和初始化。

43 返回零长度的数组或者集合,而不是null

Collections.EMPTY_SET;
Collections.EMPTY_LIST;
Collections.EMPTY_MAP;

44 为所有导出的API元素编写文档注释

Javadoc

方法的文档注释应该简洁地描述出它和客户端之间的约定

说明方法做了什么

列举出方法的前置条件 @throws @param

后置条件

类的线程安全性和可序列化性

45 将局部变量的作用域最小化

第一次使用的地方声明

try 语句

46 for each 优于 for循环

无法使用的情况:

  • 过滤
  • 转换
  • 平行迭代

47 了解和使用类库

可靠的算法库

性能不断提升

代码融入主流

Random.nextInt(int)

java.long java.util java.io

集合相关的API

java.util.concurrent

48 float 和 double 是非精确的

BigDecimal int long 进行货币计算

49 基本类型优先于装箱基本类型

基本类型只有值

装箱基本类型有null值

基本类型更节省空间和时间

何时使用装箱基本类型

  1. 集合中的元素,键 和值
  2. 类型参数
  3. 反射调用

50 如果其他类型更合适,则尽量避免使用字符串

51 当心字符串连接的性能

使用StringBuilder

52 通过接口引用对象

53 接口优先于反射机制

54 谨慎使用本地方法

55 谨慎地进行优化

要努力编写好的程序而不是快的程序

要考虑API设计决策的性能后果

在每次试图优化之前和之后,要对性能进行测量

设计API

线路层协议

永久数据格式

56 遵守普遍接受的命名惯例

57 只针对异常的情况才使用异常

异常永远不应该用于正常的控制流

设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常

如果类有状态相关的方法,则也应该有状态测试的方法或者可识别的返回值: next()方法 和 hasNext() 方法

58 可恢复的情况使用受检异常,编程错误使用运行时异常

受检异常 运行时异常 错误

非受检异常是不需要也不应该被捕获的可抛出结构

实现的所有未受检的抛出结构都应该是RuntimException的子类

59 避免不必要的使用受检的异常

60 优先使用标准的异常

IllegalArgumentException

IllegalStateException

61 抛出与抽象相对应的异常

更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常

62 每个方法抛出的异常都要有文档

javadoc 的 throws 标记

63 在细节信息中包含能捕获失败的信息

64 努力使失败保持原子性

失败的方法调用应该使对象保持在被调用之前的状态

不可变对象

检查参数有效性

恢复代码

对象拷贝

65 不要忽略异常

66 同步访问共享的可变数据

long 或者 double 类型变量的读,写不是原子的

如果读和写操作没有都被同步,同步就不会起作用

安全发布对象:

  • 保存在静态对象中;
  • 保存在volatile域,final域,或者加锁的域
  • 放到并发集合中。

67 避免过度同步

分拆锁 分离锁 非阻塞并发控制

不要在同步区域调用外来方法

同步区做尽可能少的工作

68 executor 和 task 优先于线程

69 并发工具优于 wait 和 notify

executor framework 并发集合 同步器

70 线程安全性的文档化

线程安全性有多种级别

  1. 不可变的 String
  2. 无条件的线程安全 concurrentHashMap
  3. 有条件的线程安全
  4. 非线程安全 通用的集合实现 arraylist hashmap

71 慎用延迟初始化

静态域的延迟初始化

code

实例域的延迟初始化

code

72 不要依赖于线程调度器

Thread.yield 没有可预测的语义

73 避免使用线程组

74 序列化实现

何时使用静态代码块

一个类可以使用不包含在任何方法体中的静态代码块,当类被载入时,静态代码块被执行,且只被执行一次,静态块常用来执行类属性的初始化。

对象初始化顺序

对象的初始化顺序:首先执行父类静态的内容,父类静态的内容执行完毕后,接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,父类的非静态代码块执行完毕,接着执行父类的构造方法;父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。子类的非静态代码块执行完毕再去执行子类的构造方法。总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法。

参考资料

本文是《Effective Java》读书笔记,代码会持续完善补充。


评论