本书介绍了在Java编程中78条极具实用价值的经验规则, 这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。通过对Java平台设计专家所使用的技术的全面描述, 揭示了应该做什么, 不应该做什么才能产生清晰、健壮和高效的代码。
本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。可作为技术人员的参考用书。
第二版 78条,含盖了5.0和6.0。

创建和销毁对象:

  1. 考虑用静态工厂方法代替构造器
  2. 遇到多个构造器参数时要考虑用构建器
  3. 用私有构造器或者枚举类型强化Singleton属性
  4. 通过私有构造器强化不可实例化的能力
  5. 避免创建不必要的对象
  6. 消除过期的对象引用
  7. 避免使用终结方法

对于所有对象都通用的方法

  1. 覆盖 equals 时请遵守通用约定
  2. 覆盖 equals 时总要覆盖 hashCode
  3. 始终覆盖 toString
  4. 谨慎的覆盖 clone
  5. 考虑实现 Comparable 接口

类和接口类和接口是程序设计语言的核心,主要是供程序员来设计类和接口

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

  2. 在共有类中使用访问方法而非公有域

  3. 使可变性最小化为了使类成为不可变, 遵循五条规则

    • 不要提供任何会修改对象的方法
    • 保证类不会被扩展
    • 使所有域都是final
    • 是所有域都是私有的
    • 确保对任何可变组件的互斥访问
  4. 复合优先于继承

  5. 要么为继承而设计, 并提供文档说明, 要么禁止继承说明文档应以 This implenmentation...

  6. 接口优于抽象类 接口和抽象类: 抽象类允许包含某些方法的实现, 但接口不允许

    • 现有的类可以很容易被更新, 以实现新的接口
    • 接口是定义混合类型的理想选择
    • 接口允许我们构造非层次结构的类型框架: public interface SingerDancer extend Singer,Dancer{}
  7. 接口只用于定义类型

    • 常量接口是对接口的不良使用( java.io.ObjectIntreamContast 一个接口只定义了一个静态的变量)
  8. 类层次优于标签类

    • 标签类包含了枚举声明, 标签域以及条件语句乱七八糟挤在一个类中, 可读性差, 效率低, 用类层次代替, 更好的反应类型本质上的层次挂系
  9. 用函数对象表示策略

    • 通过传递不同比较器函数, 就可以获得各种不同排列顺序是策略模式的例子之一
    • 函数指针的用途主要是实现策略模式
  10. 优先考虑静态成员类

    • 嵌套类有四种: 静态成员类、非静态成员类、匿名类和局部类
    • 如果嵌套类在单个方法之可见的, 需要用成员类
    • 如果每个成员类的每个实例都需要指向其外围实例的引用,就用成员类的非静态的, 否则用静态的
    • 假设一个嵌套类属于一个方法的内部, 只在一个地方创建实例, 且已有一个预置类型可以说明这个类的特征, 就用做匿名类, 否则用局部类

