EffectiveJava后知后觉

这本书又叫高效 Java,书中的很多条目还是让我眼前一亮的,尤其并发和序列化,大概是因为这一块本来接触的就不多吧,但也有不少内容其实是早就知道的,所以作为我个人的记录,我并不会一条条的罗列出来,主要记录一些平时用的很少或者记得模糊的东西。
这本书感觉如果是对入门不久的开发者来说,是非常有必要读一下的,但是如果你经常阅读 JDK 或者 Spring 等开源框架源码的老鸟,其实大部分你都可以跳着看了,因为从它们的源码中基本就能学到大部分的技巧。

创建和销毁对象

对象的创建与销毁算是比较耗时的,在合适的时机创建对象在高性能的系统中是非常有必要的;频繁的 GC 也会让系统卡顿。

静态工厂代替构造器

一个类允许客户端获取其实例的传统方式是提供一个公共构造方法。还有一种比较好的选择就是静态工厂,相比构造器,主要的优势有:

  1. 拥有名称,描述更为清晰。
  2. 可避免每次调用都创建一个新对象;可以确保是一个单例或者不可实例化的亦或者不可变的,这种类被称为实例受控的类。
  3. 可以返回原返回类型的子类型,可以用这种方式来隐藏实现类,适用于基于接口的框架。
  4. 返回对象的类可以根据输入参数的不同而不同(任何子类型都是允许的)
  5. 在编写包含该方法的类时,返回的对象的类不需要存在。
    这种灵活的方案构成了服务提供者框架的基础,例如 JDBC。

当然也是有缺点的:

  • 类一般不含有公有或者受保护的构造器,所以就不能被子类化
    这也鼓励开发者多使用组合而不是继承。
  • 开发者很难找到它们。
    不过也有一些惯用名称,举例说明:
    from:类型转换方法,它接受单个参数并返回此类型的相应实例;
    of:聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起;
    valueOf:from 和 to 更为详细的替代方式;
    instance 或 getinstance:返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值;
    create 或 newInstance:与 instance 或 getInstance 类似,除此之外该方法保证每次调用返回一个新的实例;
    getType:与 getInstance 类似,但是在工厂方法处于不同的类中的时候使用(Type 是工厂方法返回的对象类型);
    newType:与 newInstance 类似,但是在工厂方法处于不同的类中的时候使用(Type 是工厂方法返回的对象类型);
    type:getType 和 newType 简洁的替代方式。

例如下面一个 boolean 提升为包装的例子:

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

这里要区分与工厂模式的差异,此例中与工厂模式没有什么直接联系。
如果你使用接口的静态方法,注意不能为私有,但是私有却是必要的,Java 9 允许私有静态方法,但静态字段和静态成员类仍然需要公开。

构造方法参数过多时使用builder模式

静态工厂和构造器有个共同的局限性,不能很好的扩展到大量的可选参数。
构建器的一般套路就是让客户端利用必要的参数来调用构造器或者静态工厂得到一个 builder 对象,然后客户端在这个对象上调用各种 setter 方法设置相关参数,最后客户端调用无参的 build 方法来生成不可变的对象。
假设使用一个个的 setter 方法来设置,这时候对象就会出现不完整状态,单线程倒还无所谓。
一般来说,构建器是可以使用调用链的,这种写法非常舒服,记得在 Android 中有大量使用。


当然,构建器也有它的局限性,要创建对象你得先搞出一个构建器来,创建对象需要开销啊,在非常注重性能的系统里就有点问题了。
构建器显得更加冗长,因此只有在很多参数的时候才使用,比如 4 个以上,如果以后有可能会增加参数那么一开始就最好使用构建器。
在参数众多,并且很多参数是可选的或者相同类型,那么 builder 模式是个很不错的选择。

关于单例

