作者:eclipse
为性能而设计, 第二部分: 减少对象创建[/b]
From Java World.
在设计 Java 类的时候避免性能上的冒险
概要
许多通常的 Java 性能问题都起源于在设计过程早期中的类设计的思想, 早在许多开发者
开始考虑性能问题之前. 在这个系列中, Brian Goetz 讨论了通常的 Java 性能上的冒险
以及怎么在设计时候避免它们. 在第二部分, 他讨论了减少临时对象创建的一些技术.
(1,700 字)
By Brian Goetz
翻译 by SuperMMX
阅读整个"为性能而设计"系列:
第一部分: 接口事宜
第二部分: 减少对象创建
第三部分: 远程接口 (March 23, 2001)
虽然许多程序员把性能管理一直推迟到开发过程的最后, 性能考虑应该从第一天起就和设
计周期结合在一起. 这个系列探索一些早期的设计思想能够极大影响应用程序性能的方法.
在这篇文章里, 我继续探索大量临时对象创建的问题, 并且提供一些避免它们的一些技术.
临时对象就是一些生命周期比较短的对象, 一般用于保存其他数据而再没有其他用途. 程
序员一般用临时变量向一个方法传递数据或者从一个方法返回数据. 第一部分探讨了临时
对象是怎样给一个程序的性能带来负面的冲击, 以及一些类接口的设计思想怎样提供了临
时对象的创建. 避免了那些接口的创建, 你就能极大地减少那些影响你的程序性能的临时
对象创建的需求,
只是对 String 说不吗?
当它要创建临时变量时, String 类是最大的罪人中的一个. 为了演示, 在第一部分我写了
一个正规表达式匹配的例子, 通过和一个类似的但是经过仔细设计的接口相比较, 演示了
看起来无害的接口是怎样引起大量对象的创建, 而慢上几倍. 这里是原来的和好一些的类
的接口:
BadRegExpMatcher
[code]
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input
text, returning the matched text if possible or null if not */
public String match(String inputText);
}
[/code]
BetterRegExpMatcher
[code]
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return -1 if no
match was made; return offset of match start if a match was
made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** If a match was made, returns the length of the match; between
the offset and the length, the caller should be able to
reconstruct the match text from the offset and length */
public int getMatchLength();
/** Convenience routine to get the match string, in the event the
caller happens to wants a String */
public String getMatchText();
}
[/code]
大量使用 BadREgExpMatcher 的程序比使用 BtterRegExpMatcher 的要慢好多. 首先,
调用者不得不创建一个 String 传入 match(), 接着 match() 又创建了一个 String 来
返回匹配的文本. 结果是每次调用都有两个对象创建, 看起来不多, 但是如果要经常调用
match(), 这些对象创建带给性能的代价就太打了. BadRegExpMatcher 的性能问题不是在
它的实现中, 而是它的接口; 象它定义的接口, 没有办法避免一些临时变量的创建.
BetterRegExpMatcher 的 match() 用原类型(整数和字符数组)代替了 String 对象; 不需
要创建中间对象来在调用者和 match() 之间传递信息.
既然在设计时候避免性能问题要比写完整个系统以后再修改要容易一些, 你应该注意你的
类中控制对象创建的方法. 在 RegExpMatcher 的例子中, 它的方法要求和返回 String
对象, 就应该为潜在的性能冒险提个警告信号. 因为 String 类是不可变的, 除了最常用
以外, 所有的 String 参数在每次调用处理函数时都需要创建一个新的 String.
不可变性对于性能来说是否很坏?
因为 String 经常和大量的对象创建联系在一起, 一般来说归咎于它的不可变性. 许多
程序员认为不可变的对象与生俱来对性能没有好处. 但是, 事实多少会更复杂一些. 实
际上, 不可变性有时候提供了性能上的优势, 可变性的对象有时候导致性能问题. 不管可
变性对性能来说有帮助或者有害, 依赖于对象是怎么使用的.
程序经常处理和修改文本字符串 -- 和不可变性非常不匹配. 每次你想处理一个 String --
想查找和解析出前缀或者子串, 变小写或者大写, 或者把两个字符串合并 -- 你必须创建
一个新的 String 对象. (在合并的情况下, 编译器也会隐藏地创建一个 StringBuffer()
对象)
另一个方面, 一个不可变的对象的一个引用可以自由共享, 而不用担心被引用的对象要被
修改, 这个比可变对象提供性能优势, 就象下一节例子所说的.
可变的对象有它们自己的临时对象问题.
在 RegExpMatcher 的例子中, 你看见了 当一个方法返回一个 String 类型时, 它通常
需要新建一个 String 对象. BadRegExpMatcher 的一个问题就是 match() 返回一个对
象而不是一个原类型 -- 但是只因为一个方法返回一个对象, 不意味着必须有一个新对
象创建. 考虑一下 java.awt 中的几何类, 象 Point 和 Rectangle. 一个 Rectangle
只是四个整数(x, y, 宽度, 长度)的容器. AWT Component 类存储组件的位置, 通过
getBounds()作为一个Rectangle 返回
[code]
public class Component {
...
public Rectangle getBounds();
}
[/code]
在上面的例子中, getBounds() 只是一个存储元 -- 它只使一些 Component 内部的一
些状态信息可用. getBounds() 需要创建它返回的 Rectangle 吗? 可能. 考虑一下下面
getBounds() 可能的实现.
[code]
public class Component {
...
protected Rectangle myBounds;
public Rectangle getBounds() { return myBounds; }
}
[/code]
当一个调用者调用上面例子中的 getBounds(), 没有新对象创建 -- 因为组件已经知道它
在哪里 -- 所以 getBounds() 效率很高. 但是 Rectangle 的可变性又有了其他问题. 当
一个调用者运行一下程序会发生什么呢?
[code]
Rectangle r = component.getBounds();
...
r.height *= 2;
[/code]
因为 Rectangle 是可变的, 它在 Component 不知道的情况下使 Component 移动. 对象
AWT 这样的 GUI 工具箱来说, 这是个灾难, 因为当一个组件移动以后, 屏幕需要重绘,
件监听器需要被通知, 等等. 所以上面的实现 Component.getBounds() 的代码看起来很
危险. 一个安全一点的实现就象下面这样:
[code]
public Rectangle getBounds() {
return new Rectangle(myBounds.x, myBounds.y,
myBounds.height, myBounds.width);
}
[/code]
但是现在, 每一个 getBounds() 的调用都创建一个新对象, 就象 RegExpMatcher 一样.
实际上, 下面的代码片段创建了 4 个临时对象:
[code]
int x = component.getBounds().x;
int y = component.getBounds().y;
int h = component.getBounds().height;
int w = component.getBounds().width;
[/code]
在 String 的情况中, 对象创建是必要的, 因为 String 是不可变的. 但在这个例子中,
对象的创建也是必要的, 因为 Rectangle 是可变的. 我们使用 String 避免了这个问题,
在我们的接口中没有使用对象. 虽然在 RegExpMatcher 的情况下很好, 这个方法不总是
可行的或者是希望的. 幸运的是, 你可以在实际类的时候可以使用一些技巧, 来免除太多
小对象的问题, 而不是完全避免小对象.
减少对象的技巧 1: 加上好的存取函数
在 Swing 工具箱的初始版本中, 对象小对象的临时创建, 象 Point, Rectangle 和 Dimension
极大地阻碍了性能. 把它们放在一个 Point 或者 Rectangle 中来一次返回多个值, 看起
来更有效, 实际上, 对象的创建比多个方法调用代价更高. 在 Swing 的最后发布之前, 通
过给 Component 和其他一些类加一些新的存取方法, 问题就简单地解决了, 就象下面这样:
[code]
public int getX() { return myBounds.x; }
public int getY() { return myBounds.y; }
public int getHeight() {