avatar

目录
Effective Java (翻译)

Chapter 1 Introduction

这本书是用来帮助你高效的使用Java编程语言以及它的基本类库:java.lang,java.util 和 java.io, 也包括例如java.util.concurrent 和 java.util.function 之类的子包. 其他的类库也将会时不时的进行探讨。

这本书包含90个小节,每一小节表述了一个规则。这些规则是最有经验的程序员们普遍认为最有益的规则。这些小节被紧凑的组合放到了11个章节,并且每一个都尽可能全面的涵盖了软件设计的思想。鉴于每一个小节都可以或多或少的单独去理解,因此这本书可以不用通读。小节直接回有很多的相互引用,因此你可以通过这本书很轻松的制定自己的学习计划。

自上一版印发后,这个平台在最新版本上发布了很多新的特性。大多数小节会使用这些新的特性,通过下面这个表格你可以了解到那些小节使用了这些新的特性:

image-20200311074607828

大多数小节都会用程序例子来做更详细的解释。这本书最大的特性就是它使用代码示例来阐释很多设计模式和使用习惯。在适当的地方,小节会引用 标准参考丛书[Gamma95]。

许多小节都会包含一个或者多个项目例子来阐明一些不应该使用的用法。这些示例,就是我们所熟知的 antipatterns (反面模式),被非常醒目的标记上, 比如 // Never do this !. 在每一个示例中,该小节会详细解释为什么这样做事不好的并且会给出相应的解决方法。

这本书并不适合初学者:它假设你已经非常熟悉Java。如果你还并非熟悉Java,请选择一种教材,例如 Peter Sestoft’s Java Precisely [Sestoft16]. 然而Effective Java适用于那些有经验的,甚至为对那些高级程序员提供精神食粮

这本书中的大多数起源于几个基础的原则。清晰和简洁是至关重要的。组件不应该有使用户感到惊讶的行为。组件应该尽可能的小。(在这本书中,组件指的是任何可以被重复使用的元素,小到一个单独的方法,大到包含许多类包的复杂框架)代码应该是可以被复用而不是被直接复制。组件之间的依赖应该尽可能的小。错误应该尽今早发现,尽可能在编译的时候。

虽然本书中提到的规则并不是在任何时间都适用,但是在大多数情况下他们确实刻画出最佳的实践方法。你不应该盲目的遵循这些规则,但是偶然的违反这些规则也最好有一个合理的借口。学习编程的艺术,就像学习其他科目,首先要学会这些规则,然后尝试去破坏他们。

书中的大部分并非关于性能,而是关于如何编写清晰的,正确的,可重用的,强壮的,灵活的和可以维护的。如何你可以做到这些,那么达到你想要的性能也会是相对容易的事情(Item 67)。书中有些小节确实讨论了性能的问题,并且有些小节提供了性能相关的参数。这些参数是在我自己的机器上创作出来的,应该视为近似最好的。

不论是否有用,我把我的机器的一些参数罗列在这里。我的机器是一台老的自制的3.5GHz 双四核 Inter处理器i7-4770k并且装有16G DDR3-1866 CL9 RAM,运行Azul‘s Zulu 9.0.0.15 版本的OpenJDK,Microsoft Windows 7 专业SP1(64-bit)。

当我们讨论Java编程语言的特性和它的类库的时候,有些时候是需要说明特定的Java 版本的。为了方便起见,这本书使用别名而不是官方版本号。以下这张图给出了他们的对应关系:

image-20200311221111970

书中所涉及的例子是相当完整的,但是可读性更好些。它们自由的使用来自java.util 和java.io的类。为了使得这些例子可以编译,你需要引入更多的包,或者其他的样本文件。本书的网站包含了扩充版的示例,你可以下载下来编译并运行它们。http://joshbloch.com/effectivejava

说中大多数部分使用了Java 语言定义的一写专业术语,Java SE 8 Edition [JLS]. 有几个项值得提及的。Java语言支持4中类型:interfaces(包括annotations),classes(包括enums),arrays 和 primitives。前三个是reference type。Class instances 和arrays 是对象,primitive values 并不是对象。一个class的成员包括: fields,methods,member calsses 以及 member interface。一个method的特征包括它的名字和它的参数的类型,然而method 的特征并不包含method 的返回值。