单例真的是相当有料啊,常见的“饿汉式”基本可以保证单例,只能用大部分情况下,最简单的,使用反射可以轻易的调用私有的构造函数,对应的防范措施就是修改构造器,让它在创建的时候检查是否已经存在,如果已存在抛出异常。
如果单例的对象需求为可序列化的,那么仅仅实现标记接口是不行的,这样还是可能会出现第二个;为了维护并保证单例,必须声明所有实例域都是瞬时(transient)的,并提供一个 readResolve 方法(具体实现可以是返回单例对象即可)


从 1.5 之后,实现单例还可以使用枚举,这种方式虽然跟上面的公有静态方法获取没啥区别,但是更简洁;无偿的提供了序列化机制,绝对防止多次实例化,这差不多已经成为最佳的实现单例方法,虽然没广泛使用。


然后说起工具类,一般都是静态方法组成的,也不需要实例化,为了防止实例化,可以把构造改为私有,但只有这样还不行,和单例不同,这样内部可以实例化,但是工具类内部也没有实例化的需求,所以在构造里直接抛一个异常是最简单的做法,只不过这样的代码看起来比较奇怪,写上注释比较好,当然它也就不可能有子类了。

避免创建不必要的对象

当然,能重用就不要浪费资源去创建相同功能的新对象,如果对象是不可变的那么它始终可以被重用(使用单例模式)。
对于同时提供了静态工厂方法和构造器的不可变类,通常使用静态工厂而不是构造器,以避免创建不必要的对象,例如 Boolean.valueOf(String)Boolean(String),后者在 Java9 中已被废弃。

有些情况下看起来并不是很明细,例如 Map 中的 ketSet,返回键值的 set 集合,第一次看上去会以为每次调用都会创建一个新的 set 返回,我们可以对这个 set 进行操作;实际上,他们返回的 set 都是同一个,因为对应的是一个 Map,可以称作为这个 Map 的一个视图(适配器),所以当改变这个 set 与之关联的都会变化。
在使用正则匹配的时候如果多次使用也要使用 Pattern 实例,因为创建它是昂贵的。
PS:自动装箱是非常耗费性能的!与基本数据类型尽量不要混用。

当然,小对象的构造器只做少量的工作,所以小对象的创建和回收动作是非常廉价的,通过附加对象提升程序的清晰性和简洁性是好事。

对象的公共方法

重写 equals 方法看起来很简单,但是有很多方式会导致重写出错,其结果可能是可怕的。避免此问 题的最简单方法是不覆盖 equals 方法,在这种情况下,类的每个实例只与自身相等。
什么时候需要重写 equals 方法呢?如果一个类包含一个逻辑相等(logical equality)的概念,此概 念有别于对象标识(object identity),而且父类还没有重写过 equals 方法。
这种情况多发生在值类型的情况,例如 String、Integer 之类。如果真的需要重写,请保证下面的几个约定:

  • 自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。
    即:一个对象必须与自身相等
  • 对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须 返回 true。
    即:任何两个对象必须在是否相等的问题上达成一致
  • 传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则 x.equals(z) 必须返回 true。
    即:如果第一个对象等于第二个对象,第二个 对象等于第三个对象,那么第一个对象必须等于第三个对象
  • 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false。
    即:如果两个对象是相等的,除非一个(或两 个)对象被修改了, 那么它们必须始终保持相等
  • 对于任何非空引用 x, x.equals(null) 必须返回 false。
    即:所有的对象都必须不等于 null

如果不遵守,程序可能就会出现各种奇奇怪怪的问题。
对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;对于对象引用属性,递归地调用 equals 方法;对于 float 基本类型的属性,使用静态 Float.compare() 方法(double 也是类似,避免使用 equals 装箱操作);对于数组属性,将这些准则应用于每个元素。 如果数组属性中的每 个元素都很重要,请使用其中一个重载的 Arrays.equals 方法,对于可能存在空的情况,使用 Objects 的 equals 方法也许不错。
equals 方法的性能可能受到属性比较顺序的影响。 为了获得最佳性能,你应该首先比较最可能不同 的属性,开销比较小的属性,或者最好是两者都满足(derived fields)

