effective java

Effective Java 中文版第3版 臧秀涛译

第2章 创建和销毁对象

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

在设计类时,可以将静态工厂方法作为公有的构造器的替代或补充。静态工厂方法和公有的构造器各有所长,重要的是了解其相对优势。通常应该首选静态工厂,切忌在没有考虑静态工厂的情况下本能地提供公有的构造器。

条目2 当构造器参数较多时考虑使用生成器

静态工厂和构造器有一个共同的缺点:当可选参数非常多时,不能很好地扩展。当我们要设计的类的构造器或静态工厂具有多个参数,特别是其中的许多参数是可选的或具有相同的类型时,生成器模式Builder是个不错的选择。与重叠构造器模式相比,使用生成器的客户代码更容易阅读和编写,生成器也比 JavaBeans 更安全。

条目3 利用私有构造器或枚举类型强化 Singleton 属性

有两种常见的实现 Singleton 的方式。其原理都是将构造器设置为私有的,通过导出一个公有的静态成员来提供对唯一实例的访问。第一种方式是用一个 final 的字段作为公有的静态成员,第二种方式是提供一个公有的静态工厂方法。现在有了第三种方式,即声明一个只包含单个元素的枚举类型。单元素的枚举类型往往是实现 Singleton 的最佳方式。

条目 4 利用私有构造器防止类被实例化

有时需要编写仅包含静态方法和静态字段的类,这样的工具类并不是为了被实例化而设计的,可以让类包含一个私有的构造器来防止它被实例化。

条目 5 优先考虑通过依赖注入来连接资源

对于依赖于一个或多个底层资源,而且资源的行为会对其行为造成影响的类,不要使用 Singleton 或静态工具类来实现,也不要让该类直接创建这些资源。相反,应该将资源或创建资源的工厂传递给构造器(或静态工厂,或生成器)。
这种做法,也就是所谓的依赖注入,将极大地提升类的灵活性、可复用性和可测试性。

条目 6 避免创建不必要的对象

复用对象,而不是每次需要时都创建一个新的功能相同的对象,往往是更好的选择。不可变对象总是可以复用的。
对于既提供了静态工厂方法,又提供了构造器的不可变类,通常首选前者,以避免创建不必要的对象。
应该优先使用基本类型而不是其封装类,并提防无意中的自动装箱。
除非创建对象的开销极为高昂,否则通过维护自己的对象池来避免创建对象并不是好的选择。一个有正当理由使用对象池的典型例子是数据库连接。建立数据库连接的开销高到值得复用这些对象。

条目7:清除过期的对象引用

在某些场景下,如果不清除过期的对象的引用会存在内存泄露的风险,表现为随垃圾收集器活动增加或内存占用增加而导致的性能下降。

清除过期的对象引用的场景:

  1. 每当出现类自己管理的自己的内存的情形时,应该警惕内存泄露。

    比如类自管理的栈,如果栈新增后减,弹出去的元素并不会被垃圾收集器清理,因为垃圾收集器并不会感知到该元素已经无用。

  2. 缓存。对于一条缓存项(包括键和值), 只有在缓存外有对其键的引用时,它才有存在的意义,可以用weekHashMap实现。缓存项在过期后会被自动删除。

    只有当缓存项预期的生命周期由指向其键的外部引用而不是由值决定时,WeekHashMap才有用

  3. 监听器和其他回调。如果实现了一个api,客户端注册了回调,但是没有显式地注销,否则回调对象就会不断累积。确保回调及时被垃圾收集器处理的方法是只存储对他们的弱引用。

条目8: 避免使用终结方法和清理方法

终结方法(finalizer)是不可预测的,java9开始终结方法已经被废弃了。java9引入了清理方法(cleaner)替代终结方法,危险性要比终结方法小,但是仍然是不可预测的,且运行很慢,一般来说也是不必要的。

如果类的对象封装了需要终止的资源,只须让这样的类实现AutoCloseable接口,并要求客户端在每个实例不需要时就调用其close方法,通常可以用try-with-resources来确保即使存在异常也能正常终止。

条目9:与try-finally相比,首选try-with-resources

java类库中有很多通过调用close方法手动关闭资源,这样的雷子包括inputstream、outputstream和java.sql.connection。

当有try-finally打开多个资源时,代码就变得很难维护,同时首先抛出异常的代码很容易被吞掉,第一个异常不会被记录在异常栈轨迹信息中。

在java7引入try-with-resources,配合该语句使用,资源必须实现AutoCloseable接口,该接口仅包含一个返回类型为void的close方法。

如果read和close方法都抛出了异常,后者的异常会被抑制,也可能会抑制多个异常。这些抑制异常并没有被简单丢弃,而是会被打印到轨迹信息中,并表明它们被抑制了。

第3章 对所有对象都用通用的方法

条目10:在重写equals方法时要遵守通用约定