泛型如何处理泛型,使其尽可能的简化

  1. 请不要在新代码中使用原生态类型

    • 原生态类型在运行时容易异常, 例: Set<Object> 参数化类型,表示包含任何对象类型的集合; Set<?> 通配符类型,表示只能包含某种未知对象类型的一个集合; Set 是原生态类型, 脱离了泛型系统; 前两者安全
    • 有个例外: 泛型在运行中可被擦除,在”类文字”中必须使用原生态类型,不能使用参数化类型
      1
      2
      3
      例如: 
      list.class, String[].class, 和int.class 合法,
      但是List<String.class>和List<?>.class 不合法
  2. 消除非受检警告

    • 非受检强制化警告, 非受检方法调用警告, 非受检普通数组创建警告, 以及非受检转换警告
    • 如果无法消除警告, 用 @SuppressWarings("unchecked"), 该注解可以用在任何粒度的级别中, 尽可能在小的范围使用,把注释原因记录下来
  3. 列表优先于数组

    • 数组和反省有不同的类型规则: 数组是协变且具体化的;泛型是不可边的饿且可以被擦除; 数组和泛型提供了运行时的类型安全但没有编译时的类型安全, 数组和泛型不能很好的混合使用, 第一反应应该是用列表代替数组
  4. 优先考虑泛型

    • <E extends Delayed> 泛型, 比进行转换的类型来的更安全, 也更容易, 把类做成泛型
  5. 优先考虑泛型方法

    • 泛型方法显著特征: 不许明确指定类型参数的值, 不像调用泛型构造器的时候必须指定
    • 泛型方法像泛型一样, 比转换输入参数并返回值的方法更安全
  6. 利用有限制通配符来提升API的灵活性

    • 如果类型参数只在方法生命中出现一次, 可以用通配符取代: 如果是无限制的类型参数, 就用无限制的通配符取代, 反之, 用有限制的通配符
  7. 优先考虑类型安全的异构容器

    • 限制每个容器只能有固定数目的类型参数, 可以通过将类型参数放在键上而不是容器上. 对于安全的异构容器,可将 class 对象作为键, 以使用的 class 对象称作类型令牌
      例如: 用 DatabaseRow 类型表示一个数据行(容器),用泛型 Column<T> 作为它的键

枚举和注解主要介绍了两种类型: 枚举类型,注解类型

  1. enum 代替 int 常量

    • 如果多个枚举同时共享相同行为, 考虑枚举策略
  2. 用实例域代替序数

    • 不要根据枚举的序数导出与它关联的值, 而是将它保存在一个实例域中
  3. 用EnumSet代替位域

    • 位域: 将几个常量合并到一个集合中, 称为位域
    • 因为枚举类型要用在集合Set中, 所以不用位域表示
  4. 用EnumMap代替序数索引

    • 不要使用序数来索引数组,使用 EnumMap, 例如: 当你表现的 关系是多维的使用 EnumMap<...,EnumMap<...>>
  5. 用接口模拟可伸缩的枚举

    • 虽然不能编写可扩展的枚举类型,但是可以通过编写接口实现该接口的基础枚举类型
  6. 注解优先于命名模式

    • 命名模式的缺点: 拼写错误、无法确保它们只用于响应的程序元素上. 未提供将参数值与程序元素关联起来的好方法
    • 只需使用 java 平台提供的注解类型
  7. 坚持使用Override注解

    • 在每个方法声明使用 Override 注解来覆盖超类声明
  8. 用标记接口定义类型

    • 标记接口: 不包含方法声明的接口,只是声明一个类实现了某种属性的接口
    • 标记接口胜过标记注解:
      ① 标记接口定义的类型是由标记类的实例实现的; 标记注解没有定义这样的类型
      ② 标记接口更加精确的进行锁定
    • 标记注解的胜过标记接口的优点:
      ① 通过默认的方式添加一个或者多个注解类型元素, 给已被使用的注解类型添加更多信息
      ② 是注解机制的一部分, 在支持注解在框架中具有一致性
      
    • 定义一个新方法且没有关联, 使用标记接口
    • 想要标记程序元素而非类和接口, 考虑以后要添加其他信息或者标记要适合广泛使用了注解类型的框架, 使用标记注解
    • 主要表明了, 要想定义类型, 一定使用接口