本书中使用的一些不同于 The Java Language Specification的名字。并不像 The Java Language Specification,本书将继承作为子类的代名词。本书中简单的表述了calss实现一个interface或者interface继承另外一个。当没有声明access level 时,本书默认使用package-private 而不是technically correct package access [JLS, 6.6.1].

exported API,simply API 代表的是classes,interface,constructors,members,seralized forms(关于API,全称是application programming interface).实现API的程序员被称作user of the API,使用API的人被称作client of the API。

Classes,interfaces,constructors,members,serialized forms被统称做API element。API 是可以被引用,而且API elements 是可以被client使用的。并非巧合,这些elements也将会被Javadoc 工具类来生成说明文档。

在Java 9中引入了module,如果类库使用了module,类库的module declaration将会清晰的阐释该API中包含哪些packages。

Chapter 2 Creating and Destroying Objects

本章关心的是创建和销毁对象:什么时候和怎样创建对象,什么时候和怎样避免创建对象,如何确保对象被及时销毁,以及如何管理对象销毁时的清理工作。

Item 1:Consider static factory methods instead of constructors

传统的做法是提供公共的构造函数来帮助client获取实例。然而,推荐程序员使用另外一种方法来获取实例:A class 应该提供一个静态工厂方法,并且该静态类的返回值应该是该类的实例。如下给出一个关于Boolean类型的例子,这个方法会将原始的boolean 值转换成boolean对象,并且返回对象的引用:

java
1
2
3
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}

值得注意的是,上面提到的并不是一个Design Patterns[Gamma95] 中提到的静态工厂方法的模式,他们是不能直接画等号的。

一个类可以提供静态工厂方法以代替公共构造函数,或者在其基础上提供静态工厂方法。提供静态工厂方法来代替公共构造函数既有优点也有缺点。

静态工厂的一个优点就是,不像构造函数,它们是有名字的。如果一个构造函数的参数本身没有描述返回的对象,那么有一个完美名字的静态工厂方法就会表现得更容易使用,也让代码的可读性更强。例如,这个构造函数 BigInteger(int,int,Random)会返回一个有可能是prime的BigInteger,那么使用probablePrime 来作为静态工厂方法的名字可能表达的会更贴切。(这个静态方法被Java 4 收录了)

一个类只可以有一个指定签名(A method’s signature consists of its name and the type of its formal paramaters; the signature does not include the mthods’s return type)的构造函数。总所周知,程序员可以创建另外一个有不同参数顺序的构造方法来避开这种限制。但是,这是一个非常坏的想法。API的用户将无法记住这些构造方法,最终导致不小心调用了错的方法。没有文档的指导,人们也无法通过阅读代码了解这些构造函数。

因为静态工厂方法有名字,他们没有类似构造方法的限制。当一个类可能需要多个具有相同签名的构造函数的时候,可以考虑使用可读性更强的静态工厂方法来代替构造函数。


本段内容并非出自Effective Java,仅仅我对静态工厂具有可读性有点的理解。举个例子来加深我对这段话的理解

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person{
private String name;
private int age;
private String profession;

private Person(String name,int age,String profession){
this.name = name;
this.age = age;
this.profession = profession;
}

/*
例子可能不是太恰当,但是这两个静态方法的可读性相较于原始的构造函数就会好一些。
使用API的人很清楚地知道自己要用那个方法创建person 对象
*/
public static final Person create1Student(String studentName,int studentAge){
return new Person(studentName,studentAge,"Student");
}

public static final Person create1Doctor(String doctorName,int doctorAge){
return new Person(doctorName,doctorAge,"Doctor");
}

}

静态工厂方法的第二个有点就是,不像构造函数那样,调用静态工厂方法不需要创建新的对象。这使得immutable class(Item 17)可以使用预构建的实例,或者缓存那些已经构建的实例,以及可以重发的分配这些实例来避免重复创建不需要的对象。Boolean.valueOf(boolean) 方法说明了这样的技巧:它永远不创建对象(it never creates object)。这个技巧跟Flyweight pattern[Gamma95] 非常类似。如何需要经常使用这个对象,这个技巧可以很大程度上提高性能,尤其当创建该对象花费高的时候。


本段内容并非出自Effective Java:

原话是这样写的 A second advantage of static factory methods is that, unlike constructors, they are not required to create a new object each time they’re invoked. 最初我不是很理解,因为不论调用静态构造方法还是构造函数,我们的目的都是一样的:拿到该类的实例。那最根本上我们都会调用构造函数来创建对象啊(就像上面Person 的例子,即便是静态方法,其内部也使用了构造函数),为什么作者说并不是每次都需要创建对象呢?