编写和测试 equals(和 hashCode)方法很繁琐,生的代码也很普通。替代手动编写和测试这些方 法的优雅的手段是,使用谷歌 AutoValue 开源框架,该框架自动为你生成这些方法,只需在类上添加一 个注解即可。
虽然 IDE 也会提供相关功能,但是生成的代码很冗长,并且不能动态更新。

然而,大多数情况下,我们并不需要去重写 eq。


在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。 如果不这样做,你的类违反了 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作,相等的对象必须具有相等的哈希码
这在根据类的 equals 方法,两个不同的实例在逻辑上是相同的情况下显得很重要。例如 HashMap 就做了优化,缓存了与每一项(entry)相关的哈希码,如果哈希码不匹配,则不会检查对象是否相等了。
哈希的生成你可能看到很多都用一个 31 这个数,因为它是一个奇数的素数,避免乘法溢出,并且它可以被 JVM 优化为位运算。
可以看出 equals 和 hashCode 使用了大量递归,这两个方法还是很重的。
如果一个类是不可变的,并且计算哈希码的代价很大,那么可以考虑在对象中缓存哈希码,而不是在每次请求时重新计算哈希码。 如果你认为这种类型的大多数对象将被用作哈希键,那么应该在创建实例时计算哈希码。
最后,不要试图从哈希码计算中排除重要的属性来提高性能


对于 toString 的通用约定是:建议所有的子类重写这个方法,不过在静态工具类中编写 toString 方法是没有意义的,同样枚举中也完全没有必要。


虽然存在 Cloneable 这样的接口,不幸的是,它没有达到这个目的。它的主要缺点是缺少 clone 方法,而 Object 的 clone 方法是受保护的。你只能借助反射来调用,仅仅因为它实现了 Cloneable 接口,就调用对象上的 clone 方法这想法是行不通的。
那么这个接口的意义是,如果一个类实现了 Cloneable 接口,那么 Object 的 clone 方法将返回该对象的逐个属性 (field-by-field) 拷贝;否则会抛出异常,这确实是一个不太好的设计。
通常情况下,实现一个接口用来表示可以为客户做什么。但对于 Cloneable 接 口,它会修改父类上受保护方法的行为。

1
2
3
4
5
6
7
8
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // Can't happen
}
}

这是一个简单的例子,在没有引用变量的情况下是可行的,但是如果涉及引用变量,就得递归调用 clone,同时它用到了协变规范;这也牵扯到我们常说的深度克隆和浅度克隆。
在数组上调用 clone 会返回一个数组,其运行时和编译时类型与被克隆的数组相同。这是复制数组的首选习语。事实上,数组是 clone 机制的唯一有力的用途。
如果 elements 属性是 final 的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值,在克隆上真的是各种问题,并且 Object 的克隆方法是不同步的,也正是这样用的并不多。
如果确实要用,实现 Cloneable 的所有类应该重写公共 clone 方法,而这个方法的返回类型是类本身。这个方法应该首先调用 super.clone,然后修复任何需要修复的属性。
对象复制更好的方法是提供一个复制构造方法或复制工厂。考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。这个规则的一个明显的例外是数组,它最好用 clone 方法复制。
这里给几个参考:Java对象的属性拷贝效率比较一入Java深似海(评价 Java 中的 clone 方法?)

类和接口

组合优于继承

不要继承一个现有的类,而应该给你的新类增加 一个私有属性,该属性是现有类的实例引用,这种设计被称为组合(composition),因为现有的类成为新类的组成部分。
如果使用继承,那么当父类有所变化时,子类也会受影响,即使子类的代码并没有任何变化。
这很像是装饰器模式,这里要区别与代理模式;包装类的缺点很少, 一个警告是包装类不适合在回调框架中使用,可能会存在 Self 问题(this 指向);有些人担心转发方法调用的性能影响,以及包装对象对内存占用, 两者在实践中都没有太大的影响。
只有在子类真的是父类的子类型的情况下,继承才是合适的。
所以,从这个角度,JDK 中 Stack 类不应该继承 Vector 类,Properties 不应该继承 Hashtable,它们之间使用组合更加合适。
继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。