方法

  • 主要是方法设计的几个方面: 如何处理参数和返回值, 如何设计方法签名, 如何未方法编写文档

  • 本章大多数适用于构造器和普通方法

  • 焦点集中在可用性、健壮性和灵活性

  1. 检查参数的有效性

    • 编写方法或者构造器时, 需考虑参数有哪些限制, 把限制写在文档中, 在这个方法的开头处, 通过显示的检查实施这些限制
  2. 必要时进行保护性拷贝

    • 如果类具有客户端得到或者返回到客户端的可变组件, 类就必须保护性地拷贝这些组件. 如果拷贝的成本受限, 并且类信任它的客户端不会不恰当的修改组件, 就可以在文档中指明客户端的职责是不得修改受到影响的组件, 来代替保护性的拷贝
  3. 谨慎设计方法签名

    • 谨慎地选择方法的名称: 易于理解, 在同一个包中的其他名称风格一致的名称, 大家都认可的名称
    • 不要过于追求提供便利的方法
    • 避免过长的参数列表: 缩短长参数列表有三种方法:
      ① 把方法分解成多个方法, 每个方法只需这些参数的一个子集, 例如: List 接口,提供了 sublist 方法, 这个方法有两个参数, 并返回列表的一个视图,可以与 indexOf 或者 lastIndexOf 结合起来
      ② 创建辅助类: 用来保存参数的分组, 辅助类一般未静态成员类,若一个频繁出现的参数序列可以被看做是代表某个独特的实体, 建议使用这种方法
      ③ 从对象构建到方法调用都用 Builder 模式: 如果方法中有多个参数最好定义一个对象表示所有参数
    • 对于参数类型, 优先使用接口而不是类
    • 对于 boolean 参数,优先使用两个元素的枚举类型
  4. 慎用重载

    • 一般情况下, 对于多个具有相同参数数目的方法来说,尽量避免重载方法
    • 至少应该避免: 同一组参数只需经过类型转换就可以传递给不同的重载方法
    • 如果不能避免, 例如正在改造一个现有的类以实现新的接口, 应保证: 当传递同样的参数时, 所有重载方法的行为必须一致
  5. 慎用可变参数

    • 可变参数的灵活性: 如确定某个方法95%的调用会有3个或者更少的参数, 就应声明方法的5个重载, 每个重载方法带有0到3个普通参数, 参数超过3个就可以使用可变参数的方法
  6. 返回零长度的数组或者集合,而不是 null

    • 返回类型为数组或集合的方法没理由返回 null, 而不是返回一个零长度的数组或者集合
  7. 为所有导出的API元素编写文档注释

    • 为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释
    • 方法的文档注释应该简洁的描述出它与客户端之间的约定. 应该列举出这个方法的前提条件和后置条件(一般情况下前提条件是由@throws标签针对 未受检的异常描述,也可以在@params标记中指定前提条件;后置条件: 调用成功完成之后,哪些条件必须满足).还应该描述它的副作用,以及描述类或者方法的线程安全性
    • 类、接口和域,概要描述应该是一个名词短语, 描述了该类或者接口的实例, 或者域本身所代表的事物
    • 需注意的: 泛型、枚举和注解: 泛型或者方法编写文档时, 确保在文档中说明所有的类型参数; 为枚举类型编写文档时, 确保在文档中说明常量、以及类型和任何公有的方法; 为注解类型编写文档时, 确保在文档中说明所有成员和类型本身
    • 类的导出 API 的两个特征即安全性和可序列化性
    • 采用一致性的风格来遵循标准的约定(在文档注释内部出现任何HTML标签都是允许的,但是HTML元字符必须要经过转义)

通用程序设计主要是 Java 语言的具体细节, 局部变量、控制结构、类库的用法、各种数据类型的用法, 以及两种不是语言本身提供的机制(反射和本地方法)的用法.