重写equals看似简单,但是很容易犯错,如果满足下列的任意条件,就不应重写equals:

  • 该类的每个实例本质上都是唯一的
  • 该类没必要提供一个“逻辑”相等的测试
  • 超类已经重写了equals方法,而且其行为适合这个类
  • 类是私有的或包私有的,我们可以确信其equals方法绝不会被调用

当一个类在对象相同之外还存在逻辑相等的概念,而且其上层超类都没有重写equals方法时,应该重写equals方法。

重写equals,必须遵循其通用约定:

  • 自反性:对于任何非null引用值,x.equals(x)=true
  • 对称性:对于非null的引用值x和y,有且仅当y.equals(x)返回true时,x.equals(y)必须返回true
  • 传递性:对于非null的引用值x、y和z,如果x.equals(y)返回true时,且y.equals(y)返回true时,x.equals(z)必须返回true
  • 一致性:对于非null的引用值x和y,只要equals比较中用到的信息没有修改,多次调用x.equals(y)必须一致地返回true或false
  • 非空:对于非null的引用值x,x.equals(null)必须返回false

里氏替换原则:一个类型的任何重要属性都应该适用于其所有子类型,以便为该类型编写的任何方法在其子类型上同样有效。

示例:比如Point的某个子类的实例仍然是一个Point,且仍然需要表现得和Point一样。

重写equals的技巧:

  • 使用==运算符检查参数是否为指向当前对象的引用

  • 使用instanceof运算符检查参数是否具有正确的类型

  • 将参数强制转换为正确的类型

  • 对于类中的每个“重要”字段,检查参数的这一字段和当前对象的相应字段是否匹配

    1. Float.equals和Double.equals性能很差,每次都会存在自动装箱

    2. 为了避免npe,可以使用Objects.equals(Object, Object)来检查字段是否相等

    3. 比较顺序也可能会影响性能。首先比较更可能不同的字段,或比较开销不那么高的字段

条目11:重写equals方法时应该总是重写hashCode方法

重写equals方法的每个类都必须重写hashCode。否则,使实例无法正常使用诸如HashMap和HashSet等集合,应为相等的对象必须有相等的哈希码。

计算哈希码的注意事项:

  • 如果一个字段的值可以通过其他字段计算出来,那么在计算哈希码的时候可以不考虑它
  • 在写equals比较中没用到的任何字段,也必须排除在外,否则两个相等对象可能会产生不同哈希码

计算哈希码时为什么常用31,因为31是一个奇素数,31有个好处是可以将乘法替换为位移和减法:31*i == (i << 5) - i

条目 12 总是重写 toString 方法

虽然 Object 类提供了 toString 方法的一个实现,但它所返回的字符串通常不是类的用户所希望看到的。它由类名、@ 符号以及哈希码的无符号十六进制形式组成,如 PhoneNumber@adbbd。toString 的通用约定指出,所返回的字符串应该是“一个简洁但信息丰富,而且适合人阅读的表达形式”。
在我们编写的每个可实例化的类中都要重写 Object 的 toString 实现,除非有超类已经这样做了。这样会使类用起来更舒服,而且有助于调试。toString 方法应该以一种美观的格式返回对这个对象的简洁、有用的描述。

条目 13 谨慎重写 clone 方法

考虑到与 Cloneable 相关的所有问题,新的接口不应该扩展该接口,新的可扩展的类也不应该实现该接口。尽管 final 类实现 Cloneable 的危害要小得多,但应该将其视作一种性能优化,除非是有充足理由的少数情况,否则也不建议使用。一般来说,复制功能最好通过构造器或工厂来提供。

Cloneable设计缺陷多,浅拷贝易引发问题,且不执行构造器,推荐用复制构造器工厂方法实现对象复制(可灵活实现深拷贝)

不过数组是个例外,它们最好用 clone 方法来复制

数组的clone()是原生高效实现,一维数组可实现元素级拷贝,是官方推荐的最优方式。对于引用类型数组clone依然是浅拷贝

条目 14 考虑实现 Comparable 接口

每当要实现一个可以合理地进行排序的值类时,都应该让这个类实现 Comparable 接口,这样它的实例就可以轻松地被排序、查找和用在基于比较的集合中。在 compareTo 方法的实现中,当比较字段的值时,应避免使用<>运算符。相反,请使用基本类型的封装类中的静态 compare 方法,或使用 Comparator 接口中的比较器构造方法。

第4章 类和接口

条目15 最小化类和成员的可访问性

区分设计良好的组件和设计不良的组件最重要的因素是,这个组件能在多大程度上将其内部数据和其他实现细节对别的组件隐藏起来。设计良好的组件会隐藏其所有的实现细节,并将API与实现清晰地隔离。然后,组件之间仅通过它们的API进行通信,而对彼此的内部工作一无所知。
应该尽可能(合理地)降低程序元素的可访问性。在精心设计了一个最小的公有API之后,应该防止任何游离的类、接口或成员成为这个API的一部分。除了作为常量的公有静态final字段外,公有类不应该有任何公有的字段请确保公有的静态final字段所引用的对象是不可变的。