java
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
26
27
28
29
30
31
32
33
34
35
36
37
/*
刚刚修改了Person类,这次我预先初始化了一个doctor和一个student。
这样调用静态方法的时候就可以直接返回已经缓存好的对象,并不需要再去重复创建新的对象给Client。
因此,如果创建新的对象开销很大的情况下,重复使用公共的对象就简单高效简单了
*/
public class Person{

private static Person student = new Person("A",15,"Student");
private static Person doctor = new Person("B",22,"Doctor");

private String name;
private int age;
private String profession;

private Person(String name,int age,String profession){
this.name = name;
this.age = age;
this.profession = profession;
}

public static final Person create1Student(String studentName,int studentAge){
if(student != null){
return student;
}else{
return new Person(studentName,studentAge,"Student");
}
}

public static final Person create1Doctor(String doctorName,int doctorAge){
if(doctor != null){
return doctor;
}else{
return new Person(doctorName,doctorAge,"Doctor");
}
}

}

静态工厂方法被重复调用但是返回的是同一个对象的能力使得类对实例的存在有了严格的把控。使用这种方法的类被称作instance-controlled。那么我们为什么使用instance-controlled的类呢?Instance control 保证类是singleton(Item 3) 或者 noninstantiable(Item 4).并且,它使得immutable value calss(Item 17) 来保证没有完全相同的两个实例存在:a.equals(b) 当且仅当 a==b。这也是Flyweight parttern[Gamma 95]的基本。Enum 类型(Item 34)提供了这种保证。

静态工厂方法的第三个有点就是,不像构造函数,他们可以返回静态工厂返回的子类型。这很大程度上提高了让你你选择返回值的灵活性。

API使用这种方法使得其可以返回对象但是不需要公开级别的类。这种隐藏实现类的方法使得API更紧凑。这种技术也促成了interface-based frameworks(Item 20),其中接口为静态工厂提供自然返回类型。

Java 8 之前,interface不能包含静态方法。按照惯例,interface的静态工厂方法被放在noninstantiable companion calss(Item 4)。例如,Java Collections Framework有45个工具类来实现它的接口,提供不可修改的,同步的集合等等。几乎所有的这些实现都是通过不需要实例化的类(java.util.Collections)中的静态工厂方法导出的。返回对象的类都是隐藏的。


这段并非出自Effective Java:

根据上面的描述, 我去查看了 java.util.Collections 类。比如synchronizedCollection:

  • 是通过一个静态工厂方法来返回synchronizedCollection。

    java
    1
    2
    3
    4
    5
    6
    7
    public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
    return new SynchronizedCollection<>(c);
    }

    static <T> Collection<T> synchronizedCollection(Collection<T> c, Object mutex) {
    return new SynchronizedCollection<>(c, mutex);
    }
  • 真正create synchronizedCollections(synchronizedCollection的构造方法)是non-public的, 也就是外部包是无法直接通过调用构造方法来拿到synchronizedCollection实例。

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    SynchronizedCollection(Collection<E> c) {
    this.c = Objects.requireNonNull(c);
    mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
    this.c = Objects.requireNonNull(c);
    this.mutex = Objects.requireNonNull(mutex);
    }

Collections Framework API的规模要比它之前返回单独的45个公共类小得多,每一个类都有一个便利的实现。它不仅仅API数量大量介绍,而且还引入概念的权重:为了使用这些API,程序员必须掌握的概念的数量和难度。程序员指导API返回的对象恰好是API通过interfance定义的,因此不需要查阅class实现文档。并且,用户使用静态工厂方法仅仅需要参照接口返回的对象而不需要查看它真正的实现,这通常是一个很好的方法(Item 64)。

Java 8允许接口包含静态方法,因此,通常没有理由为接口类提供不可实例化的伴随类。很多的静态公共成员应该放在接口本身。但是,请注意,有可能仍然需要把大部分的实现隐藏那些静态方法后面的私有类包中。这是因为java 8要求接口的这些静态成员是公共的。Java 9 才允许私有的静态方法,但是静态变量和成员类依然保持公开。

第四个优点是静态工厂的返回值可以根据输入参数的不同而不同。声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。