接口的默认方法

从 Java8 开始,接口可以有默认方法了,这确实解决了一些问题,但是也引入了一些新问题,跟继承类似,接口的实现者是感知不到接口中新增的默认方法,
例如,一个将集合同步化的工具类,在接口新增了默认方法后并不会感知,这也导致这个方法不会被同步,如果作者不及时重写默认方法,可能就会导致问题出现。
在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时可能会失败。
所以,应该避免使用默认方法向现有的接口添加新的方法,除非这个需要是关键的。

常量接口模式

所谓的常量接口模式就是定义一个接口,里面用来存储常量;
常量接口模式是对接口的糟糕使用,对类的用户来说,类实现一个常量接口是没有意义 的;事实上,它甚至可能使他们感到困惑,如果实现了常量接口,那么它的所有子类的命名空间都会被接口中的常量所污染,并且未来版本中如果不需要常量了,仍然需要实现接口,来保证二进制兼容。
JDK 中也有部分实现,这些接口应该被视为不规范的,不应该被效仿。更好的方式应该是使用一个不可实例化的类来做,抽象类也不可以,容易被误解是来做继承的。
总之,接口只能用于定义类型。 它们不应该仅用于导出常量。

关于内部类

或者叫嵌套类, 如果一个嵌套类在其他一些情况下是有用的,那么它应该是一个顶级类
有四种嵌套类: 静态成员类,非静态成员类,匿名类和局部类。 除了第一种以外,剩下的三种都被称为内部类。
如果嵌套类的实例可以与其宿主类的实例隔离存在,那么嵌套类必须是静态成员类:不可能在没有宿主实例的情况下创建非静态成员类的实例。
静态成员类的一个常见用途是作为公共帮助类,类似:Aa.Bb.NAME,非静态成员类的一个常见用法是定义一个 Adapter,可参考 Map 中的各种视图。
在非静态成员类中,每个实例都隐藏的引用了自己的宿主,存储这个引用就需要占用时间和空间,更严重的可能会导致 GC 无法回收宿主对象,就是因为引用是不可见的,很难被检测到。
私有静态成员类的常见用法是表示由它们的宿主类表示的对象的组件,例如 Map.Entry;
总结一下就是:如果一个嵌套的类需要在一个方法之外可见,或者太长而不能很好地适应一个方法,使用一个成员类。
如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的;;否则,使其静态。
假设这个类属于一个方法内部,如果你只需 要从一个地方创建实例,并且存在一个预置类型来说明这个类的特征,那么把它作为一个匿名类;否则,把它变成局部类。

泛型

泛型已经都用的很熟练了,编译时会自动强制转换,相当于运行时擦除,注意这里编译后的字节码还是保存着泛型的信息的,还有就是泛型不可以使用原始类型(没有任何类型参数的泛型类型的名称,简单说就是不用泛型,或者使用 ?通配符的情况),这样编译器没办法帮你检查错误。
如果跟数组对比,会发现即有相似又有差异,最大的区别为数组是支持协变的,例如 String 数组可以赋值给 Object 数组,但是泛型不可能,也正是因为这个差异,导致不能使用泛型数组(尽管很少有用,创建无限定通配符类型的数组是合法的),所以作为替代方案一般我们都使用集合来替代,虽然牺牲了一些性能,但是安全。
泛型这个东西解释起来不难,但是运用到你写的代码中很难,说白了就是一个设计问题。
参数化不难,但是泛型化真的需要一定的水平。

编译器可能无法证明你的代码是安全的,所以会给你发出警告,但是你可以证明,这种情况下你可以使用 @SuppressWarnings 注解来进行标注,并使用注释来说明原因。
使用 SuppressWarnings 的时候一定要小心,做到最小范围化,否则可能就出现吞掉其他警告的可能。