条目16 在公有类中,使用访问器方法,而不使用公有的字段

公有类永远不应该暴露可变字段。公有类暴露不可变的字段的危害会小一些,但仍然值得怀疑。然而,对于包私有的类或私有的嵌套类而言,无论字段是可变的还是不可变的,有时暴露它们是可取的。

条目17 使可变性最小化

不可变类是指其实例无法修改的类。不要轻易为每个getter方法编写一个对应的setter 方法。在设计类时,除非有充分的理由将其设计为可变的,否则就应该将其设计为不可变的。如果类无法被设计为不可变的,就应该尽可能限制其可变性。减少对象可能存在的状态的数量,推断其状态就会更容易,也减少了出错的可能性。用 private final 来声明类中的每个字段,除非有充分的理由不这样做。

条目 18 组合优先于继承

继承非常强大,但也存在问题,因为它会破坏封装。只有当子类和超类之间存在真正的子类型关系时,才适合使用继承。即便如此,如果子类与超类在不同的包中,并且超类不是为继承而设计的,那么继承有可能导致脆弱性。为了避免这种脆弱性,应该使用“组合并转发”方式而不是继承,特别是在存在一个合适的接口来实现包装器类的情况下。包装器类不仅比子类更健壮,而且也更强大。

组合(Composition)+ 方法转发(Forwarding)是修饰器模式的核心实现方式。

组合(Composition):修饰器类持有一个被修饰对象的引用(而非继承),这是修饰器模式的基础,也是优于继承的核心原因。

方法转发(Forwarding):修饰器类先调用被修饰对象的原有方法(转发),再在其前后添加自定义逻辑(修饰)。

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

设计用于继承的类是一项艰巨的任务。必须将可重写方法的所有自身使用情况写到文档中,而一旦写到文档中,就必须在类的整个生命周期内坚守承诺。子类会依赖超类的实现细节,如果我们未能遵守承诺,修改了超类,子类有可能遭到破坏。为了让他人为编写高效的子类,我们可能需要导出一个或多个受保护的方法。除非知道确实需要子类,否则最好通过将类声明为 final 的或确保没有可访问的构造器来禁止继承。

条目 20 与抽象类相比,优先选择接口

要定义支持多种实现的类型,接口通常是最佳选择。如果导出了一个不是很简单的接口,请务必考虑配合提供一个骨架实现。在可能的情况下,应该通过接口上的默认方法来提供骨架实现,以便该接口的所有实现者都可以使用。即便如此,接口上的限制通常会使得抽象类形式成为骨架实现的不二之选。

骨架实现类=接口+通用默认实现+抽象方法

作用:让子类不用从头实现接口,只填“差异化逻辑”

模版方法设计模式=用抽象类或接口定义骨架+固定的流程方法

条目 21 为传诸后世而设计接口

尽管默认方法现在已经成为 Java 平台的一部分,但谨慎设计接口仍然是极其重要的。虽然默认方法使得向现有的接口中添加方法成为可能,但这样做存在很大的风险。如果接口包含一个小缺陷,则可能会永远困扰其用户;如果接口存在严重的缺陷,则可能会毁掉包含它的 API。虽然在接口发布之后再修正一些缺陷也是有可能的,但千万不要寄希望于此。

条目 22 接口仅用于定义类型

接口应仅用于定义类型。如果只是要导出常量,不应该使用接口。

条目 23 优先使用类层次结构而不是标记类

标记类只是对类层次结构的比较蹩脚的模仿,通常不适合使用。如果想编写一个带有显式标记字段的类,应该认真考虑一下是否可以去除这个标记,并用一个层次结构来代替这个类。当我们遇到一个带有标记字段的现有类时,也应该考虑将其重构为一个层次结构。

条目 24 与非静态成员类相比,优先选择静态成员类

有四种不同的嵌套类,各有其应用场景。如果一个嵌套类需要在单个方法之外仍然可见,或者因为太长而不适合放在方法内部,那么就使用成员类。如果成员类的每个实例都需要一个指向其包围实例的引用,那么就把它设计为非静态的;否则就设计为静态的。 假设这个类属于一个方法的内部,如果只需要在一个地方创建其实例,并且存在一个可以描述这个类的预先存在的类型,那么就把它设计为匿名类;否则就把它设计为局部类。

条目 25 将源文件限制为单个顶层类

永远不要将多个顶层类或接口放在一个源文件中。遵循这个规则可以确保在编译时不会出现单个类的重复定义问题。这反过来又保证了编译生成的类文件和所得到的程序的行为都不会受到源文件传递给编译器时的顺序的影响

posted @ 2025-07-06 15:44  小小灰迪  阅读(14)  评论(0)    收藏  举报