Effective Java

1、用静态工厂方法代替构造器

静态工厂方法的优势:

  • 有方法名称,比起重载多个构造函数,使用者能知道不同参数构建的对象有什么不同
  • 不必每次调用都创建一个新对象(享元模式)
  • 可以返回原返回类型的任何子类型的对象(面向接口编程,可返回接口类型而不必是实现类型)
  • 返回的对象可随着每次调用而发生变化,取决于参数值(面向接口编程,隐藏实现细节,用户只知道返回的是接口类的子类具体实现类)
  • 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在

缺点:

  • 类如果不含有共有的或受保护的构造器,就不能被子类化
  • 程序员很难发现静态方法

2、遇到多个构造器参数时要考虑使用构建

使用建造者模式

3、用私有构造器或者枚举类型强化 Singeton 属性

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

对于只有静态域和静态方法的工具类,往往不需要实例化,但是默认有无参构造,通过显式指定私有的构造函数来达到这个目的

5、优先考虑依赖注入来引用资源

不要用 Singleton 和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为。应将这些资源通过工厂传给构造器,静态工厂或构建器来创建类,即依赖注入。

6、避免创建不必要的对象

优先使用静态工厂方法,而不是构造器

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

对象池技术只有在像数据连接池这样创建数据库连接代价非常昂贵的前提下,才非常有意义

7、消除过期的对象引用

只要类是自己管理内存,程序员就应该警惕内存泄漏问题

8、避免使用终结方法和清除方法

finalizer 方法

9、优先使用 try-with-resources

如果编写一个类,它代表必须被关闭的资源,那么这个类应该实现 AutoCloseable 接口

10、覆盖 equals 时请遵守通用规定

  1. 类的每个实例本质上是唯一的
  2. 类没有必要提供逻辑相等的测试功能
  3. 超类已经覆盖了 equals,超类的行为对于这个类也是合适的
  4. 类是私有的,或者是包级私有的,可以确定它的 equals 方法永远不会被调用。

实现 equals 方法的步骤:

  1. 使用 == 操作符检查,参数是否为这个对象的引用
  2. 使用 instanceof 操作符检查 参数是否为正确的类型
  3. 把参数转换成正确的类型
  4. 对于这个类中的每个关键域,检查参数中的域是否和对象中的对应域匹配

11、覆盖 equals 时总要覆盖 hashCode

因为不覆盖无法与基于散列的集合一起操作

12、始终要覆盖 toString

使类更易于调试

实际应用中,toString 方法返回对象中包含的所有值得关注的信息

13、谨慎地覆盖 clone

实现 Cloneable 接口的类是为了提供一个功能适当的公有的 clone 方法

14、考虑实现 Comparable 接口

实现之后可以跟很多泛型算法以及接口的集合实现协作

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

设计良好的组件会隐藏所有的实现细节,把 Api 与实现清晰地隔离起来。

16、要在公有类而非公有域中使用访问方法

公有类永远都不应该暴露可变的域

17、使可变性最小化

  1. 不要提供任何会修改对象状态的方法
  2. 保证类不会被扩展
  3. 声明所有的域都是 final、 声明所有的域都是私有的
  4. 确保对于任何可变组件的互斥访问

18、复合优先于继承

只有当子类真正是超类的子类型时,才适合用继承。

19、要么设计继承并提供文档说明,要么禁用继承

构造器不能调用可被覆盖的方法

20、接口优于抽象类

在装饰器模式种,接口使得安全地增强类的功能成为可能

21、为后代设计接口

接口的缺省方法

22、接口只用于定义类型

常量接口模式是对接口的不良使用

接口应该只被用来定义类型,它们不应该被用来导出常量

23、类层次优于标签类

标签类即定义枚举变量,实现多个状态

24、静态成员类优于非静态成员类

嵌套类存在的目的只为它的外围类提供服务

如果成员类不要求访问外围实例,就要始终把修饰符 static 放在它的声明中

25、限制源文件为单个顶级类

永远不要把多个顶级类或者接口放在一个源文件中

26、请不要使用原生态类型

List<E> 的原生态类型为 List

使用原生态类型,就失去了泛型在安全性和描述性方面的所有优势

27、消除非受检的警告

始终尽可能小的范围内使用 SuppressWarnings 注解

28、列表优于数组

要消除未受检的转换警告,必须选择用列表代替数组

29、优先考虑泛型