泛型的另一个难点是限定通配符的使用:

  • 上限(<? extends E>
    说明传入的可以是 E 或者 E 的子类
  • 下限(<? super E>
    说明传入的值可以是 E 或者 E 的父类

这个的用法其实有点误导,感觉有点别扭;
另外需要注意的一点,可变参数也不可以和泛型同用,因为可变参数的本质就是数组,泛型数组是不可以用的。
不过可变参数的情况下还是有点区别,它不会直接像数组那样给你一个错误,而是给你一个警告,这是因为像 T... 这样的用法是安全的,但是 List<T>... 这种就是不安全的,或者可以理解为如果仅仅是单纯的传递参数是安全的,总而言之,可变参数和泛型不能很好地交互。

枚举

建议使用枚举类型来代替整型常量,使用枚举确实有很多好处,但是以我的观察,还是用 int 常量的多,我猜可能是因为这种入库的时候方便,如果 Entry 使用枚举定义,可能会有点复杂,并且与前端的约定也一般是 int 标识,还要处理序列化相关,当然如果不涉及入库操作,枚举肯定是首选。
同时,枚举确实是非常灵活的,例如:

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
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
public double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}


// Enum type with constant-specific class bodies and data
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
}

第一种方案是有问题的,首先我们不喜欢有异常参与,主要是展示可以这样用 switch 语法。
另外,永远不要从枚举的序号中得出与它相关的值; 请将其 保存在实例属性中(最好避免使 用 ordinal 方法)。

对于 int 类型的常量取值,参考 JDK 的位运算顺序是个不错的选择,即:
1 << 0 // 1
1 << 1 // 2
1 << 2 // 4
使用位域的好处:TBD

枚举其实还是蛮复杂的,不过感觉我们平常用的真的很少,也许确实有一些复杂性在里面。

Lambdas和Streams

Lambda 优于匿名类,一行代码对于 lambda 说是理想的,三行代码是合理的最大值。 如果违反这一规定,可能会严重损害程序的可读性。
如果枚举类型具有难以理解的常量特定行为,无法在几行内实现,或者需要访问实例属性或方法,那么常量特定的类主体仍然是行之有效的方法。
虽然 Lambda 是首选,但是 Lambda 并不能完全替代匿名类,其中还要注意 this 指向的问题。

方法引用优于 lambda 表达式,如果 lambda 变得太长或太复杂,它们也会给你一个结果;你可以从 lambda 中提取代码到一个新的方法中,并用对该方法的引用代替 lambda。 你可以给这个方法一个好名字,并把它文档记录下来(通常 IDE 会有智能提示)。
还有就是在 JDK 中就包含了很多内置函数,优先使用这些内置的,没有合适的再自己定义,大部分情况下,自带的就足够了,自己定义的时候记得用 @FunctionalInterface 标记。


对于 Stream API, 该 API 提供了两个关键的抽象:流 (Stream),表示有限或无限的数据元素序列,以及流管道 (stream pipeline),表示对这些元素的多级计算。
Stream pipeline 通常是惰性 (lazily) 计算求值;直到终结操作被调用后才开始计算,而为了完成终结操作而不需要的数据元素永远不会被计算出来。 这种惰性计算求值的方式,使得无限流成为可能。
通常情况下,流管道会按顺序运行,如果要并行,只需要调用 parallel 方法,但是并不建议这么做。
Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但是「可以」,并不意味着应该这样做;过度使用流会难以阅读和维护。
使用 Lambda 过程中,因为没有显式类型声明,所以好的命名非常重要。
流式处理有一个缺点是在多个阶段中一旦将值映射到其他值,原始值就丢失了。流与迭代那种方式好,要具体情况具体分析,通常来说迭代不适合并行化,也不适合计算场景。
纯函数的结果仅取决于其输入;它不依赖于任何可变状态,也不更新任何状态,即所谓的无任何副作用。

对于流的并行化,看似非常简单,只需要调用一下 parallel,但是编写安全高效的并发代码还是一样的困难,你可能会遇到各种奇奇怪怪的现象,并且可能没有任何错误提示。
所以,一定要慎用并行处理。

方法

