高性能Java
一天一个Effective Java小技巧
创建与销毁对象
用静态工厂方法取代构造器
例子:
1
2
3
|
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
|
使用静态工厂方法的好处:
- 静态工厂方法有名字。如果构造器的参数不足以描述返回对象的性质,给静态工厂方法选取一个好名字可以让代码更加便于使用和阅读。而且构造器在相同signature下只能有一个,静态工厂方法没有这种限制,通过提供不同名字可以突出它们的不同。
- 静态工厂方法不需要每次调用都创建新对象。这让immutable类可以使用预先构造的实例或缓存构造的实例,避免创建重复的对象。例如
Boolean.valueOf(boolean)
方法就从不创建对象。如果经常需要相同的对象特别是创建的代价比较高时,这样可以极大地提升性能。静态工厂方法在重复调用每次返回相同的对象可以让类更加严格地控制任意时间存在的实例。
- 静态方法工厂可以返回返回类型的任意子类型对象。
- 静态方法工厂可以根据输入参数不同返回不同的对象。
- 静态工厂方法返回对象的类在写此方法时不存在。
静态方法工厂的局限:
- 如果只提供静态方法工厂,而没有public或protected的构造器将无法被继承。
- 静态方法工厂很难找。可以使用常见的命名规则来避免此弊端。
构造器参数组合比较多时使用Builder模式
构造器和静态工厂方法都不能很好地应对大量可选参数。这种情况很多时候会提供一个最完整参数的构造器,然后缺失的构造器提供部分默认值来调用此完整的构造器,这很容易出错;也可以使用Java Bean模式,调用一个无参的构造器,然后使用set方法一个个赋值,由于构造涉及多个调用,可能会造成构造过程中处于不一致状态,一个相关的缺点就是,JavaBeans模式排除了使类不可变的可能性,需要更多的精力来保证线程安全。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// Builder Pattern
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
// Required parameters
private final int servingSize;
private final int servings;
// Optional parameters - initialized to default values
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val)
{ calories = val; return this; }
public Builder fat(int val)
{ fat = val; return this; }
public Builder sodium(int val)
{ sodium = val; return this; }
public Builder carbohydrate(int val)
{ carbohydrate = val; return this; }
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
|
Builder模式可读性更好,也适合继承。
使用构造器或枚举类型强制singleton属性
singleton就是指类只能实例化一次。
有两种常见的方法来实现singleton。一是私有化构造器,并提供公有的静态成员访问。可以使用final成员变量:
1
2
3
4
5
6
7
|
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
|
除非使用AccessibleObject.setAccessible
方法,否则没有办法访问私有构造器。可以修改构造器在第二次实例化的时候抛出异常以避免这种情况。
1
2
3
4
5
|
private Elvis() {
if (INSTANCE != null)
throw new IllegalStateException("Only one instance may be created");
System.out.println("Object is created.");
}
|
也可以使用静态工厂方法来实现singleton
1
2
3
4
5
6
7
8
|
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
|
这种公有静态成员变量的方式主要优点是简单清晰,通过API就可以知道相应的类是singleton,因为公有的静态成员是final的。而静态工厂方法的优点是可以灵活地决定是否遵从singleton而无需变动API,也可以实现泛型,最后静态工厂方法可以使用method reference如Elvis::instance。
如果类需要进行序列化,则上述方法无法保证单一实例,这种情况下参考使用enum代替readResolve控制实例,给类添加readResolve方法,并将实例字段标记为transient。
第三种方式是声明一个单元素枚举类型:
1
2
3
4
5
6
|
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
|
这种方式最好,但是如果必须继承除Enum以外的类则无法使用。
使用私有构造器禁止实例化
例如有些工具类只需提供静态方法API,而无需实例化。如果将类标记为abstract虽然可以避免实例化,但是让人误以为需要继承。Java默认提供无参构造器,可以提供无参的私有构造器以覆盖默认行为,达到无法实例化的目的,如下:
1
2
3
4
5
6
7
8
|
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... // Remainder omitted
}
|
AssertionError不是必需的,但是可以避免内部误调用。
这样做的副作用就是无法被继承,因为子类没有可以使用的父类构造器。
依赖注入优于硬连接资源
很多类都依赖一个或多个基础资源,如spell cheker依赖dictionary,如下两个错误都使用方式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
|
上述方法都假定只有一个值得使用的字典,但是通常不同语言有自己的字典。也许你会将字典字段设为非final可修改,但是这样既容易出错也无法满足并发设置。静态工具程序类和单实例不适合其行为由基础资源参数化的类。
要处理多实例,每个实例使用自己的资源,一个简单的方式是在创建实例时将资源作为参数传给构造器,这就是依赖注入的一种形式。
1
2
3
4
5
6
7
8
9
10
11
|
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
|
此模式的变体就是将资源工厂传给构造器,工厂就是可以重复创建某种类型实例的对象。Supplier<T>
接口非常适合代表工厂,接受Supplier<T>
输入的方法应该使用bounded wildcard type限制参数,以传入创建指定类型子类型的工厂,如下:
1
|
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
|
如果在项目中有成千上万的依赖时,就可以借助一些依赖注入的框架,如Spring。
避免创建不必要的对象
重复使用单个对象比需要时创建等价对象要合适,如果一个对象是immutable那么永远可以复用。
一个极端的例子如下:
1
|
String s = new String("bikini"); // DON'T DO THIS!”
|
“bikini"本身就是一个String实例,与构造器创建的对象是一样的,改写为:
而且,这样可以让虚拟机内其他使用到此字符串字面量的代码复用此对象。
对于一些同时提供了静态工厂方法和构造器的immutable类,可以使用工厂方法避免重复创建对象。例如Boolean.valueOf(String)就优于Boolean(String)。除了immutable对象,如果可以确定不会修改的mutable对象也可以复用。
如果创建对象的代价很大,那么可以将这些对象缓存。例如判断字符串是否是罗马数字:
1
2
3
4
5
|
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
|
尽管String.matches是判断字符串是否包含特定正则表达式的最简单方式,问题是此方法内部创建了一个只使用一次就回收的Pattern对象,创建此Pattern对象的开销很大,如果大量重复使用就对性能有很大影响。可以在实例化时缓存此对象:
1
2
3
4
5
6
7
8
9
10
|
// Reusing expensive object for improved performance
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
|
如果此方法从不被调用,那么ROMAN字段的初始化就没有用,可以采用延迟初始化,不过此方法要慎重使用,因为很多时候此方式除了增加实现复杂度没有可测量的性能提升。
例如Map接口的keySet方法看似会每次创建新的Set实例,但实际上同一个Map对象可能返回相同的Set实例。虽然返回的Set实例通常是可变的,但所有返回的对象在功能上都是相同的:当一个返回的对象发生更改时,其他所有对象也会发生变化,因为它们都由同一个Map实例支持。 虽然创建keySet对象的多个实例在很大程度上是无害的,但是这是不必要的,没有任何好处。
创建不必要对象的另一种方式是autoboxing:
1
2
3
4
5
6
7
8
|
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
|
程序创建了2^31个不必要的Long对象,如果将Long替换为long性能能提升很多。所以优先使用原始类型,注意无意识的autoboxing。
这里的目的不是说创建对象的代价很大,应该避免。事实上,在现代JVM上创建小对象是很经济的。相反,除非池中的对象非常重,否则通过维护自己的对象池来避免创建对象是一个坏主意。 证明对象池合理的对象的经典示例是数据库连接。 建立连接的成本非常高,以至于可以重用这些对象。 但是,一般而言,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。 现代JVM实现具有高度优化的垃圾收集器,可以轻松地在轻量级对象上胜过此类对象池。
消除过期对象引用
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
|
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
|
如果堆栈增大然后缩小,那么即使使用该堆栈的程序没有更多引用,从堆栈弹出的对象也不会被垃圾回收。这是因为堆栈维护了对这些对象的过时引用。过时的引用只是一个引用,它将不再被取消引用。在这种情况下,元素数组“活动部分”之外的所有引用均已过时。活动部分由索引小于大小的元素组成。
垃圾收集语言(更恰当地称为非故意对象保留)中的内存泄漏是隐患。如果意外地保留了对象引用,则不仅该对象将从垃圾回收中排除,而且该对象引用的任何对象也将被排除在外,依此类推。即使无意中仅保留了少数几个对象引用,也可能会阻止许多很多对象被垃圾回收,从而对性能产生潜在的巨大影响。
此类问题的解决方法很简单:一旦引用过时,则将其清空。就我们的Stack类而言,对某个项目的引用在从堆栈中弹出后便会过时。更正版本:
1
2
3
4
5
6
7
|
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
|
一般来说,如果类自己管理内存,就需要注意内存泄露。
共有方法
序列化
推荐的Java序列化替代方法
序列化的一个基本问题是,其攻击面太大而无法保护,并且会不断增长:通过在ObjectInputStream上调用readObject方法,可以反序列化对象图。 该方法本质上是一个魔术构造函数,可以使该类实例化类路径上几乎任何类型的对象,只要该类型实现Serializable接口即可。 在反序列化字节流的过程中,此方法可以执行任何一种类型的代码,因此所有这些类型的代码都是攻击面的一部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // Method omitted for brevity
}
|
对象图由201个HashSet实例组成,每个实例包含3个或更少的对象引用。 整个流的长度为5,744字节,问题在于,反序列化HashSet实例需要计算其元素的哈希码。 根哈希集的2个元素本身就是包含2个哈希集元素的哈希集,每个哈希集包含2个哈希集元素,依此类推,深度为100层。 因此,对集合进行反序列化将导致hashCode方法被调用2^100次以上。 除反序列化将永远持续的事实外,反序列化器没有任何迹象表明存在任何问题。 生成的对象很少,并且堆栈深度是有界的。
避免序列化攻击的最佳方法是永远不要反序列化任何东西。
使用enum代替readResolve控制实例
使用构造器或枚举类型强制singleton属性中提到如下例子,限制构造器访问以确保单一实例:
1
2
3
4
5
6
|
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
|
但是在实现Serializable接口后,此类将不再保证单一实例。无论使用默认还是自定义序列化方法,或是提供显式readObject方法。显式或默认的readObject方法都会返回新创建的对象,而不是初始化时的对象。
readResolve功能允许替换readObject创建的对象。如果要反序列化的对象的类定义了带有正确声明的readResolve方法,则在对新创建的对象进行反序列化后将调用此方法。 然后,此方法返回的对象引用将代替新创建的对象而返回。 在此功能的大多数使用中,不会保留对新创建对象的引用,因此该对象立即可以进行垃圾收集。
因此上述例子增加了如下内容后就能确保singleton:
1
2
3
4
5
6
|
// readResolve for instance control - you can do better!
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
|
该方法忽略反序列化的对象,返回在初始化类时创建的独特的Elvis实例。 因此,Elvis实例的序列化形式不需要包含任何实际数据; 所有实例字段都应声明为transient。 实际上,如果依赖readResolve进行实例控制,则必须将所有具有对象引用类型的实例字段声明为transient。 否则,攻击者可能会使用某种类似于条款88中的MutablePeriod攻击的技术,在运行反序列化对象的readResolve方法之前确保对其的引用。
此攻击的原理很简单,如果包含非transient的字段,这些字段的内容会在readResolve方法运行前进行反序列化。具体可以参考如下例子:
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
|
// Broken singleton - has nontransient object reference field!
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
private String[] favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
public class ElvisStealer implements Serializable {
static Elvis impersonator;
private Elvis payload;
private Object readResolve() {
// Save a reference to the "unresolved" Elvis instance
impersonator = payload;
// Return object of correct type for favoriteSongs field
return new String[] { "A Fool Such as I" };
}
private static final long serialVersionUID = 0;
}
|
可以将favoriteSongs声明为transient来避免此攻击,但是最好将Elvis变成单元素枚举类型来解决此问题,如使用构造器或枚举类型强制singleton属性中所说。
如果序列化实例控制的类写为枚举类型,Java就能保证除了声明的常量外不存在其他实例。除非攻击者使用AccessibleObject.setAccessible,但那时攻击者其实拥有执行任意本地代码的权限。
1
2
3
4
5
6
7
8
9
|
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs =
{ "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
|
如果编译时无法知道实例类型,就无法使用枚举类型,这时只能使用readResolve方法。同时readResolve方法的权限也很重要,如果类是final的,readResolve应该修饰为private,其他情况下,如果有子类继承,那么子类需要重写该方法,否则序列化的实例就是父类。
总之,尽可能使用枚举类型保证实例控制,如果没办法,则类中的实例字段必须为primitive或transient。