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

  • 静态工厂方法可以提供更易于阅读的名称。
  • 静态工厂方法能够为重复的调用返回相同对象。
  • 静态工厂方法可以返回原返回类型的任何子类型的对象,适用于基于接口的框架。

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

    Builder 模式十分灵活,builder 的参数可以在调用 build 方法来创建对象期间进行调整,也可以随着不同的对象而改变。builder 也可以自动填充某些域,例如每次创建对象时自动增加序列号。

Builder 模式也有它的不足。为了创建对象,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就成问题了。Builder 模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使用,比如 4 个或者更多个参数。但如果你未来可能需要添加参数,通常最好一开始就使用构建器。

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

对于某些工具类,我们并不希望它们会被实例化,因为实例化对它们来说没有任何意义。然后,在缺少显式构造器的情况下,编译器会自动提供一个公有、无参的构造器。

企图通过将类做成抽象类来强制该类不可被实例化是行不通的。因为该类可以被子类化,并且子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。因此,我们应该让这个类包含一个私有构造器,这样它就不能被实例化了。

这个用法也有一个副作用,它使得这个类不能被子类化,因为所有构造器都必须显式或隐式地调用超类构造器,在这种情况下,子类就没有可访问的超类构造器可调用了。

避免创建不必要的对象

有些对象的创建成本比较高,如果对象可以被安全地重用,在需要重复地使用这类昂贵的对象的地方,建议将它缓存下来重用。

典型的反面例子就是 String.matches 方法,该方法在内部为正则表达式创建了一个 Pattern 实例,却只用了一次,Pattern 实例的创建成本很高。为了提升性能,应该显式地将正则表达式编译成一个 Pattern 实例,让它成为类初始化的一部分,并将它缓存起来。另一种创建多余对象的方法,称为自动装箱。在使用上,要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

try-with-resource 优于 try-finally

要使用 try-with-resource 语句,相应资源必须先实现 AutoCloseable 接口,其中包含了单个返回 void 的 close 方法。使用 try-with-resource 不仅使代码变得简洁易懂,也更容易进行异常诊断。

覆盖 equals 时请遵守通用约定

什么时候应该覆盖 equals 方法呢?如果类具有自己特有的 “逻辑相等” 概念,且超类还没有覆盖 equals。这通常属于 “值类” 的情形。值类仅仅是一个表示值的类,如 Integer 或 String。我们在利用 equals 方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。覆盖 equals 方法时必须要遵守它的通用约定:

  • 自反性(rerlexive):对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。x.equals(null) 必须返回 false。

  • 对称性(symmctric):对于任何非 null 的引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 也必须返回 true。

  • 传递性(transitive):对于任何非 null 的引用值 x、y 和 z,如果 x.equals(y) 返回 true,且 y.equals(z) 也返回 true,那么 x.equals(z) 也必须返回 true。

  • 一致性(consistent):对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true 或 false。

覆盖 equals 时总要覆盖 hashCode

如果两个对象根据 equals(Object) 方法比较是相等的,那么调用这两个对象中的 hashCode 方法都必须产生同样的整数结果。如果两个对象根据 equals(Object) 方法比较是不相等的,那幺调用这两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

始终要覆盖 toString

虽然 Object 提供了 toString 方法的一个实现,但它返回的字符串通常并不是类的用户所期望看到的。它包含类的名称,以及一个 @ 符号,接着是散列码的无符号十六进制表示法。toString 的通用约定指出,被返回的字符串应该是一个简洁的但信息丰富,并且易于阅读的表达形式。

遵守 toString 约定并不像遵守 equals 和 hashCode 的约定那么重要,但提供好的 toString 实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。但不要在静态工具类或枚举类中编写 toString 方法。

谨慎地覆盖 clone