使用泛型比使用需要客户端代码中进行转换的类型来得更加安全和容易

不是使用泛型声明具体化数组如 new E[3],而应该使用 (E[])new Object[3]

30、优先考虑泛型方法

客户端转换输入参数并返回值的方法更加安全

31、利用有限制通配符来提升 API 的灵活性

Iterable<? extend E>:E 的某个子类型的 Iterable 接口

Collection<? super E>:E 的某个超类的集合

不要用通配符类型作为返回类型

32、谨慎并用泛型和可变参数

List<String>… stringLists,将值保存在泛型可变参数是不安全的

允许一个方法访问一个泛型可变参数数组是不安全的

33、优先考虑类型安全的异构容器

使用异构容器能限制每个容器只能有固定数目的类型参数

34、用 enum 代替 int 常量

每当需要一组固定常量,并且在编译时就知道其成员的时候,就应该使用枚举。枚举类型中的常量并不一定要始终保持不变。

35、用实例域代替序数

永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。

36、用 EnumSet 代替位域

正是因为枚举类型要用在集合中,所以没有理由用位域来表示它

37、用 EnumMap 代替序数索引

最好不要用序数来索引数组,而要使用 EnumMap

38、用接口模拟可扩展的枚举

虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型来对它进行模拟

39、注解优先于命名模式

有了注解就完全没有理由再使用命名模式,所有程序员都应该使用 Java 平台所提供的预定义的注解类型

40、坚持使用 Override 注解

在想要覆盖超类声明的每个方法声明中使用 Override 注解,编译器能替你防止大量的错误

41、用标记接口定义类型

标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型。标记接口能更加精确地进行锁定

42、Lambda 优先于匿名类

删除所有 Lambda 参数的类型,除非它们的存在能够使程序变得更加清晰

43、方法引用优先于 Lambda

只要方法引用更加简洁、清晰,就用方法引用,如果方法引用并不简洁就坚持使用 Lambda

44、坚持使用标准的函数接口

只要标准的函数接口能满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口,必须使用 @FunctionalInterface 注解对自己编写的函数接口进行标注

45、谨慎使用 Stream

滥用 Stream 会使程序代码更难以读懂和维护。最好避免利用 Stream 来处理 char 值。

46、优先选择 Stream 中无副作用的函数

forEach 操作应该只用于报告 Stream 计算的结果,而不是执行计算

47、Stream 要优先用 Collection 作为返回类型

对于公共的、返回序列的方法,Collection 或者适当的子类型通常是最佳的返回类型。

48、谨慎使用 Stream 并行

并行 Stream 不仅可能降低性能,还可能导致结果出错,以及难以预计的行为。

49、检查参数的有效性

每当编写方法或者构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且这个方法体的开头处,通过显式的检查来实施这些限制。

50、必要时进行保护性拷贝

Date 已经过时了,不应该在新代码中使用

51、谨慎设计方法签名

  • 谨慎地选择方法的名称。易于理解,大众认可
  • 不要过于追求提供便利的方法
  • 避免过长的参数列表
  • 对参数类型,要优先接口而不是类
  • 对 boolean 参数,要优先使用两个元素的枚举类型

52、慎用重载

调用哪个重载方法是在编译时做出决定的,永远不要导出两个具有相同参数数目的重载方法。始终可以在给方法起不同的名称,而不使用重载机制

53、慎用可变参数

不要使用可变参数

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

永远不要返回 null,而不返回一个零长度的数组或者集合,不需要追求这点性能。

55、谨慎返回 optional

56、为所有导出 API 元素编写文档注释

为了正确地编写 API 文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。方法的文档应该简洁地描述它和客户端之间的约定

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

要是局部变量的作用域最小化,最有力的方法就是在第一次要使用它的地方进行声明。几乎每一个局部变量的声明都应该包含一个初始化表达式。for 循环优先于 while 循环。

58、for each 优先于 for

以下三种情况使用 for 循环:

  • 解构过滤,遍历迭代器,删除元素,使用 removeIf 更好
  • 转换,遍历数组或列表,取代部分或全部元素值时
  • 平行迭代,并行遍历多个集合

59、了解和使用类库

通过使用标准类库,可以充分利用这些编写标准类库的专家知识,以及在你之前的其他人的使用经验。不再推荐使用 Reandom 而是用 ThreadLocalReandom。每个程序员都应该熟悉,java.lang、java.util、java.io 及其子包中的内容