EnumSet 类(Item 36)没有公共构造方法,只有静态工厂方法。在OpenJDK实现中,静态工厂方法根据底层枚举类型的大小返回两个子类:如果该枚举的长度小于等于64,就像大多数枚举类型一样,静态工厂返回RegularEnumSet 实例,底层是long型。如果enum类型长度大于64,静态工厂方法返回一个JumboEnumSet实例,底层是long数组。


本段并非来自Effective Java.下面的静态工厂类来自EnumSet

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

/**
* Creates an empty enum set with the specified element type.
*
* @param <E> The class of the elements in the set
* @param elementType the class object of the element type for this enum
* set
* @return An empty enum set of the specified type.
* @throws NullPointerException if <tt>elementType</tt> is null
*/
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");

if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}

这两个返回类的实现对客户是不透明的。如果RegularEnumSet对于小的枚举类型不再有性能优势,它有可能在将来的版本中消失并且不会有不好的影响。同样的,出于提高性能的考虑,将来也有可能添加第三个或者第四个EnumSet的实现。客户既不知道也不关心从这个工厂方法获得的对象;他们仅仅关心获得的这个对象是EnumSet的子类。

静态工厂方法的第五个优点是,在编写包含该返回对象的类时,返回对象的类不需要存在。这种灵活的静态工厂方法来源于基本的服务提供者框架(service provider framerworks),比如Java Database Connection API(JDBC)。服务提供者框架是一个提供服务实现的提供者系统,这个系统提供实现给用户,从而将客户端从实现中分离出来。

服务提供者框架有三个必要的组件:一个服务的接口以及它的实现;一个供应者注册API,让供应者注册服务的实现;和一个服务访问API,让客户来获取服务的实例。服务访问API可以允许用户指定条件来选择相应的实现。在条件确实的情况下,服务访问API会返回一个默认的实现,或者允许客户遍历所有的实现。这个服务访问API是一个灵活的静态工厂是服务提供者框架的基本。

服务提供者框架第四个可选组件是服务提供者接口,它是用来生产服务接口实例的工厂对象。当服务提供者接口消失的情况下,必须以反射的方式(reflectively Item 65)实例化。例如JDBC, Connection 作为服务接口的部分, DriverManager.registerDriver 是提供者注册的API,DriverManager.getConnection 是服务访问的API,Driver 是服务提供者的接口。

有很多消息服务框架模式的变体。例如,相比提供者提供的,服务访问API可以返回一个更加丰富的服务接口给客户。这是Bridge pattern(Gamma95)。依赖注入框架(Item 5)可以看做是强大的服务提供者。从Java6开始,这个平台引入了general-purpose 服务提供者框架,java.util.ServiceLoader, 所以你不需要,一般情况下也不应该,自己去写(Item 59)。JDBC 没有使用ServiceLoader,因为前者早于后者。

仅仅提供静态工厂方法的主要限制是类没有public或者protected 的构造方法,因此不可以子类化。例如,在Collections框架里面任何便利的实现是不可能子类化的。但是这样也会因祸得福,因为它鼓励程序员使用组合(composition)而不是继承(Item 18),并且是不可变类型锁需要的(Item 17)。

第二个缺点是静态工厂方法很难被程序员发现。他们不会像构造函数那样出现在API文档里面,所以对于提供静态工厂方法而不是构造函数的类来说,想要查明如何实例化是很困难的。Javadoc工具可能某天会注意到静态工厂方法。与此同时,你可以采取通用的命名规则来减缓这类问题,使得静态工厂方法被引起注意。下面给出一些静态工厂方法比较通用的名字,但仅仅是一小部分(far from exhaustive):

  • from - 类型转换方法,输入单个参数然后返回一个相应类型的实例。例如:

    java
    1
    Date d = Date.from(instant);
  • of - 聚合方法,输入多个参数然后返回一个合并的类型。例如:

    java
    1
    Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf - 更详细的替代from和of,例如:

    java
    1
    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance / getInstance - 返回一个实例,如果有输入参数的话,这些参数可以描述该实例但不能说他们有相同的值。例如:

    java
    1
    StackWalker luke = SatckWalker.getInstance(options);
  • create / newInstance - 类似于instance/getInstance,但是create/newInstance保证每次调用都会返回新的对象。例如:

    java
    1
    Object newArray = Array.newInstance(calssObject, arrayLen);
  • getType - 类似于getInstance,但是getType是被在不同class使用的工厂方法。Type是指工厂方法返回的对象类型。例如:

    java
    1
    FileStore fs = File.getFileStore(path);
  • newType - 类似于newInstance, 但是newType是使用在不同的class中的工厂方法。Type是指工厂方法返回对象的类型。例如:

    java
    1
    BufferedReader br = Files.newBufferedReader(path);
  • type - 一种简洁的方法替代getType和newType。例如:

    java
    1
    List<Complaint> litany = Collections.list(legacyLitany);