Cloneable 接口的目的是作为对象的一个 mixin 接口,表明这样的对象允许克隆。但遗憾的是,它并没有成功地达到这个目的。它的主要缺陷在于缺少一个 clone 方法,而 Object 的 clone 方法是受保护的。尽管存在这样或那样的缺陷,这项设施仍然被广泛使用,因此值得我们进一步了解。

既然 Cloneable 接口并没有包含任何方法,那它到底有什么作用呢?它决定了 Object 中受保护的 clone 方法实现的行为:如果一个类实现了 Cloneable,那么 Object 的 clone 方法就返回该对象的逐域拷贝,否则就会抛出 CloneNotSupportedException 异常。这是接口的一种极端非典型的用法,不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么,然而 Cloneable 接口改变了超类中受保护的方法的行为。

如果你希望在一个类中实现 Cloneable 接口,并且它的超类都提供了行为良好的 clone 方法。首先,调用 super.clone 方法,由此得到的对象将是原始对象功能完整的克隆。对 super.clone 方法的调用应当包含在一个 try-catch 块中。如果对象中包含的域引用了可变的对象,则需要为可变对象也递归调用 clone 方法:

  1. # ArrayListclone方法
  2. public Object clone() {
  3. try {
  4. ArrayList<?> v = (ArrayList<?>) super.clone();
  5. v.elementData = Arrays.copyOf(elementData, size);
  6. v.modCount = 0;
  7. return v;
  8. } catch (CloneNotSupportedException e) {
  9. // this shouldn't happen, since we are Cloneable
  10. throw new InternalError(e);
  11. }
  12. }

克隆复杂对象的最后一种办法是,先调用 super.clone 方法,然后把结果对象中的所有域都设置成它们的初始状态,然后重新产生对象的状态。在我们的 ArrayList 例子中,modCount 被初始化为了 0,并且拷贝的对象包含内部深层结构的可变对象。

像构造器一样,clone 方法也不应该在构造的过程中,调用可以覆盖的方法。如果 clone 调用了一个在子类中被覆盖的方法,那么在该方法所在的子类有机会修正它在克隆对象中的状态之前,该方法就会先被执行,这样很有可能会导致克隆对象和原始对象之间的不一致。

考虑实现 Comparable 接口

一旦类实现了 Comparable 接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。你付出很小的努力就可以获得非常强大的功能。JDK 中的所有值类以及所有的枚举类型都实现了 Comparable 接口。如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或按年代顺序,那你就应该坚决考虑实现 Comparable 接口。

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

compareTo 方法中域的比较是顺序的比较,而不是等同性的比较。如果一个域没有实现 Comparable 接口或者你需要使用一个非标准的排序关系,就可以使用一个显式的 Comparator 来代替。

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

区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是否隐藏了其内部数据和其他实现细节。设计良好的组件会隐藏所有的实现细节,把 API 与实现清晰地隔离开来。然后,组件之间只通过 API 通信,而不需要知道其他模块的内部工作情况。这个概念被称为封装,是软件设计的基本原则之一。

信息隐藏之所以非常重要有许多原因,其中大多是因为:它可以有效地解耦系统的各个组件,使得这些组件可以独立地开发、测试、优化、使用、理解和修改。因为这些组件可以并行开发,所以加快了系统开发的速度,同时减轻了维护的负担。

Java 提供了许多机制来协助信息隐藏。访问控制(Access Control)机制决定了类、接口和成员的可访问性。正确地使用这此修饰符对于实现信息隐藏是非常关键的。对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列:

  • 私有的(private):只有在声明该成员的顶层类内部才可以访问这个成员
  • 包级私有的(package-private):声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为默认(default)访问级别,但接口成员除外,它们默认的访问级别是公有的。
  • 受保护的(protected):声明该成员的类及子类可以访问这个成员,并且声明该成员的包内部的任何类也可以访问这个成员。
  • 公有的(public):在任何地方都可以访问该成员。

有一条规则限制了降低方法的可访问性的能力。如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。这可确保任何可使用超类实例的地方也都可以使用子类实例(里氏替换原则)。如果违反了这条规则,编译器就会产生一条错误消息。

