Q:我该何时调用构造函数,何时调用其它方法呢?
最直观的回答就是,在你想new一个对象的时候调用构造函数;这是new这个关键字的用途。而我的回答是:构造函数往往被滥用了,调用它们和它们所做的工作两方面都被滥用了。下面是一些需要考虑的问题:
Setter方法:正如我们在之前的问题中所看到的,有些人会写很多构造函数。而通常来讲,最好要控制住构造函数的数量,然后提供一些setter方法,让他们它们做剩余的初始化工作。如果这些方法返回this,那你可以通过一个表达式就完成对象的创建;否则,创建一个对象需要多条语句。善用setter方法是件好事,因为在创建对象时需要修改的变量往往之后也可能要修改,所以为什么要在构造函数和setter方法里写一样的代码呢?
工厂:有时候你想创建某个类或某个接口的实例,但你并不关心到底是那个子类创建的,亦或你想推迟到运行时再做决定。比如,你正在写一个计算器程序,你可能会想调用new Number(string),如果string是浮点型格式的话希望它返回Double,如果string是整数格式的话,希望它返回Long。但出于以下两点,你无法实现上述功能:Number是一个抽象类,你不能直接调用它的构造函数,并且每一次调用构造函数都会返回所属类的实例,而并不是它子类的实例。
一种可以像构造函数一样返回对象且对如何构造有更大选择余地(也可以指定其类型)的方法被称为工厂。Java没有自带对工厂模式的支持,但是你仍可以自己动手写一个工厂模式。
缓存与回收:构造函数一定会创建一个新的对象。但是创建一个新的对象消耗非常大。像现实世界中一样,你可以以循环利用的方法来降低垃圾回收的代价。比如,new Boolean(x)会创建一个Boolean对象,但你最好优先循环使用已有的值(x ? Boolean.TRUE : Boolean.FALSE),而不是浪费资源去申请一个新的。如果Java提倡使用上述的机制而不是单一的提倡使用构造函数就完美了。Boolean只是一个例子;你应该也考虑其它不可变类,诸如Character、Integer也许还包括一些你自定义的类。下面是一个有关Number的回收工厂的例子。如果我有选择权的话,我想调用Number.make,但是很显然我没法向Number类添加方法,所以我只能用别的方法了:
public Number numberFactory(String str) throws NumberFormatException {
try { long l = Long.parseLong(str); if (l >= 0 && l < cachedLongs.length) { int i = (int)l; if (cachedLongs != null) return cachedLongs; else return cachedLongs = new Long(str); } else { return new Long(l); } } catch (NumberFormatException e) { double d = Double.parseDouble(str); return d == 0.0 ? ZERO : d == 1.0 ? ONE : new Double(d); } } private Long[] cachedLongs = new Long[100]; private Double ZERO = new Double(0.0); private Double ONE = new Double(1.0);
可以看出new的功能很有用,但是工厂的回收机制同样很有用。Java之所以仅支持new,是因为这是最简单最有效的方法,并且Java的宗旨也是尽量保持语言自身的简洁。但这并不意味着你自己的类库需要按照这一低标准来约束自己。(而且这并不意味着内置的库也需要这种约束条件,但是很可惜,他们还是这么做了。)
Q:我的代码会在创建对象或在GC开始之前时被杀掉吗?
假设应用程序不得不操纵许多3D几何点。很明显,依Java的风格来做就是去写一个Point类,内含3个double变量x、y、z坐标。但是,为大量点进行申请和回收的确会导致性能上的问题。而你可以自己建立资源池对存储进行管理。你可以在程序运行之初申请一大批Point对象,并将其存入数组中,而不是每次用到时才去申请。得到的数组(封装在一个类中)就像Point的工厂一样,但它是上下文感知的(socially-concious)回收工厂。调用pool.point(x,y,z) 时会返回数组中第一个未被使用的Point对象,将其3个变量设置为指定的值,并把它标记为已使用。而作为一个程序员来讲,当这些对象不再使用时,将它们放回资源池中便成了你的责任。
完成这点的方法有很多。如果你确定所申请的Point对象在使用一段时间之后会被丢弃的话,那最简单的方法就是这样做:利用int pos = pool.mark() 来标识当前资源池的位置。当你用完了之后,可以调用pool.restore(pos) 将原来位置的标志位重置。如果你想同时使用多个Point对象,那从不同的资源池里申请吧。资源池节省了垃圾回收时的开销(如果你有一个好的处理对象回收的模型)但是你仍然躲不开初始化对象时候的开销。你可以选择用“Fortran式”的方法来解决这个问题:用三个数组来存储x、y和z坐标,而不是用Point对象。你可以一个管理一批Point的类,而不必为单个点定义Point类。下面是一个资源池类的例子:
public class PointPool { /** Allocate a pool of n Points. **/ public PointPool(int n) { x = new double[n]; y = new double[n]; z = new double[n]; next = 0; } public double x[], y[], z[]; /** Initialize the next point, represented as in integer index. **/ int point(double x1, double y1, double z1) { x[next] = x1; y[next] = y1; z[next] = z1; return next++; } /** Initialize the next point, initilized to zeros. **/ int point() { return point(0.0, 0.0, 0.0); } /** Initialize the next point as a copy of a point in some pool. **/ int point(PointPool pool, int p) { return point(pool.x[p], pool.y[p], pool.z[p]); } public int next; }
你可以这样使用它:
PointPool pool = new PointPool(1000000); PointPool results = new PointPool(100); ... int pos = pool.next; doComplexCalculation(...); pool.next = pos; ... void doComplexCalculation(...) { ... int p1 = pool.point(x, y, z); int p2 = pool.point(p, q, r); double diff = pool.x[p1] - pool.x[p2]; ... int p_final = results.point(pool,p1); ... }
用PointPool 的方法申请100万个点花了半秒钟,而用Point类直接申请100万个点的方法需要6秒钟,所以相当于提速了12倍。
把p1,p2和p_final直接当做Point来声明远比当做int来声明好的多吧?在C/C++中,你可以用typedef int Point命令,但是Java不允许这样做。如果你想冒险一下,可以自己设置一下makefile,让文件在Java编译器运行之前先过一遍C语言的预处理器,然后你就可以这样写了:#define Point int.
Q:我在循环中有一个复杂的表达式。为了保证效率,我想让这个计算仅做一次。但是为了可读性,我想让它留在循环里被调用的地方。我该怎么办?
我们假设有这样一个例子,match是一个正则表达式的模式匹配函数,compile将一个字符串编译成一个有限状态机以供match调用:
for(;;) { ... String str = ... match(str, compile("a*b*c*")); ... }
由于Java没有宏定义,随着时间的推移,你也许会需要一些控制,但你的选择很有限。其中一种可行的选择是,使用带有变量初始化的内部接口,这虽然不优雅但是是一种可行的方法。
for(;;) { ... String str = ... interface P1 {FSA f = compile("a*b*c*);} match(str, P1.f); ... }
P1.f会在第一次使用P1时进行初始化,并且不会再改变,因为接口中的变量是隐式的static final的。如果你不想这么做,那可以换一种可以提供更多控制选择的语言。在Common Lisp中,字符序列#.表示其紧随在后的表达式会在读(编译)时计算,而不是在运行时。所以你可以这样写:
(loop ... (match str #.(compile "a*b*c*")) ...)
|