[TOC]

创建和销毁对象

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

  • 返回类实例的静态方法
  • 与构造器相比优势:
    • 有名称(相对于参数不同的多个构造器)
    • 调用时不用创建一个新对象
    • 可以返回原返回类型的任何子类型对象(类似工厂模式)
    • 创建参数化类型实例时代码简洁
      • Map m = new HashMap()
      • Map m = HashMap.newInstance();
  • 缺点:
    • 使用静态工厂方法来创建对象,将构造器设为私有,不能被子类化。(鼓励使用复合,避免继承)
    • 和静态方法没有区别

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

  • 构造器:
    • 多参数时使用重叠构造器
    • 缺点:不够灵活,同时参数多了无法阅读相应参数的含义
  • javaBeans:
    • 调用无参构造函数,然后调用set方法来赋值
    • 缺点:构造过程分到了几个调用中,会导致线程不安全
  • Builder模式

    • 容易编写,易于阅读
    • 构造器或静态工厂有多个参数,考虑使用Builder

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      public class Test {
      private int a;
      public static class Builder {
      private int a;
      public Builder(int a) {
      this.a = a;
      }
      public Builder a(int a) {
      this.a = a;
      }
      public test build() {
      return new Test(this);
      }
      }
      private Test(Builder builder) {
      a = builder.a;
      }
      }
      获取实例: Test test = new Test.Builder(1).a(1).build();

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

单例模式

1
2
3
4
5
6
7
8
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
调用:
Elvis e = Elvis.INSTANCE

4. 不希望被实例化的类(工具类等),使用私有的构造器来防止被实例化。(不能子类化)

5. 避免创造不必要的对象

  • 尽量重用对象
  • 对于不可变类或已知不会修改的可变对象,静态工厂方法优于构造器,构造器每次都创建新对象。
  • 优先使用基本类型而不是装箱基本类型(自动装箱有一定的成本)

6. 消除过期对象引用

  • 防止内存泄露(无意识的对象保持)
  • 对象引用已经过期,手动指定为null
  • 当缓存项生命周期由外部引用决定时,考虑使用WeakHashMap实现缓存。(没有引用时自动删除)
  • 清除缓存中老的对象,使用LinkedHashMap.removeEldestEntry方法在添加新对象时进行清理。
  • 监听器和其他回调也是内存泄露的来源,通过保存弱引用来避免。

7. 避免使用终结方法(finalizer)

  • 问题:finalizer不能保证被及时的执行
  • 替换方案:通过显示的终止方法来替代finalizer(try-finally)
  • finalizer可以充当安全网,忘记显示终止时,保证资源的释放(不建议依赖,建议修改代码)

所有对象都通用的方法

非final的Object方法相关的约定

8. equals的通用约定

  • 尽量避免覆盖equals方法
  • 不需要覆盖equals方法的情况:
    • 类的实例本质上都是唯一的,如thread,枚举
    • 类不需要提供逻辑相等的功能,如Random
    • 超类覆盖了equals方法,且对子类适用
    • equals不会被调用,类私有或是包级私有
  • 值类的情况下需要覆盖equals方法。(未满足不需要覆盖的情况)
  • 覆盖equals方法需要遵守的约定:
    • 自反性(reflexive):x.equals(x) == true
    • 对称性(symmetric):x.equals(y) == true && y.equals(x) == true
    • 传递性(transitive):x.equals(y) == true && y.equals(z) == true ==> x.equals(z) == true
    • 一致性(consistent):多次调用 x.equals(y) 都为true
    • x非空,x.equals(null) == false
  • 实现高质量equals的原则:
    • 使用==操作符检查“参数是否为这个对象的引用”
    • 使用instanceof操作符检查参数是否为正确的类型。
    • 参数转换成正确的类型进行比较
    • 保证每个关键域都匹配
    • 编写的equals保证对称性,传递性,一致性
  • 其他告诫:
    • 覆盖equals同时覆盖hashCode
    • 不要企图让equals过于智能
    • 不要把equals声明中的Object对象替换为其他的类型

9. 覆盖equals时同时覆盖hashCode

  • 覆盖hashCode的原则:
    • 如果equals用到的信息没有被修改,多次调用hashCode返回同一个整数
    • equals相等的对象必须具有相等的hashCode(防止出现equals相等hashCode不相等)
    • equals不相等的对象不一定要返回不相等的hashCode,但是尽量保证不同能提高hash table的性能(hashmap 使用hashCode生成key)

10. 始终覆盖toString

  • toString方法返回对象中值得关注的信息,更容易阅读

11. 谨慎覆盖clone

  • clone不调用构造器,生成出来的对象不能对原对象产生影响(deep clone)
  • 实现Cloneable接口的类都应该用一个公有方法覆盖clone,此方法先调用super.clone,然后修正部分域(deep clone)。
  • 尽量避免覆盖clone,拷贝对象应采用deep clone。