总而言之,应该始终尽可能(合理)地降低程序元素的可访问性。在仔细地设计了一个最小的公有 API 后,应该防止把任何散乱的类、接口或者成员变成 API 的一部分。

使可变性最小化

不可变类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内不变。不可变的类比可变类更加易于设计、实现和使用。它们不易出错且更加安全。为了使类成为不可变,要遵循下面五条规则:

  • 不要提供任何会修改对象状态的方法。
  • 保证类不会被扩展。这样可以防止子类改变该类的不可变行为。
  • 声明所有的域都是 final 的。
  • 声明所有的域都为私有的。
  • 确保对于任何可变组件的互斥访问。

如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。降低对象可以存在的状态数,可以更容易地分析该对象的行为,同时降低出错的可能性。因此,除非有令人信服的理由使域变成非 final 的,否则让每个域都是 final 的。

复合优于继承

继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。

与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。因而,子类必须要跟着其超类的更新而演变,除非超类是专为扩展而设计,并且具有很好的文档说明。只有当子类真正是超类的子类型时,才适合用继承。

幸运的是,有一种办法可以避免前面提到的所有问题。即不扩展现有的类,而是在新类中增加一个私有域,它引用现有类的一个实例。这种设计被称为复合(composition),因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这样得到的类将非常稳固,它不依赖于现有类的实现细节。这也正是 Decorator(装饰者)模式。

接口优于抽象类

Java 提供了两种机制用来定义允许名个实现的类型:接口和抽象类。自从 Java 8 为接口引入了缺省(default)方法后,这两种机制都允许为某些实例方法提供实现。主要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类,又因为 Java 只允许单继承,所以用抽象类作为类型定义受到了限制。任何定义了所有必要的方法并遵守通用约定的类,都允许实现一个接口,无论这个类是处在类层次结构中的什么位置。

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

嵌套类(nested class)是指定义在另一个类的内部的类。嵌套类存在的目的应该只是为它的外围类提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该是顶层类。

嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名类(anonymous class)和 局部类(local class)。除了第一种外,其他三种都称为内部类(inner class)。

静态成员类是最简单的一种嵌套类,它可以访问外围类的所有成员,包括那些声明为私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。从语法上讲,静态成员类和非静态成员类间唯一的区别是,静态成员类的声明中包含修饰符 static。尽管它们的语法非常相似,但这两种嵌套类有很大的不同。非静态成员类的每个实例都隐含地与外围类的一个外围实例相关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法。在没有外围实例的情况下,想创建非静态成员类的实例是不可能的。如果声明成员类不要求访问外围实例,就要始终把修饰符 static 放在它的声明中。

匿名类是没有名字的,它不是外围类的一个成员。它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。在 Java 中增加 Lambda 之前,匿名类是动态地创建小型函数对象和过程对象的最佳方式,但是现在会优先选择 Lambda。

局部类是四种嵌套类中使用最少的类。在任何可以声明局部变量的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类是在非静态环境中定义时才有外围实例,并且不能包含静态成员。如果一个嵌套类需要在单个方法外仍然是可见的,或者它太长了。则不适合放在方法内部,就应该使用成员类。

限制源文件为单个顶级类

虽然 Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,只会带来巨大的风险。因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。哪一个定义会被用到,取决于源文件被传给编译器的顺序。

请不要使用原生态类型

声明中具有一个或者多个 类型参数(Type Parameter)的类或接口,就是泛型(Generic) 类或接口。例如,List 接口就只有单个类型参数 E,表示列表的元素类型。泛型类和接口统称为 泛型(Generic Type)。

每一种泛型定义一组参数化的类型(Parameterized Type),构成格式为:先是类或者接口的名称,接着用尖括号 <> 把对应于泛型形式类型参数的实际类型参数(Actual Type Paramter)列表括起来。最后,每一种泛型都定义一个 原生态类型(Raw Type),即不带任何实际类型参数的泛型名称。例如,与 List 相对应的原生态类型是 List。原生态类型的存在主要是为了与泛型出现之前的代码相兼容。