45、将局部变量的作用域最小化
* 要使局部变量的作用域最小化, 最有理的方法是在第一次使用它的地方声明, 使方法小而集中
* 几乎每个局部变量的声明都应该包含一个初始化的表达式
* 在循环变量和块区域外不要再次使用该变量

  1. for-each循环优先于传统的for循环

    • 如果编写的类型是一组元素,要选择的接口: CollectionIterable
    • 有三种情况无法使用 for-each: 过滤(遍历集合删除指定元素, 要使用显示迭代器)、转换(若要遍历, 取代它部分或者全部的元素值, 需要使用列表迭代器或者数组索引, 以便设定元素的值)、平行迭代(若需要并行地遍历多个集合, 需要显示的控制迭代器或者索引变量, 以便所有迭代器和索引变量都可以得到同步前移)
  2. 了解和使用类库

    • 使用标准类库的好处: 正确性、时间花在程序上而不是底层细节、性能不断提高、是自己的代码融入主流
    • 不要重新发明轮子
  3. 如果需要精确的答案,请避免使用 floatdouble

    • floatdouble 类型不适合用于货币计算, 需要正确的解决需要使用 BigDecimal, int 或者 long 进行货币计算( BigDecimal 缺点: 与基本类型相比不方便)
    • 若性能很关键, 不介意自己记录十进制小数点,使用 int; 如果不超过18位使用 long; 若超过18位,使用 BigDecimal
  4. 基本类型优于装箱基本类型

    • 区别: ① 基本类型只有值,而装箱基本类型具有与他们的值不同的同一性
        ② 基本类型只有功能完备的值而每个装箱基本类型除了他对应的基本类型的所有功能值之外, 还有非功能值 `null` 
        ③基本类型通常比装箱基本类型更节省空间和时间
      
    • 使用装箱基本类型的情境: 作为集合中的元素、键和值, 不能将基本类型放在集合中, 所以要使用基本类型; 在参数化类型中, 要使用装箱基本类型作为类型参数, 因为Java不允许使用基本类型
    • 在可以选择的时候基本类型优于装箱基本类型
  5. 如果其他类型更合适,则尽量避免使用字符串

    • 字符串不适合代替其他的值类型、字符串不适合代替枚举类型、字符串不适合代替聚集类型(实体)、字符串不适合代替能力表(利用提供的字符串键, 对每个线程局部变量的内容进行访问授权)
  6. 当心字符串连接的性能

    • 将多个字符串合并为一个这字符串来表示一个较小的大小固定的对象, 使用操作符很合适;但不适合大规模的场景中, 例如为连接n个字符串而重复的使用字符串操作符, 需要n的平方级的时间
    • 若数量大, 考虑性能, 使用 StringBuilder(的 append 方法)代替 String
  7. 通过接口引用对象

    • 若有合适的接口类型在, 对于参数、返回值、变量和域来说, 都应该使用接口类型进行声明
    • 若没有合适的接口在, 完全可以用类而不是接口来引用对象
  8. 接口优于反射机制

    • 反射机制的缺点: 丧失了编译时检查的好处、执行反射访问所需要的代码非常笨拙和冗长、性能损失
    • 使用反射机制场景: 类浏览器、对象监视器、代码分析工具、解释性的内嵌式系统以及远程调用
  9. 谨慎的使用本地方法

    • 本地方法指: 用本地的程序设计语言编写的特殊方法
  10. 谨慎的进行优化

    • 要努力编写好的程序而不是快的程序、在未绝对清晰的优化方案前,不要优化、避免那些限制性能的设计决策、考虑API设计决策的性能后果、在每次试图优化前和后,要对性能进行测量
  11. 遵守普遍接受的命名惯例

    • 包名应该是层次状的用句号分隔每个部分, 每个部分包括小写子母和数字, 很少使用数字, 包名包括一个或者多个描述该包的组成,但组成应简短, 通常不超过8个字符, 使用缩写也可以接受
    • 类和接口的名称, 包括枚举和注解类型的名称都应该包括一个或者多个单词,每个单词的首字母大写
    • 方法和域的名称与类和接口的名称一样, 只不过方法或者域名第一个字母小写; 特例: 常量包含一个或者多个大写的单词并用下划线分隔
    • 参数类型名城管通常由单个字符组成, T 表示任意的类型, E 表示集合的元素类型, KV 表示映射的键值, X 表示异常
    • 执行动作的方法通常用动词或动词短语命名
    • 返回布尔值, 名称可以 is 开头, 很少用 has, 后面跟名词或名词短语….

异常

