一个问题如果被回答地很少,有可能是因为知道答案的人很少,亦或是因为问题本身模糊不清、微不足道(但对你来讲可能很关键)。我似乎发明了一个术语,但是它在一个信息量很大的叫做 About.com Urban legends 网站里也被提到了。Java相关的常见问题非常之多,但接下来我要讲的是Java不常问到的问题(不常见问题列表就没那么多了,其中包括了一些对 C语言 的冷嘲热讽。)
嗯,大部分时候是的。但也存在一些特例,比如:不管choice的值是什么,下面代码finally中的语句就不会被执行。
try { if (choice) while (true) ; else System.exit(1); } finally { code.to.cleanup(); }
不。有时候对象x可以是一个c的子类c1,要么c1.m()这个方法不存在,要么x中某些方法调用了super.m()。无论上述那种情况,this.getClass()都会返回c1,而不是C.m()中的c。不过如果C是被final修饰的,那每次都会返回c是成立的。
Q:我自定义了一个equals方法,但是Hashtable忽略了它,为什么?
想要完全理解equals函数实际上是很难的。首先看下面几方面:
1、你定义了一个错误的equals方法。比如你这样写:
public class C { public boolean equals(C that) { return id(this) == id(that); } }
但为了让table.get(c)能正常工作,你需要为equals方法设置一个Object类型参数,而不是C类型的参数:
public class C { public boolean equals(Object that) { return (that instanceof C) && id(this) == id((C)that); } }
为什么?Hashtable.get方法大概 长 这样:
public class Hashtable { public Object get(Object key) { Object entry; //... if (entry.equals(key)) //... } }
现在 ,entry.equals(key) 触发的方法取决于实际运行时的对象引用entry,以及声明的编译时变量key的类型。所以,当你调用table.get(new C(…))时,this会在C类中寻找参数为Object的equals方法。如果恰巧你有一个参数定义为为C的equals方法,那并没有任何关系。它会忽略,并继续寻找函数签名为equals(Object)的函数,最终找到equals(Object)。如果你想重写一个方法,你需要将它们的参数类型 也匹配上。有些情况下,你可能想要两种方法,这样可以在类型已知的情况下避免由类型转换带来的额外开销:
public class C { public boolean equals(Object that) { return (this == that) || ((that instanceof C) && this.equals((C)that)); } public boolean equals(C that) { return id(this) == id(that); // Or whatever is appropriate for class C } }
2、 你实现的equals方法并不是绝对等价的:equals方法必须是对称的、传递的和自反的。对称性是指a.equals(b)的值必须与b.equals(a)一致。(大多数人会把这一点搞混。)传递性是指如果a.equals(b)为真且b.equals(c)也为真 , 那么a.equals(c)必须为真。自反性是指a.equals(a)必须为真,并且这也是为什么要有上述(this == that)这个条件测试(这是比较好的做法,因为这会提高效率: 利用==测试要比跟踪一个对象进行测试快很多 ,并且一定程度上屏蔽了循环指针链的递归问题)。
3、你 忘记了hashCode方法。任何时候你定义了一个equals方法,那么就应该同时定义一个hashCode方法。你必须保证两个相等的对象有着同样的hashCode,并且如果你想追求更好的hashtable性能,应该尝试着把最不 相等 的对象设置成不同的hashCodes。 一些类将hashCodes进行了缓存, 所以它们仅被计算一次。如果是这样的话,你在equals方法中加一句if (this.hashSlot != that.hashSlot) return false , 会节省不少时间。
4、你 没有处理好继承。首先,考虑到如果来自两个不同类的对象可以相等的话。在你说“不!一定不会!”之前,想想下面这种情况:一个Rectangle类中有width和height两个字段,另一个Box类除了上述两个字段外还有一个depth字段。那么,如果depth==0,这时的Box是否与Rectangle等价呢?你也许会赞成这个观点。如果你所处理的类不是被final修饰的,那么它有可能 成为其它类 的父类, 此时作为一个良民,你会想要善待你的子类 。特别的情况下,你可能想允许C类的子类利用super调用C.equals(),就像这样:
public class C2 extends C { int newField = 0; public boolean equals(Object that) { if (this == that) return true; else if (!(that instanceof C2)) return false; else return this.newField == ((C2)that).newField && super.equals(that); } }
为了能实现上述功能,你需要在C.euqals的定义中对类谨慎地处理。例如,检查类型时用that instanceof C而不是that.getClass() == C.class。具体原因参看前面IAQ。 如果你确定两个对象的父类一样的时候是相等的 ,那就可以使用this.getClass() == that.getClass() 。
5、你没 有处理好循环引用,比如像这样:
public class LinkedList { Object contents; LinkedList next = null; public boolean equals(Object that) { return (this == that) || ((that instanceof LinkedList) && this.equals((LinkedList)that)); } public boolean equals(LinkedList that) { // Buggy! return Util.equals(this.contents, that.contents) && Util.equals(this.next, that.next); } }
这里我假设有一个Util类可以做如下工作:
public static boolean equals(Object x, Object y) { return (x == y) || (x != null && x.equals(y)); }
我想把这个方法放到Object内部; 如果没有它,那你不得不在测试的时候抛出null指针的异常 。总之,LinkedList.equals 这个方法如果用来检测两个循环引用的链表,那它永远不会返回 (链表中一个元素指向另一个元素) 。至于如何在线性时间内,仅使用两个字的额外存储空间完成这件事,请参看Common Lisp的list-length函数的描述。(我怕你们想自己搞清楚它,所以这里就不剧透答案了。)
下面是针对上述问题的 一段 简化后的代码示例:
/** A version of Hashtable that lets you do * table.put("dog", "canine");, and then have * table.get("dogs") return "canine". **/ public class HashtableWithPlurals extends Hashtable { /** Make the table map both key and key + "s" to value. **/ public Object put(Object key, Object value) { super.put(key + "s", value); return super.put(key, value); } }
你需要在调用super的时候非常小心,并且一定要清楚super的方法究竟会做什么。在这个例子中,Hashtable.put 的职责是将key和value的映射关系记录到表中。然而,如果hashtable太满了,那Hashtable.put 会为表分配一个更大的数组,将所有旧的对象拷贝过去,然后再 次递归调 用table.put(key, value)。因为Java是根据目标运行时的类别解析方法的,在这个例子中,代码中Hashtable递归调用将会调用HashtableWithPlurals.put(key, valu e)。 最终的结果就是:有时候(当table的容量在错误的时间溢出时),你在得到“dogs”和“dog”的同时,也得到一个“dogss”。任何文档提到过put递归调用这种现象有时会发生么?没有。在这种情况下,查看JDK的源码是非常有帮助的。
你不应该对Properties对象调用get方法;而应该调用getProperty方法。许多人认为二者的区别是getProperty声明了返回值为String类型,而get声明的返回值类型为Object。但实际上二者之间有更大的区别:getProperty会查看默认值。get是继承自Hashtable的方法,它会忽视默认值,所以get的职责就像Hashtable文档中描述的一样,但是这种方式可能会跟你想象中的不一样。 其它继承 自Hashtable的方法也会忽略默认值(如isEmpty和toString方法),举个例子:
Properties defaults = new Properties(); defaults.put("color", "black"); Properties props = new Properties(defaults); System.out.println(props.get("color") + ", " + props.getProperty(color)); // This prints "null, black"
这点在文档中有描述么?可能吧。 Hashtable的文档中提到了table的实体 ,同时提到了如果你认为默认值不是表中实体的话,那么Properties的行为是与Hashtable一致的。如果出于某些原因,你认为默认值是表中的实体(正如你会以为能得到与getProperty一样的效果)那你就晕了。
Q : 继 承看起来很容易出错。有什么办法能防止犯错么?
前两个问题都表示出了一个观点,那就是程序员需要在继承类的时候特别小心, 并且在使用 其它 类的子类时也同样要小心 。上述两个问题让John Outsterhout发表了如下言论“实现继承导致了代码之间纠缠不清,变得更为脆弱,这正如goto语句被滥用时发现的问题一样。最终,这导致面向对象系统经常饱受复杂度和缺乏代码重用的困扰” ( Scripting , IEEE Computer, March 1998)。与此同时, 据说 Edsger Dijkstra说过“ 面向对象编程有时也并不尽如人意,这极有可能起源于加利福尼亚 ”(来自一些签名的文件)。
我认为没有可以保证一定安全的方法,但是下面是一些可以加以考虑的事情:
Q : 除了继承,还有 其它 类似的做法 吗 ?
委托是继承的一种替代品。委托的意思就是可以将 其它 类的实例以实例变量的方式添加到一个类中,并将参数传递给这个实例变量。通常来讲,这要比继承更加安全,因为由于实例变量是一个已知类,而不是一个新类,所以这么做的话会迫使你深思熟虑每次要传递的参数 。 与此同时,这么做也不会强制你接受父类的所有方法:你可以仅使用其中一些需要的方法。另一方面,这会使你写更多的代码,也就导致了其很难复用(因为它不是一个子类)。
在HashtableWithPlurals例子中,利用代理的方式可以这样写 ( 注意:在JDK1.2版本中,Dictionary是不推荐使用的;可以使用Map替代 ):
/** A version of Hashtable that lets you do * table.put("dog", "canine");, and then have * table.get("dogs") return "canine". **/ public class HashtableWithPlurals extends Dictionary { Hashtable table = new Hashtable(); /** Make the table map both key and key + "s" to value. **/ public Object put(Object key, Object value) { table.put(key + "s", value); return table.put(key, value); } //... Need to implement other methods as well }
在Properties例子中,如果你想强调默认值是实体这种解释的话,那最好使用代理。 为什么Properties还用继承处理呢?因为Java的实现团队追求简洁的代码 ,而 且他们太匆忙了。
由于一些原因,并不推荐大家使用全局变量:
出于上述原因,Java决定废弃全局变量。
那要看你想做什么了。无论哪种情况,你都需要确定以下两件事:认清这个所谓的全局变量一共需要多少个副本?以及放在哪里比较合适?以下是一些常见的解决方案:
如果你真的只是想在用户首次启动JVM的时候,在程序中保留一个副本的话,那你也许可以使用一个静态实例变量。比如,在你的应用中有一个叫做MainWindow的类,并且你想记录下用户打开窗口的数量,并在用户关闭最后一个窗口时初始化“真要退出吗?”这个对话框。如此一来,你可以这样做:
// One variable per class (per JVM) public Class MainWindow { static int numWindows = 0; ... // when opening: MainWindow.numWindows++; // when closing: MainWindow.numWindows--; }
大多数情况下,你需要的只是一个 类的实例变量 。比如,假设你在写一个网页浏览器并且想将访问历史记录当做全局变量 , 那么在Java中,如果将其设置成一个Browser类内的实例变量会更好。这样的话,用户完全可以在同一个JVM中同时运行两个浏览器,之间也不会相互影响。
// One variable per instance public class Browser { HistoryList history = new HistoryList(); ... // Make entries in this.history }
现在 , 假设你完成了浏览器的大部分设计与实现,这时候你发现想要在Http类内的Cookies类里面打印出一些错误信息 , 但是不知道在哪里展示这些信息。你可以简单 地 在Browser类中添加一个实例变量,用它来记录待输出的流或帧, 但目前你还没有将当前的Browser对象中的实例传递给Cookies类的方法 。你并不希望在传递Browser对象的时候修改大部分的函数签名。你也不能用一个静态变量来解决,因为可能有多个Browser对象同时运行。然而,如果你可以保证每个线程中只有一个Browser对象(尽管每个Browser对象可能会有多个线程),那么有一种比较好的解决方法:在Browser类中存储一个静态表,保存线程与Browser对象之间的映射关系,然后根据当前所在线程查找正确的Browser对象(这里就是要找到待显示错误信息的Browser对象)。
// One "variable" per thread public class Browser { static Hashtable browsers = new Hashtable(); public Browser() { // Constructor browsers.put(Thread.currentThread(), this); } ... public void reportError(String message) { Thread t = Thread.currentThread(); ((Browser)Browser.browsers.get(t)) .show(message) } }
最后,如果你想要一个全局变量在JVM期间一直存在 ,亦或是想让其在多个JVM之间通过网络互相共享。那么你大概需要一个通过JDBC访问的数据库,或者将数据序列化,然后将它存成文件的形式。
长话短说:Java1.5之前的版本不可以。Java1.5之后的版本可以通过引用static imports实现;你现在可以这样写:import static java.lang.Math.*然后直接可以调用sin(x)。但是要注意来自Sun的警告“:你什么时候应该使用静态导入?一定要谨慎!”
下面是一些针对Java1.5之前版本的解决方案:
如果你只是想用Math中的一小部分方法,那你可以将 它们 封装到你自己的类中: | public static double sin(double x) { return Math.sin(x); } public static double cos(double x) { return Math.cos(x); } //... sin(x) |
静态方法需要一个目标(就是点符号左面的东西),这个目标要么是一个类的名字,要么是一个与具体取值无关的某个类的对象,但一定要声明成正确的类。所以你可以为每次调用少敲三个字符,就像这样: | // Can't instantiate Math, so it must be null. Math m = null; //... m.sin(x) |
java.lang.Math是被final修饰的类,所以不能被继承,但如果你有一些自己的静态函数,并且想在自己的各个类之间互相共享使用的话,那你可以把 它们 打包起来,然后再使用的时候继承 它们 : | public abstract class MyStaticMethods { public static double mysin(double x) { //... } } public class MyClass1 extends MyStaticMethods { //... mysin(x) } |
Just Java的作者 Peter van der Linden 在他的FAQ中反对上述最后两种做法。大多数情况下,我也认为Math m = null 是一种糟糕的做法,但我不认同MyStaticMethods 的例子是一种“为了使用可有可无的缩写( 不如直接用类别层级的方式进行表示 )而导致缺乏面向对象风格的继承做法”。首先,说缩写不重要是一种旁观者的想法;缩写可能是极其重要的(参看 这个例子 来了解我是如何利用这种做法来达到理想效果的)。其次,倒不如他自以为是的说这是一种糟糕的面向对象风格。对于Java来讲,你可以说这事一种糟糕的风格,但是对于具有多继承机制的语言来讲,我 的 这种用法更容易被接受。
另一种考虑这个问题的点是:Java的某些特性(对每个语言来讲)会有一些无可避免的权衡,并且其中还混杂着各种问题。我同意MyClass1继承MyStaticMethods这种做法会误导用户以为MyClass1继承了一些来自MyStaticMethods的方法,并且我也赞同这样做会无法继承真正需要的类,这也是不好的。但对Java而言,类一般是封装和编译(大部分时候)和一些命名空间的单元。MyStaticMethods这种方法在继承机制面前有负面效果,但是在命名空间这方面有正面作用。如果你认为继承更重要,那我不会与你争论了。但你真的认为一个类同时做多件事要比只做一件事好吗?你真的认为风格的规定一定比权衡更重要吗?
当然不是。我 这里的否定 是指null instanceof Object 会返回false。
下面是一些你需要了解的与null相关的事情:
1、 你不能对null调用方法:当x是null且m是非静态方法时,调用x.m()是错误的。(当m时静态方法时候是合法的,但那是跟x的类相关,与x这个对象本身的值并无关系。)
2、null 只有一个,并不是每个类都有一个 自己的 null。例如,((String) null == (Hashtable) null)这样会返回true。
3、 可以将null当做参数传给一个方法,前提是这个方法支持这种做法。要注意的是,有些方法支持这样做,有些方法不支持。比如,System.out.println(null)这样写没有问题,但是string.compareTo(null)这样就不行了。所以除非参数本身是显而易见的,否则你写方法的时候应该在javadoc里说明null这种参数是否是合法的。
4、 JDK1.1到1.1.5版本中,将null当做参数直接传给匿名内部类的构造函数 ( 如new SomeClass(null){…})会导致编译错误。但传入一个结果是null的表达式是没有问题的,或者传入强制类型转换的null也可以,如new SomeClass( (String)null){…} ) 。
5、 Null通常来讲至少有三种不同的含义:
// null means not applicable // There is no empty tree. class Node { Object data; Node left, right; void print() { if (left != null) left.print(); System.out.println(data); if (right != null) right.print(); } } | // null means empty tree // Note static, non-static methods class Node { Object data; Node left, right; void static print(Node node) { if (node != null) node.print(); } void print() { print(left); System.out.println(data); print(right); } } | // Separate class for Empty // null is never used interface Node { void print(); } class DataNode implements Node{ Object data; Node left, right; void print() { left.print(); System.out.println(data); right.print(); } } class EmptyNode implements Node { void print() { } } |
C语言有sizeof运算符,这是必须要有的,因为用户需要管理malloc的调用,同时也是因为一些原生的类型(如long)的大小并没有
统一的标准。Java并不需要sizeof,但是如果有这个运算符的话当然会方便很多。如果想在Java里得到类似sizeof的效果,你可以这样做:
static Runtime runtime = Runtime.getRuntime(); ... long start, end; Object obj; runtime.gc(); start = runtime.freememory(); obj = new Object(); // Or whatever you want to look at end = runtime.freememory(); System.out.println("That took " + (start-end) + " bytes.");
这个方法并不总是奏效, 因为垃圾回收可能发生在你代码正在进行检测的时候,那样就会丢掉字节的计数 。并且,如果你使用的 是J IT类的编译器,那么生成代码也会产生一些额外的字节。
在Sun 的JDK VM中,你也许会感到很吃惊,一个Object会占用16字节,或是4字大小。其中的内容是这样的:头信息占用了两个字大小,一个字指向了对象所属的类,另一个字指向了实例的变量。 即使Object没有实例变量,Java也会为其分配一字大小的空间。 最后,还有一个“handle”,这是一个指向两字大小的头信息的指针。Sun声称这一额外的间接层使垃圾回收过程变得更为简单。(而近15年以来,高性能Lisp和Smalltalk两种语言却不使用间接层的垃圾回收器。我也听说微软的JVM并没有这种额外的间接层, 这点 尚未被我证实。)
一个空的new String()占用40字节,或是10字: 3个字来存储头信息 ,3个字来存储实例变量(开始索引 、 结束索引以及字符数组),和4个字来存储空的字符数组。从一个已有的字符串建立一个字串仅需6个字的空间,因为字符数组是共享的。将Integer类型的key/value键值对存入Hashtable需要64字节(这其中包含了预先给Hashtable中的数组所分配的4字节): 我会让你明白 这是 为什么。
在一个类中,实例变量的初始化代码可以出现在3个地方:
在类(或父类)的实例变量初始化器中。 class C { String var = "val";} | |
在类(或父类)的构造函数中。 public C() { var = "val"; } | |
在 类的 初始化代码块中 。这是Java1.1中新加入的功能;这类似于静态初始化代码块,但是不用static关键字修饰。 { var = "val"; } |
当你写下new C()时,初始化的顺序是这样的(不考虑内存不够的情况):
1、调用C父类的构造函数(除非C是Object这个类,因为 Object没有父类)。大多数情况都会调用无参数构造函数,除非程序员在构造函数最开始的时候显式地写下了super(…)。
2、 一旦父类的构造函数返回了, 接下来实例变量初始化器和对象初始化器会按照文字顺序(从左到右)执行。不要被javadoc和javap用字母顺序所迷惑,在这里并不重要。
3、现在会执行构造函数中余下的 代码。这里可以设置实例变量,或者做任何 其它 事情。
实际上你对上述三种初始化方式有很大的自主选择权。我推荐的是使用实例变量初始化器,这样一来,如果这个变量的值与所用的构造函数无关,则可以不必为每个构造函数都写一遍初始化代码了。仅在初始化情况非常复杂(比如
,这样可以避免在多个构造函数中重复初始化同样的东西。剩下的就可以让构造函数去完成了。
下面是一个例子:
Program: class A { String a1 = ABC.echo(" 1: a1"); String a2 = ABC.echo(" 2: a2"); public A() {ABC.echo(" 3: A()");} } class B extends A { String b1 = ABC.echo(" 4: b1"); String b2; public B() { ABC.echo(" 5: B()"); b1 = ABC.echo(" 6: b1 reset"); a2 = ABC.echo(" 7: a2 reset"); } } class C extends B { String c1; { c1 = ABC.echo(" 8: c1"); } String c2; String c3 = ABC.echo(" 9: c3"); public C() { ABC.echo("10: C()"); c2 = ABC.echo("11: c2"); b2 = ABC.echo("12: b2"); } } public class ABC { static String echo(String arg) { System.out.println(arg); return arg; } public static void main(String[] args) { new C(); } } |
1: a1
2: a2
3: b1
4: B()
5: b1 reset
6: a2 reset
7: c1
8: c3
9: C()
10: c2
11: b2
从实例创建中区分出类的初始化是很重要的一点。实例在你利用new来调用构造函数时被创建。一个类C,是在第一次被激活使用的时候初始化的。在这个过程中,这个类的初始化代码会以 文本顺序 运行。一共有两种类初始化代码:静态初始化代码块(static {…})和类的变量初始化(static String var = …)。
以下是对激活使用 (active use) 的一些定义,当你第一次进行如下任何一种操作时,就出发了激活使用这个条件:
1、通过调用构造函数创建了一个C的实例。
2、调用了C中定义的的静态方法(不是继承来的)。
3、对C中定义的静态变量(不是继承来的)进行读写。如果静态变量是被常量表达式(比如一些只用到了原始操作符的表达式(如+或者||)、常量以及被static final所修饰的变量)那么不会算数,因为这些是在编译的时 候被初始化的。
下面是一个例子:
Program:class A { static String a1 = ABC.echo(" 1: a1"); static String a2 = ABC.echo(" 2: a2"); } class B extends A { static String b1 = ABC.echo(" 3: b1"); static String b2; static { ABC.echo(" 4: B()"); b1 = ABC.echo(" 5: b1 reset"); a2 = ABC.echo(" 6: a2 reset"); } } class C extends B { static String c1; static { c1 = ABC.echo(" 7: c1"); } static String c2; static String c3 = ABC.echo(" 8: c3"); static { ABC.echo(" 9: C()"); c2 = ABC.echo("10: c2"); b2 = ABC.echo("11: b2"); } } public class ABC { static String echo(String arg) { System.out.println(arg); return arg; } public static void main(String[] args) { new C(); } } |
1: a1
2: a2
3: b1
4: B()
5: b1 reset
6: a2 reset
7: c1
8: c3
9: C()
10: c2
11: b2
你当然不需要写(2 6 )个构造函数。假设你有一个类叫C,它的定义如下:
public class C { int a,b,c,d,e,f; }
你可以为构造函数做如下几件事:
1、 对极有可能需要的几种变量组合进行猜测,并且为之提供构造函数。赞成的观点认为:这是惯用的做法。反对的观点认为:很难完全猜对;会产生大量冗余代码。
2、 定义可串联的setter方法,因为它们会返回this。如此一来,为每个实例变量定义一个setter,然后调用默认构造函数之后调用 它们 :
public C setA(int val) { a = val; return this; } ... new C().setA(1).setC(3).setE(5);
赞成:这是一种相当简洁且高效的方法。一些类似的观点在Bjarne Stroustrop的 The Design and Evolution of C++ 一书中第156页被讨论过了。 反对 :你需要实现所有的setter,这并不遵从JavaBean规则(因为它们返回this而不是void),并且如果两个值之间需要交互的话 , 那这种方法也不适用了。
new C() {{ a = 1; c = 3; e = 5; }}
赞成:十分简洁,没有使用setter那么凌乱; 反对 :实例变量不能是私有的,处理子类需要额外的间接成本,而这个对象可能根本就不是C这个类(虽然它是C的一个实例) 。这仅在你对实例变量有访问权限的时候才 管用,然而包括经验丰富的Java程序员在内的大多数人都不会明白。
实际上很简单:定义一个新的没有命名的(匿名的)C的子类,而这个子类没有新添任何方法或变量,但初始化代码块初始化了a 、 c和e。如此定义这个类的话,你就相当于在创建一个实例。当我把这展示给Guy Steele看得时候,他说“哈哈!这太酷了,好吧,但我可能不会提倡这么做 …… ”。和平时一样,Guy是对的(对了,你还可以用这种方法创建并初始化向量。你要知道能如此创建并初始化是非常给力的一件事儿,想想看,new String[] {“one”, “two”, “three”}就可以初始化一个String数组了。 曾经你以为必须用赋值语句对vector进行初始化的工作,现在也可以用类似的方法解决了new Vector(3) {{add(“one”); add(“two”); add(“three”)}} )。
4、 你可以换一种支持选择性初始化部分变量的语言。比如,C++就支持默认参数。所以你可以这么写:
class C { public: C(int a=1, int b=2, int c=3, int d=4, int e=5); } ... new C(10); // Construct an instance with defaults for b,c,d,e
Common Lisp和Python都有关键字参数,也支持默认参数,所以你可以这么写:
C(a=10, c=30, e=50) # Construct an instance; use defaults for b and d.
Q : 我 该 何时调用构造函数,何时调用 其它 方 法呢?
最直观的回答就是 , 在你想new一个对象的时候调用构造函数;这是new这个关键字的用途。而我的回答是:构造函数往往被滥用了,调用它们和它们所做的工作两方面都被滥用了。下面是一些需要考虑的问题:
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[i] != null) return cachedLongs[i]; else return cachedLongs[i] = 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的宗旨也是尽量保持语言自身的简洁。但这并不意味着你自己的类库需要按照这一低标准来约束自己。(而且这并不意味着内置的库也需要这种约束条件,但是很可惜,他们还是这么做了。)
假设应用程序不得不操纵许多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.
我们假设有这样一个例子,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*")) ...)
我该从何说起?下面是一些最该知道的东西。我在一个循环里写了一个计时功能,用来报告每秒钟千次迭代速度(K/sec)和每次迭代所需微秒数(uSecs)。整个测试在Sparc 20上完成,JDK版本为1.1.4,编译器为JIT。随后我注意到了如下信息:
K/sec uSecs Code Operation
========= ======= ==================== ===========
147,058 0.007 a = a & 0×100; get element of int bits
314 3.180 bitset.get(3); get element of Bitset
20,000 0.050 obj = objs[1]; get element of Array
5,263 0.190 str.charAt(5); get element of String
361 2.770 buf.charAt(5); get element of StringBuffer
337 2.960 objs2.elementAt(1); get element of Vector
241 4.140 hash.get(“a”); get element of Hashtable
336 2.970 bitset.set(3); set element of Bitset
5,555 0.180 objs[1] = obj; set element of Array
355 2.810 buf.setCharAt(5,’ ‘) set element of StringBuffer
308 3.240 objs2.setElementAt(1 set element of Vector
237 4.210 hash.put(“a”, obj); set element of Hashtable
Java相关的书籍有很多,大概可以分为三个等级:
糟糕的。 大部分Java书籍都是由那些找不到Java相关工作的人写出来的(因为编程几乎总是比出书更挣钱;我都干过,所以我敢这么说)。这种书漏洞百出,会有不好的建议以及糟糕的程序。这些书对于初学者来说很危险,但是对 其它 语言稍有经验的编程人员是可以很容易认出并拒绝的。
非常好的。 Java类的好书数量并不多。我更偏向于 官方说明文档 和 Arnlod和Gosling 、 Marty Hall 及 Peter van der Linden 这些作者写的书。作为参考,我喜欢 Java in a Nutshell 系列书籍,以及 Sun 的在线参考(我将 javadoc API 和 语言规范 以及它们的 变更都拷到了本地硬盘中 。此外我将它们添加到了我浏览器的书签里,这样我总是可以快速访问它们 了。)
不确定的。在上述两种极端之间存在着一些中等质量的书籍,一般这些书是由不够了解Java的人写出来的 。 他们要么没花时间研究Java究竟是如何工作的,或者是想快速出版太过仓促。 举一个例子,来自Rise and Resurrection of the American Programmer的 Edward Yourdon的Java and the new Internet programming paradigm 一书。下面是Yourdon眼中与众不同的Java:
int[] a = {0, 1, 2}; int[] b = a; b[0] = 99;
那a[0]也会变成99,因为a和b都是同一个对象的指针(或引用)。
原文链接: norvig 翻译:ImportNew.com -exlsunshine
译文链接:[]