虽然使用原生态类型(没有类型参数的泛型)是合法的,但是永远不应该这么做。如果使用原生态类型,就失去了泛型在安全性和描述性方面的所有优势。在不确定或者不在乎集合中的元素类型的情况下,你也许会使用原生态类型,但更安全的做法是使用无限制的通配符类型。例如,泛型 Set 的无限制通配符类型为 Set<?>。这是最普通的参数化 Set 类型,可以持有任何集合。

消除非受检的警告

用泛型编程时会遇到许多编译器警告,有许多非受检警告很容易消除,而有些警告非常难以消除。当你遇到需要进行一番思考的警告时,要坚持尽可能地消除每一个非受检警告。如果消除了所有警告,就可以确保代码是类型安全的,这意味着不会在运行时出现 ClassCastException 异常。

如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以用 @SuppressWarnings(“unchecked”) 注解来禁止这条警告。如果在禁止警告之前没有先证实代码是类型安全的,那就只是给你自己一种错误的安全感而已。代码在编译时可能没有出现任何警告,但它在运行时仍然会抛出 ClassCastException 异常。

@SuppressWarnings 注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。但应该始终在尽可能小的范围内使用 @Suppresswarnings 注解。它通常是个变量声明或是非常简短的方法或构造器。永远不要在整个类上使用 @SuppressWarnings,这么做可能会掩盖重要的警告。

列表优于数组

数组与泛型相比,有两个重要的不同点。首先,数组是协变的(covariant)。这表示如果 Sub 为 Super 的子类型,那么数组类型 Sub[] 就是 Super[] 的子类型。相反,泛型则是可变的(invariant),对于任意两个不同的类型 Type1 和 Type2,List 既不是 List 的子类型,也不是 List 的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上可以说数组才是有缺陷的。

数组与泛型之间的第二大区别在于,数组是具体化的(reificd)。因此数组会在运行时知道和强化它们的元素类型。如果企图将 String 保存到 Long 数组中,就会得到一个 ArrayStoreException 异常。相比之下,泛型则是通过擦除(erasure)来实现的。这意味着,泛型只在编译时强化它们的类型信息,并在运行时抛弃(或擦除)它们的元素类型信息。擦除使泛型可以与未使用泛型的代码随意互用,以确保在 Java 5 中平滑过渡到泛型。

总之,数组和泛型有着截然不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。

优先考虑泛型类、泛型方法

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

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

谨慎并用泛型和可变参数

虽然可变参数方法和泛型都是在 Java 5 中就有了,但它们不能良好地相互作用。可变参数的作用在于让客户端能够将可变数量的参数传给方法,当调用一个可变参数方法时,会创建一个数组用来存放可变参数;这个数组应该是一个实现细节,它是可见的。因此,当可变参数有泛型或者参数化类型时,编译警告信息就会产生混乱。

在 Java 7 之前,带泛型可变参数的方法的设计者,对于在调用处出错的警告信息一点办法也没有。这使得这些 API 使用起来非常不愉快。在 Java 7 中,增加了 @SafeVarargs 注解,它让带泛型 vararg 参数的方法的设计者能够自动禁止客户端的警告。本质上,@SafeVarargs 注解是通过方法的设计者做出承诺,声明这是类型安全的。作为对于该承诺的交换,编译器同意不再向该方法的用户发出警告说这些调用可能不安全。

重要的是,不要随意用 @SafeVarargs 对方法进行注解,除非它真正是安全的。那么它凭什么确保安全呢?回顾一下,泛型数组是在调用方法时创建的,用来保存可变参数。如果该方法没有在数组中保存任何值,也不允许对数组的引用转义,那它就是安全的。换句话说,如果可变参数数组只用来将数量可变的参数从调用程序传到方法,那么该方法就是安全的。