不应该用于正常的控制流、良好的API不因该强迫客户端为了正常的控制流而使用异常

  1. 只针对异常情况才使用异常

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

    • 受检异常: 期望调用者能够适当的恢复
    • 运行时异常和错误: 用运行时异常表明编程错误
  3. 避免不必要的使用受检异常

    • 过分使用受检异常会使 API 使用不方便
  4. 优先使用标准的异常

    • 重用现有异常的好处: 使 API 更加易于学习和使用、可读性更好、异常类越少, 意味着内存印迹就越小, 装载这些类的时间开销也越少
    • 经常重用的异常: IllegalStateException (参数值不合适)、 IllegalStateException (接受对象的状态使调用非法)、 NullPointerExceptionIllegalArgumentException; ConcurrentModificationException(如果对象被设计为专用于单线程或者外部同步机制配合使用,一旦被并发的修改,会抛出此异常); UnsupportedOperationException(对象不支持所请求的操作)
  5. 抛出与抽象相对应的异常

    • 现象: 若抛出的异常与所执行的任务没有任何联系, 当方法传递由低层抽象抛出的异常
    • 异常转译: 更高层的实现应该捕获低层的异常, 同时抛出可以按照高层抽象进行解释的异常
    • 若不能阻止或处理来自低层的异常, 一般做法是异常转译, 除非低层方法可以保证它抛出的所有异常对高层也合适将异常从低层传播到高层
  6. 每个方法抛出的异常都要有文档

    • 声明受检异常, 并利用 Javadoc@throws 标记,准确的记录抛出异常的异常条件
  7. 在细节消息中包含能捕获失败的信息

    • 异常的细节信息应该包含所有 “对该异常有贡献”的参数和域的值
  8. 努力使失败保持原子性

    • 现象: 对象抛出异常后,希望这个对象仍然保持良好的可用状态中, 即使是发生在某个操作的过程中
    • 失败的方法调用应该使对象保持在被调用之前的状态, 具有这种属性的方法被称为失败原子性
    • 途径: 在执行操作前检查参数的有效性, 总之, 产生的异常应该让对象保持在该方法调用之前的状态
  9. 不要忽略异常

    • 空的catch块使异常达不到应有的目的
    • 可以忽略异常的情况: 关闭 FileInputStream 的时候, 因为未改变文件状态, 不必执行任何恢复动作

并发

当多个线程共享可变数据的时候, 每个读或者写数据的线程都必须执行同步

不要使用 Thread.stop

  1. 同步访问共享的可变数据

  2. 避免过度同步

    • 会导致性能降低、死锁、不确定行为
  3. executortask 优先于线程

    • 线程池: 想让不止一个线程处理队列的请求, 只要调用一个不同的静态工厂, 这个工厂创建了一种不同的 executor service, 称作线程池
    • 小程序或轻载的服务器,使用 Executors.newCachedThreadPool 是个不错的选择, 不需要配置;大负载的服务器缓存的线程池就不是很好的选择, 直接使用 ThreadPoolExecutor 的类
    • Task 有两种: Runnable 及近亲 Callable (与Runable类似,但有返回值), 执行任务的机制是 exector service, 有很大的灵活性
    • Executor Feamework 可以代替 TimerScheduledThreadPoolExecutor, 区别: 被调度的线程池executor更灵活, timer 只用一个线程执行任务, 面对长期运行的任务时,会影响定时的准确性
  4. 并发工具优先于 waitnotify

    • java.util.concurrent 更高级的工具分成三类: Executor Framewok、并发集合(Concurrent Collection)以及同步器(Synchronizer)
    • ConcurrentHashMap 提供并发性外速度也快; 同步器 Synchronizer 是一些使线程能够等待另一个线程的对象, 允许协调运作, 常用的同步器是 CountDownLatchSemaphore
    • 倒计数锁存器是一次性障碍, 允许一个或多个线程等一个或多个线程做某些事, 唯一的构造器带有int类型参数
    • 对于间歇式定时, 始终应该优先使用 System.nanoTime, 不受系统实时时钟的调整所影响,而不是 System.currentTimeMills
    • 务必确保始终是利用标准的模式从 while 循环内部调用 wait, 一般情况使用 notifyAll, 使用 notify 要小心
  5. 线程安全性的文档化

    • 不可变(immutable)、无条件的线程安全(unconditionally thread-safe)、有条件的线程安全(conditionally thread-safe)、非线程安全(not thread-safe)、线程对立的(thread-hostile)
    • 有条件的线程安全类必须在文档中说明 “哪个方法调用序列需要外部同步, 以及在执行这些序列的时候要获得哪把锁”
  6. 慎用延迟初始化

    • 延迟初始化: 是延迟到需要域的值时才将它初始化的这种行为, 它像一把双刃剑降低了初始化类或者创建实例的开销但却增加了访问延迟初始化的域的开销
    • 对于实例域, 使用双重检查模式, 对于静态域, 使用延迟初始化域
      1
      2
      3
      4
      5
      6
      7
         private final FiledType filed = computerFileValue();
      或 private FiledType filed;
      synchronized FiledType getFiled(){
      if(filed == null){
      filed = computerFiledValue();
      return filed;
      }}
  7. 不要依赖于线程调度器

    • 不要依赖 Thread.yield 或者线程优先级
  8. 避免使用线程组

    • 线程组的初衷是作为一种隔离 applet(小程序)的机制
    • 线程不推荐使用, 若设计的类需要处理线程的逻辑组, 使用线程池的 executor