关于方法的设计,例如方法名的选取、参数不要过多、优先使用接口类型等等这些都已是业界规范,除此之外与布尔型参数相比,优先使用两个元素枚举类型
对于重载,也需要警惕,重载(overloaded)方法之间的选择是静态的,而重写 (overridden)方法之间的选择是动态的,这里关键是静态选择,无论你运行时实际是什么类型,都是根据你编写时候来确定走那个重载。
可变参数和数组是一种,不可算重载。在数组和 Object 的重载中,null 会优先匹配数组类型,因为比 Object 更具体。
很多时候重载不如另起一个更好的名称,例如 readInt、readLong 比使用重载更形象。
像自动装箱、Lambda 之类进一步增加了重载的复杂性。开发者应当确保当传递相同的参数时,所有的重载行为都是一致的。
性能关键的情况下使用可变参数时要小心,每次调用可变参数方法都会导致数组分配和初始化。

尽量放回空数组或者集合,不要使用 null,编写客户端的程序员可能忘记编写特殊情况代码来处理 null 返回,如果有证据表明分配空集合会损害性能,可以通过重复返回相同的不可变空集合来避免分配,或者例如 Collections.emptySet 对象;
不论是抛出异常还是返回 null,都不合适,前者代价太高,后者需要调用者单独处理;在 Java8 中的 Optional 或许是一个比较好的方案,跟 Guava 中的用法基本一致,容器类型,包括集合、映射、Stream、数组 和 Optional,不应该封装在 Optional 中。
需要注意的是:返回包含已装箱基本类型的 Optional 的代价高得惊人,他们有专门的类似 OptionalInt 的包装。
在集合或数组中使用 Optional 的键、值或元素几乎都是不合适的。
除了作为返回值之外,不应该在任 何其他地方中使用 Optional,使用它也必然会带来一定的性能消耗。

通用程序设计

从 Java 7 开始,就不应该再使用 Random。在大多数情况下,选择的随机数生成器现在是 ThreadLocalRandom 它能产生更高质量的随机数,而且速度非常快。
每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包,也就是不要重复造轮子。
若需要精确答案就应避免使用 float 和 double 类型,二进制无法精确表示 0.1,就像十进制无法表示 1/3,要么进行倍数换算成整数运算,要么使用 BigDecimal,不过使用 BigDecimal 的时候一定要用字符串的构造,否则也是不精确的。
BigDecimal 与原始算术类型相比很不方便,而且速度要慢得多;如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。

警惕自动装箱的风险,在比较的时候 == 所带来的问题,以及包装类型默认值为 null,避免 NPE。

不要去计较效率上的一些小小的得失,在 97% 的情况下,不成熟的优化才是一切问题的根源。
优化方面的准则:不要优化;在你还没有绝对清晰的未优化方案之前,请不要进行优化。
努力编写好的程序,而不是快速的程序。
这并不意味着在程序完成之前可以忽略性能问题,实现上的性能问题可以日后优化,对于架构缺陷,如果不重写系统,就不可能解决限制性能的问题,因此在设计时就要考虑性能。
总结一下就是,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设 API、线路层协议和持久数据格式时。
当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。
第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。

在命名方面,不可实例化的实用程序类通常使用复数名词来命名(xxxxs),转换对象类型的实例方法用 to 开头,返回视图的方法用 as 开头;返回布尔类型的方法用 is 或者 has(比较少)开头;组件(包名)应该很短,通常为 8 个或更少的字符,鼓励使用有意义的缩写,例如 util 而不是 utilities。

异常

只有在异常情况下才能使用异常,一切其他的骚操作都是不可取的,不仅模糊了代码意思,还降低性能,不要将它们勇于普通的控制流程。
优先使用标准异常例如:IllegalArgumentException、IllegalStateException、UnsupportedOperationException。
对于底层抛出的异常,直接抛出会让人摸不着头脑,通常会在高层进行转换,方便阅读,并且最好写清楚文档。

并发和序列化