总的来说,静态工厂方法和公共构造函数都有他们的用途,值得我们去了解他们各自的优点。通常更偏向于静态工厂方法,因此请避免在不优先考虑静态工厂的情况下提供公共构造函数。

Item 2: Consider a builder when faced with many constructor parameters

静态工厂和构造函数共同的限制:面对大量可选参数时,他们不能很好地扩展。想象一下用一个类来代表出现在包装食品上的营养成分标签。这些标签有几个必须的属性- 份量,每个容器份和每份卡路里 以及二十多个可选字段-总脂肪,饱和脂肪,反式脂肪,胆固醇,钠等。大多数产品中只有极少数产品有非零值得可选属性。

那么你应该使用什么样的构造方法或者静态工厂方法呢?传统情况下,程序员会使用 telescoping constructor pattern(伸缩构造函数模型),就是提供一个构造函数仅仅带有你需要的参数,另外一个仅有单个参数,第三个带有两个参数,等等, 最后的一个构造函数带有所有的参数。下面是类似的应用展示。为了简洁起见,仅仅显示了四个可选字段:

java
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
26
27
28
29
30
31
32
33
// Telescoping constructor pattern - does not scale well!
public class NutritionFacts {
private final int servingSize; // (mL) required
private final int servings; // (per container) required
private final int calories; // (per serving) optional
private final int fat; // (g/serving) optional
private final int sodium; // (mg/serving) optional
private final int carbohydrate; // (g/serving) optional
public NutritionFacts(int servingSize, int servings) {
this(servingSize, servings, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories) {
this(servingSize, servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat) {
this(servingSize, servings, calories, fat, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0);
}
public NutritionFacts(int servingSize, int servings,
int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}

当你想创建实例的时候,使用最短的那个并且带有所有你想要的参数:

java
1
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0,35 ,27);

通常情况下,这个构造函数调用会需要设置很多你并不需要的参数,但是不管怎么说你被强迫传入这些参数。 在这种情况下,我们传入了0值.仅仅带有6个参数可能并不是太坏,但是很快就会因为参数增长而变得措手不及。

总之,伸缩式模型是可以工作的,但是客户端的代码应对很过参数的时候回很困难,并且难以阅读。读者仅仅想知道他们是什么意思就必须很仔细的去数参数来找到答案。相同类型参数的长序列可能会导致细微的错误。如果客户不小心调换两个参数,编译器就不会报错,但是程序会在runtime时出现行为异常(Item 51)。

第二个替代方法时当你面临很多可选参数的构造函数时使用JavaBean pattern,就是调用一个没有参数的构造函数,然后调用setter方法去设置每一个必须的和感兴趣的参数:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutritionFacts {
// Parameters initialized to default values (if any)
private int servingSize = -1; // Required; no default value
private int servings = -1; // Required; no default value
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public NutritionFacts() { }
// Setters
public void setServingSize(int val) { servingSize = val; }
public void setServings(int val) { servings = val; }
public void setCalories(int val) { calories = val; }
public void setFat(int val) { fat = val; }
public void setSodium(int val) { sodium = val; }
public void setCarbohydrate(int val) { carbohydrate = val; }
}

这种模式没有伸缩式构造函数模式的缺点。很简单,但是有点啰嗦,而且容易理解:

java
1
2
3
4
5
6
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

不幸的是,JavaBeans模式自己也有很严重的缺点。因为构造函数被分为了几部分来调用,javabena 有可能在构建过程中出现状态不一致的情况。 JavaBean没有仅仅验证构造函数参数来保证强制性一致的选项。尝试使用一个状态不一致的对象可能会导致,这些bug很难被删除和调试。

文章作者: Aries Kou
文章链接: http://yoursite.com/posts/Effective-Java/199%E3%80%813168353.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Aries' Blog
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论