序列化

  1. 谨慎地实现Serializable
    • 实现 Serializable 接口而付出的最大的代价, 一旦一个类被发布, 就大大降低了”改变这个类的实现”的灵活性
    • 实现 Serializable 的第二个代价, 增加了出现 bug 和安全漏洞的可能性
    • 实现 Serializable 的第三个代价, 随着类发行新的版本, 相关的测试负担也增加了
    • 内部类不应该实现 Serializable
    • 提供一个可访问的无参构造器, 这种允许子类实现 Serializable
  1. 考虑使用自定义的序列化形式
    • 如果没有认真考虑默认的序列化形式是否合适, 不要贸然接受
    • 如果一个对象的物理表示法等同于它的逻辑内容, 可能就适合默认的序列化形式
    • 如果有实质性区别时,有很多缺点: 它使这个类的导出API永远地束缚在该类的内部表示法上; 消耗过多的空间、消耗过多的时间、引起栈溢出
    • 当决定类可实例化的时候, 确认好要采用什么样的序列化形式, 只有当默认的序列化形式能够合理地描述对象的逻辑状态时, 才使用默认的序列化形式, 否则就要设计一个自定义的序列化形式, 合理的描述对象的状态
  1. 保护性的编写 readObject 方法
    • 当一个对象被反序列化的时候, 对于客户端不应该拥有的对象引用, 如果哪个域包含了这样的对象引用, 就必须要做保护性的拷贝, 这是非常重要的
    • 健壮readObject的方法:
      ① 对于对象引用域必须保持为私有的类, 要保护性的拷贝这些域中的每个对象, 不可变类的可变组件属于这一类别
      ② 对于任何约束条件, 若检查失败, 抛出一个 InvalidObjectException 异常, 检查动作应跟在所有的保护性拷贝之后
      ③ 若整个对象图在被反序列化后, 必须进行验证, 应该使用 ObjectInputValidation 接口
  1. 对于实例控制, 枚举类优于 readResolve
    • 尽可能使用枚举类型来实施实例控制的约束条件, 若做不到, 同时又需要一个即可序列化又是实例受控的类, 就必须提供一个 readResolver 方法, 确保该类的所有实例域都为基本类型或者 transient
  1. 考虑用序列化代理代替序列化实例
    • 序列化代理模式也有局限性
    • 当你发现必须在一个不能被客户端扩展的类上编写 readObject 或者 writeObject 方法的时候,应该考虑使用序列化代理模式