关于如何停止线程,stop 方法早已经废弃,主流是控制标志位的方法,但是请注意,标志位的变量一定要 volatile 保证可见性,因为 JVM 的优化(在非同步代码中可能会重排)和 Java 内存模型的关系,必须要进行 volatile 化
或者使用 synchronized 进行同步读写标志位的方法,注意,是读写都需要同步,单独独立到方法中去。
同时,要避免过度同步,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,这个类不知道该方法会做什么事情,也无法控制它,根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏(Effective Java 中的例子很经典)。
应该将外来代码移出同步区,必要情况可以使用快照方式来避免 CME 等异常,例如使用 JUC 的 CopyOnWriteArrayList。
当你不确定的时候,就不要同步类,而应该建立文档,注明它不是线程安全的,让调用者从外部同步;如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻塞并发控制。
不建议使用困难的 wait 和 notify,也正是因为正确地使用 wait 和 notify 比较困难,就应该用更高级的并发工具来代替。
最常用的同步器是 CountDownLatch 和 Semaphore,较不常用的是 CyclicBarrier 和 Exchanger ,功能最强大的同步器是 Phaser。
Lock 字段应该始终声明为 final,对于延迟初始化,除非需要,否则不要这样做。


关于序列化,请优先选择 Java 序列化的替代方案,序列化的一个根本问题是它的可攻击范围太大,且难以保护,而且问题还在不断增多;当你反序列化一个你不信任的字节流时,你就会受到攻击。避免序列化利用的最好方法是永远不要反序列化任何东西。
反序列化真的非常危险,你不应该接受来自不可信来源的 RMI 流量,但同时它确实又是必须的,或者说广泛使用的,比如 RMI(远程方法调用)、JMX(Java 管理扩展)和 JMS(Java 消息传递系统)。
设计不良的序列化形式,可能会造成严重后果;序列化是一种用于创建对象的超语言机制,或者说『隐藏的构造函数』,readObject 方法实际上相当于另外一个公有的构造器,或者说是一个「用字节流作为唯一参数」的构造器,并且这个字节流可以伪造;如无必要不要实现 Serializable。
为了防止字节流的伪造,建议重写 readObject 方法,在调用 defaultReadObject 之后进行手动校验,但这也只能防止部分攻击;自定义序列化规则的时候注意 transient 的运用,反序列化的时候进行手动恢复。
当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做 保护性拷贝,这是非常重要的。
如果单例对象要序列化,请在 readObject 忽略任何序列化相关的逻辑,因为默认无论你做如何防范,它总会返回一个新对象。
readResolve 的可访问性 (accessibility) 也十分重要,单例的防攻击可以使用枚举类限制。

PS:领先的跨平台结构化数据表示是 JSON 和 Protocol Buers,也称为 protobuf(二进制,效率更高)。
如果没有显式声明 serialVersionUID,系统会自动使用 SHA-1 进行生成。
建议是无论选择哪种序列化形式,都要在编写的每个可序列化类中声明显式的序列版本 UID,目的是为了版本兼容。
建议使用序列化代理模式(为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理),序列化代理方式可以阻止伪字节流的攻击以及内部字段的盗用攻击,但是也有一定的局限性,例如性能方面的损耗。

其他

如果 instanceof 的第一个操作数是 null,那么不管第二个操作数是那种类型,都返回 false;

函数式方法:每次执行返回一个新的实例,而不是修改这个实例。要善用不可变对象;缺点主要是每个不同的值都需要创建一个单独的对象,创建过程可能代价很高。

除非有充分的理由使类成为可变类,否则类应该是不可变的,唯一的缺点是在某些情况下可能会出现性能问题。

序列化攻击。

Java7 开始有了下划线语法,对于底数为 10 的数字,无论是整型还是浮点型的,都应该用下划线将数字分成三个数字组,表示一千的正负幂。

Java8 开始支持可重复注解,使用 @Repeatable;

标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
标记接口对于标记注解的另一个优点是可以更精确地定位目标;

优先使用基本类型而不是基本类型的包装类。

在 Java 7 中添加的 Objects.requireNonNull 方 法灵活方便,因此没有理由再手动执行空值检查。

性能监控方面,JDK Micro Benchmark Framework:JMH

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~