60、如果需要精确的答案,请避免使用 float 和 double

float 和 double 类型尤其不适合货币计算,应该使用 BigDecimal、int 或者 long 进行货币计算

61、基本类型优先于装箱基本类型

自动装箱减少了使用装箱基本类型的繁琐性,但是并没有减少它的风险

62、如果其他类型更适合,则避免使用字符串

字符串不适合代替其他的值类型

63、了解字符串连接的性能

为了更高的性能,请用 StringBuilder 代替 String。

64、通过接口引用对象

如果合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。使用 List、Set、Map 而不是 ArrayList、HashSet、HashMap。

65、接口优先于反射机制

66、谨慎地使用本地方法

使用本地方法提高性能不值得提倡

67、谨慎地进行优化

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

68、遵守普遍接受的命名惯例

不可实例化的工具类经常使用复数名词。使用大家公认的做法。

69、只针对异常的情况才使用异常

异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。设计良好的 API 不应该强迫它的客户端为了正常的控制流而是用异常。

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

如果期望调用者能够适当地恢复,对于这种情况就应该使用受检异常。用运行时异常来表示编程错误。大多数的运行时异常都表示前提违例。前提违例是 API 的客户端没有遵守 API 规范建立的约定。所有未受检的抛出结构都应该是 RuntimeException 的子类。

71、避免不必要地使用受检异常

72、优先使用标准的异常

不要直接重用 Exception、RuntimeException、Throwable 或者Error

73、抛出与抽象对应的异常

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

74、每个方法抛出的所有异常都要建立文档

始终要单独地声明受检异常,利用 @throws 准确地记录下抛出的每个异常的条件

75、在细节信息中包含失败-捕获信息

为了捕获失败,异常的细节信息应该包含对该异常有贡献的所有参数和域的值。

76、努力使失败保持原子性

一般而言,失败的方法调用应该使对象保持在被调用之前的状态。

77、不要忽略异常

空的 catch 块会使异常达不到应有的目的。如果选择忽略异常,catch 块应该包含一条注释,说明为什么可以这么做,并且变量应该被命名为 ignored

78、同步访问共享的可变数据

为了线程间进行可靠的通信,也为了互斥访问,同步是必要的。千万不要使用 Thread.stop 方法,一个线程组织另一个线程建议的做法是轮询 boolean 值。

除非读和写操作都被同步,否则无法保证同步能起作用。将可变数据限制再单个线程中。

79、避免过度同步

为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。应该在同步区域内做尽可能少的工作。

80、executor、task 和 stream 优先于线程

81、并发工具优先于 wait 和 notify

应该优先使用 ConcurrentHashMap 而不是 Collections.synchronizedMap

对于间歇式的定时,始优先使用 System.nanoTime 而不是 System.currentTimeMillis

82、线程安全性的文档化

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别

  • 不可变的
  • 无条件的线程安全
  • 有条件的线程安全
  • 非线程安全
  • 线程对立

lock 域应该始终声明为 final

83、慎用延迟初始化

延迟初始化是指延迟到需要域的值才将它初始化的行为。

如果利用延迟优化来破坏初始化的循环,就需要使用同步访问方法

如果出于性能的考虑对静态域使用延迟初始化就是用静态内部类方法

如果处于性能的考虑而需要对实例域使用延迟初始化,就用双重检查方法

84、不要依赖于线程调度器

任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的

如果线程没有做有意义的工作,就不应该运行

85、其他方法优先于 Java 序列化

避免序列化攻击的最佳方式是永远不要反序列化任何东西。在新编写的任何系统中没有理由再使用 Java 序列化。

86、谨慎地实现 Serializable 接口

实现 Serializable 接口而付出的最大代价是,一旦一个类发布,就大大降低了改变这个类的实现的灵活性。

87、考虑使用自定义的序列化形式

如果事先认真考虑默认的序列化形式是否合适,则不要贸然接受。

即使你确定了默认序列化形式是否合适的,通常还必须提供一个 readObject 方法以约束关系和安全性

88、保护性地编写 readObejct 方法

89、对于实例控制,枚举类型优先于 readResolve

90、考虑序列化代理代替序列化实例


Effective Java
https://reajason.vercel.app/2022/01/24/Effective Java/
作者
ReaJason
发布于
2022年1月24日
许可协议