12. 考虑实现comparable接口

  • 使实例具有被排序的能力(集合实现排序依赖)

类和接口

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

  • 封装:隐藏实现细节,API与实现隔离开,模块间只通过API通信。有效解除各模块之间的耦合。
  • 访问控制机制:
    • 尽可能使每个类或者成员不被外界访问,明确成员的最小访问级别(私有,包级私有,受保护(子类+包内访问),公有)
    • 实例域不能是公有的。
    • 静态final域可以设置为公有来暴露常量,该域不能指向可变对象的引用。

14. 公有类中使用访问方法而非公有域

  • 公有类不改暴露可变的域,暴露不可变的域危害小。

15. 使可变性最小化

  • 多使用不可变类 = 实例不能被修改,包含信息创建时提供。
  • 不可变类遵循规则:
    • 不提供修改对象状态的方法
    • 类不能被扩展,子类化
    • 所有域都是final的
    • 所有域都私有
    • 确保对任何可变组件的互斥访问

16. 复合优先于继承

  • decorator模式
  • 继承打破了封装性,子类依赖超类,超类修改可能会使子类异常。只有当子类和超类之间确实存在子类型的关系时,才适用继承。
  • 同个包内合理的设计,来通过继承是比较安全的(但仍然有风险)。跨包继承则很危险。
  • 复合:新的类中增加一个私有域,引用现有类的一个实例,调用该实例的方法来进行转发。尽量用复合和转发来替代继承。

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

18. 接口优于抽象类

  • 抽象类允许包含某些方法的实现,接口则不允许。
  • 接口是定义混合类型的理想选择,允许我们构造费层次结构的类型框架。接口使得安全地增强类的功能成为可能。
  • 设计公有接口要谨慎,因为一旦被广泛使用,再想改变接口几乎是不可能的(会导致所有实现类编译出错)
  • 提供接口的同时,考虑同时提供骨架实现类(类似AbstractCollection)

19. 接口只用于定义类型

  • 接口应该只被用来定义类型,不应该用来导出常量。
  • 使用枚举类或者不可实例化的工具类来导出常量。

20. 类层次优于标签类

  • 标签类:多种不同风格的实现通过枚举挤在单个类中。如一个图案类既可以是圆也可以是正方形。标签类过于冗长、容易出错,并且效率低下。
  • 不使用标签类,通过抽象转换为类层次结构。

21. 用函数对象表示策略

  • 使用策略模式

22. 优先考虑静态成员类

  • 嵌套类:定义在一个类内部的类,为了给外围类提供服务。分为静态成员类,非静态成员类,匿名类,局部类。
  • 声明成员类不要求访问外围实例时,使用静态成员类,否则使用非静态成员类。
  • 匿名类常用来创建函数对象(策略模式),过程对象(Runnable、Thread),静态工厂方法(18条)

泛型


(未很好理解)

23. 不要使用原生态类型

  • 原生态类型没有类型安全性检查,表述性也不如泛型。尽量使用泛型,避免是哟经原生态类型。

24. 消除非受检警告

  • 尽可能消除每个非受检警告(unchecked warnings),确保代码的类型安全。
  • 对于无法消除警告,但是可以证明引起警告的代码是类型安全的,可以用@SuppressWarnings(“unchecked”)注解来禁止该条警告。尽可能在小范围中使用SuppressWarnings注解。每次使用时,都要添加注释,说明为什么是安全。

25. 列表优先于于数组

26. 优先考虑泛型

  • 通过将对象泛型化来强化对象。
  • 参考stack的实现

27. 优先考虑泛型方法

  • 静态工具方法适合泛型化,如Collections包含的算法方法。

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

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

  • 使用Class 作为key,构建一个value值是不同类型的类map类。

枚举和注解

30. 用enum代替int常量

  • 有多种不同类型的int枚举常量时,使用起来可能会用混,而enum可以避免这个问题。
  • 枚举是final和单例的。枚举类型允许添加任意的方法和域,并实现任意的接口。(参考RoundingMode)

31. 用实例域代替序数

  • 避免使用ordinal方法获取序数,而是增加实例域用来保存枚举值关联的值。
  • ordinal方法仅在设计像EnumSet和EnumMap这样的类的时候才使用。

32. 用EnumSet代替位域(bit field)

  • 位域可读性差
  • EnumSet 内部也是采用位运算的方式实现

33. 用EnumMap代替序数索引

  • 不要用序数来索引数组,使用EnumMap(不要使用ordinal)

34. 用接口模拟可伸缩的枚举

  • 枚举实现接口

35. 多使用注解

36. 坚持使用Override注解

  • Override只用在方法声明中,表示被注解的方法声明覆盖了超类型中的一个声明
  • 对要覆盖超类声明的每个方法声明都必须使用Override注释(覆盖抽象方法时除外)

37. 用标记接口定义类型

  • 标记接口,没有方法声明的接口,只是为了指明一个类具有某种属性(Serializable)
  • 标记接口用于定义类型,标记注解用来给注解元素添加更多信息。

方法

38. 检查参数的有效性

  • 方法或构造器的参数限制写在文档中,同时进行显式的检查,尽量减少因参数不符合限制而产生的异常。

39. 必要时进行保护性拷贝

  • 对于构造器的每个可变参数进行保护性拷贝。

40. 谨慎设计方法前面

  • 方法名称易于理解
  • 不要提供太多方法,提供功能齐全方法为主,有常用的情况才提供快捷方式
  • 参数列表不要太长,考虑最多4个
  • 参数类型优先使用接口
  • 两个元素的枚举类型优于boolean参数

41. 慎用重载

  • 永远不要导出两个具有相同参数数目的重载方法

42. 慎用可变参数

  • int… args(通过创建一个数组来传递参数列表)

43. 返回零长度的数组或者稽核,而不是null

  • 返回类型为数组或集合的方法没理由返回null,避免上层还需要对null做额外的处理

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

  • 在每个被导出的类、接口、构造器、方法和域声明之前增加文档注释
  • 注释简洁描述出它和客户端之间的约定。(条件,副作用)

通用程序设计

45. 局部变量作用域最小化

  • 第一次使用的地方声明局部变量
  • 方法小而集中

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

  • 传统的for循环迭代器和索引变量出现多次,可能会出现编译器无法发现的错误。相对于for-each有性能损失。(for-each对数组索引的边界值只计算一次)
  • for-each不适用的情况:
    • 过滤:遍历删除选定元素
    • 转换:部分元素值进行操作
    • 平行迭代:并行遍历多个集合

47. 了解和使用类库

  • 熟悉类库以及新特性,不要重复造轮子。

48. 需要精确答案时,避免使用float和double

  • float和double不提供精确结果,使用BigDecimal、int、long进行精确计算
  • BigDecimal使用不便,性能不如基本运算类型。性能敏感的功能中,考虑把小数转换成int或者long进行处理。

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

  • 基本类型(int,double,boolean)
  • 引用类型(String,List,基本类型对应的引用类型 = 装箱基本类型,Double等)
  • 当操作中混合使用基本类型和装箱基本类型时,装箱基本类型会自动拆箱,避免自动拆箱导致的NullPointerException
  • 装箱基本类型使用场景:集合中的元素、键和值;泛型;反射的方法调用

50. 如果有其他类型更适合,避免使用字符串

  • 数值,枚举,含多个属性的实体不要用string进行代替

51. 当心字符串连接的性能

  • 连接字符串使用StringBuilder

52. 通过接口引用对象

  • 面向接口编程,是程序更加灵活,可以方便的更换实现,而不修改其他代码。

53. 接口优先于反射机制

  • 反射:通过程序来访问类的信息,缺点:
    • 丧失了编译时类型检查的好处
    • 执行反射访问所需要的代码非常笨拙和冗长
    • 性能损失
  • 应用程序运行时不应该以反射方式访问对象
  • 通过反射创建实例(构造器不带参数的情况可以直接使用Class.newInstance)

54. 谨慎地使用本地方法(native method)

55. 谨慎的进行优化

  • 编写好的程序而不是快的程序
  • 设计系统时考虑性能
  • 依赖性能测量来进行优化

56. 遵守普遍接受的命名管理

异常

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

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

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

  • 受检异常(checked exception):调用者能够适当地恢复时使用该类异常,要求继续抛出或者进行捕获
  • 非受检异常:不需要也不应该被捕获的可抛出异常,可以不处理
    • 运行时异常(run-time exception)
    • 错误(error)

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

  • 会抛出受检异常的API使用起来不方便,需要额外的代码。
  • 把受检异常变成非受检异常

60. 优先使用标准的异常

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

  • 异常转译:高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常
  • 最好的方法是避免低层抛出异常,或者只记录日志而不抛出异常,无法处理低层异常时才通过异常转译抛给高层。

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

  • 单独地声明受检异常,用@throws标记记录抛出异常的条件
  • 用@throws记录每个可能抛出的未受检异常,不要使用throws关键字将未受检的异常包含在方法的声明中。

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

  • 异常的细节包含异常相关的参数和域的值

64. 努力使失败保持原子性

  • 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。

65. 不要忽略异常

  • 空的catch来忽略异常

并发(阅读完java并发编程后再阅读)

序列化

将对象编码成字节流,并从字节流编码中重新构建对象。

74. 谨慎地实现Serializable接口

  • 实现Serializable接口带来的问题:
    • 一旦类被发布,会降低改变类实现的灵活性。(新旧序列化形式不兼容)
    • bug和安全性。反序列化是隐藏的构造器,会破坏对象的约束关系。(单例)
    • 测试负担增加,新旧兼容测试
  • 为了继承而设计的类尽可能少的去实现Serializable接口,用户的接口也应该少的继承Serializable接口。减少扩展的负担。
  • 内部类不应该实现Serializable

75. 考虑使用自定义的序列化形式