有关Java的不经常被问到的问题
作者 Peter Norvig
问题:什么是不经常被问到的问题?
一个不经常被问到的问题,有可能是因为只有很少的人知道答案或者因为这个问题比较含糊(但是有时这个问题对你解决问题很重要)。我认为是我先发明这个词的,但是它在信息量很大的一个网站上也出现过(About.com Urban Legends)。这里有很多Java FAQs(有关Java的经常被问到的问题),但是只有一个Java IAQ(有关Java的不经常被问到的问题,以后用 Java IAQ来代替)。(在网上只存在很少的IAQ列表,比如包含对C有讽刺意味的列表)
问题:finally中的语句永远会被执行到,对不对?
好吧,通常来说是的。但是下面例子中finally中的语句永远不会被执行,不管布尔变量choice是true还是false:
try { if (choice) { while (true) ; } else { System.exit(1); } } finally { code.to.cleanup(); } |
问题:在类C中的方法m中, this.getClass()是不是总是返回C?
No. It's possible that for some object x that is an instance of some subclass C1 of C either there is no C1.m() method, or some method on x called super.m(). In either case, this.getClass() is C1, not C within the body of C.m(). If C is final, then you're ok.
不是的。如果有一个类C1是C的子类,在C1中没有m方法,或者在C1中调用了super.m()。这两种情况下this.getClass()返回的是C1。 但是如果C是final的,则返回C
问题:我定义了equals方法,但是Hashtable忽略了它.为什么?
equals方法很容易被用错。如果你发现equals方法有问题,请看看下面的一些地方是不是有问题:
public class C { public boolean equals(C that) { return id(this) == id(that); } } |
但是要让table.get(c)工作的话,你需要将C中的equals方法的参数改成Object。
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)会调用equals方法,根据entry的运行时类型和key的编译时类型。所以当用户调用table.get(new C(...))时,看起来就像在类C中调用了参数是Object的equals方法。如果你将equals的参数定义成C,这是不对的。系统会忽略这个方法,并寻找方法签名是equals(Object)的方法,最后它会找打Object.equals(Object)。如果你想重载这个方法,你的方法签名必须和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 } } |
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); }
} |
如果想让super.euqals工作的话,你必须仔细的定义和实现在C中的equals方法。例如,使用instanceof C,而不是that.getClass() == C.class。只有在你很确认连个对象是同一个类时才使用that.getClass() == C.class。
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); }
} |
这里我i假设Util方法是:
public static boolean equals(Object x, Object y) { return (x == y) || (x != null && x.equals(y)); } |
我希望这个方法传入的是Object,如果不是的话,你可以在测试的时候抛出null。然而,LinkedList.equals方法不会返回,如果参与比较的连个LinkedList有循环引用(一个LinkedList的中元素指向另外一个元素)。可以参见Common Lisp函数的list-length,是如何使用两个word的额外存储空间来解决这个问题的。(我之所以不给出答案,是想让你自己先想一想)
Q:为什么有时调用父类的方法会失败?
例如:
/** 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); } } |
当你在调用父类的方法的时候,你必须对父类非常了解。这这个例子中,Hashtable.put的约定是:它会维护key和value的关系。但是,如果hashtable快满的时候,Hashtable.put方法将会分配一个更多的数组,将原来的copy到新数组中去,然后重新递归调用table.put(key, value)方法。
因为Java是根据运行时类型来解析方法的,在我们的例子中Hashtable的递归调用会调用HashtableWithPlurals.put(key,value),最后的结果是偶然的(当在错误的时间hashtable上溢时),你将得到”dogss”,”dogs”,”dog”指向同一个value。是不是所有的的put都存在这样的情况的?不是,这时你需要查看JDK的code了。
注:可以在调用put之前,将s去掉或者使用封装类,而不是继承已有的类
http://stackoverflow.com/questions/6323923/i-tried-to-forward-a-method-to-super-but-it-occasionally-doesnt-work-why
问题:当我调用get时,为什么我的Properties对象忽略了默认值?
你不应该调用get方法,应该使用getProperty方法。很多人假设getProperty方法只是返回一个String类型,get方法返回Object类型。但是实际上,两者之间区别很多:getProperty会使用default,get方法是从Hashtable继承来的,它忽略了default。get方法的具体行为已经在Hashtable的文档中说明了,但是可能不是你想要的。其它从Hashtable继承而来的方法也会忽略default(例如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" |
这中现象在文档中说明了吗?或许说了。所以不要盲目的相信文档。
问题:继承很容易出错,我应该如果避免?
前面两个问题告诉程序员在继承一个类,或者只是使用继承的类时,需要格外仔细。因为这样的问题,所以JohnOusterhout说:“继承会导致缠绕不清和程序的脆弱性,就行goto的滥用一样。”(Scripting, IEEE Computer,March 1998)EdsgerDijkstra说:“面向对象编程很可能是一个坏的想法如果它只是起源于加利福尼亚”。(参见:http://programmers.stackexchange.com/questions/99166/dijkstra-quote-on-object-orientation)我认为这里没有通用的规则去遵守,但是有一些点需要注意一下:
问题:对于继承我们还有别的什么方法来代替吗?
代理是一种。代理意味这你会维持对另外一个类的引用,然后调用这个类。通常代理比继承更安全,因为它强制你考虑每次调用之前的参数。因为是一个引用,它不强迫你接受父类的所以方法,这样你只需要提供对你自己有意义的方法就可以了。另一方面,它使你写更多的代码,并且很难重用(因为它不是子类)。
例如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 } |
The Properties example, if you wanted to enforce the interpretation thatdefault values are entries, would be better done with delegation. Why was itdone with inheritance, then? Because the Java implementation team was rushed,and took the course that required writing less code.
在Properties的例子中,如果你想强制解释default为一个entries时,使用代理会比较好。但是为什么它是用继承实现的呢?因为实现Java的团队很匆忙,他们想写更少的代码。
问题:为什么Java中没有全局变量
因为一些原因全局变量被认为是一种糟糕的形式:
所以,Java决定不是用全局变量。
问题:但是我还是希望有全局变量,我有什么替代方法吗?
这取决于你想要干什么。这时你需要考虑两件事情:管用这个变量,我需要多少个copy ?我把它放到哪里比较合适?下面是一些通用的解决方法:
如果你想在一个虚拟机中只有一个copy的话,你可以使用static 变量。例如,你有一个MainWindow类,你需要计算用户打开了多少个windows,当用户准备关闭最后一个windows时,给用户一个提示,你真的想推出吗?这时,你可以: |
// One variable per class (per JVM) public Class MainWindow { static int numWindows = 0; ... // when opening: MainWindow.numWindows++; // when closing: MainWindow.numWindows--; } |
在更多的时候,你需要实例变量。例如,你写了一个浏览器,希望将历史列表当作全局变量。在Java中,可以将历史列表作为实例变量。如果用户在同一个虚拟机上运行两个浏览器的话,两个历史记录不会相互冲突。 |
// One variable per instance public class Browser { HistoryList history = new HistoryList(); ... // Make entries in this.history } |
现在假设,你已经完成了设计,和浏览器的大部分的coding,这时你发现,Cookie类(在Http类中)你想显示一个错误信息。但是你不知道在哪里显示合适。你可能会在Browser类中添加一个变量来引用输出流,但是你没有将当前的Browser实例传给Cookie。你又不想改变很多方法的签名来将Browser传入。你不能使用static变量,因为可能会有多个浏览器在运行。然而,如果你能保证在一个线程中是有一个浏览器实例的话,这时有一个好的方法:将线程和浏览器的map放入Browser类中的static变量中,使用时只需要通过当前的线程获得浏览器的实例就可以了: |
// 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) } } |
最后,如果你想在两个不同的虚拟机之间进行交互的话,你可以使用数据库或者序列号 |
|
问题: 我可以使用sin(x)而不是Math.sin(x)
简单的说:在Java1.5之前,不行,之后可以。使用static imports。但是注意从Sun来的警告:“什么时候可以使用staticimport呢?尽量少使用”
下面是在Java1.5之前可以使用的方法:
如果你希望很少的方法,你可以将它们放入自己的类中: |
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) } |
Peter van der Linden, Just Java的作者,在他的FAQ不推荐使用后两者方法。 我承认Math m =null这种方式不好,但是第三种方法我觉得不是他说的“为了得到很小的名字的简写,而使用很烂的OOP方法”。首先,小不小的事情是在于每个人的看法,这种简写可能是很有意义的。(可以在我的这个例子中看到an example)。第二,说这种方式是很烂的是很不妥的。你可以说这种方法不好,但是在支多继承的语言中,这种方法是更易接受的。
从另外一个方面来看Java的这个特性,需要考虑取舍和平衡很多事情。我同意在这种情况下使用继承或误导用户以为MyClass1是从MyStaticMethod继承来的。阻止MyClass1去继承它真正需要继承的类是不合适的。在Java中一个类也是一个封装单元和命名空间。第三种方式和打乱继承关系,但是在命名空间上确大有益处。如果你说前者比较重要,我不和你争论,但是如果你认为一个类只做一件事情,而不是一次做多件是情,我就和和你争论了。如果你只是抓住形式,而不进行取舍。
问题: null是一个对象吗?
绝对不是。我的意思是nullinstanceof Object是false。还有一些你需要注意的:
// 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() { } } |
问题:一个对象有多大?为什么Java没有sizeof?
C有一个sizeof操作符,因为它需要,用户需要来自己使用malloc来管理内存。因为基本类型的size不是标准的。Java不需要sizeof,但是这里还是有方便的方法来计算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."); |
这种方法不是十分安全,因为GC可能在任何时间运行。同时,如果你使用了just-ni-time编译器,一些字节可能是从生成的代码中来的。
你可能会发现一个Object会占用16个字节,在Sun的JDK中。可以分解如下:这里有两个word的头,一个word指向Object class,另外一个指向实例。尽管Object没有实例变量,Java还是分配了一个word给实例变量。最后,这里有一个handle,它指向另外2word的头。Sun说这个额外的重定向使得GC更简单。(据我说知,在Lisp和Smalltalk的GC可以完全不需要这个额外的字节来完成GC已经15年了,我听说但是没有确认微软的JVM也没有这个额外的重定向)
An empty newString() takes 40 bytes, or 10 words: 3words of pointer overhead, 3 words for the instance variables (the start index,end index, and character array), and 4 words for the empty char array. Creatinga substring of an existing string takes "only" 6 words, because thechar array is shared. Putting an Integer key and Integer value into a Hashtabletakes 64 bytes (in addition to the four bytes that were pre-allocated in theHashtable array): I'll let you work out why.
一个空String占用40字节:3个word前面说过了,3个word给实例变量来用(开始的地方,结束的地方,字符串数组),4个word给空字符数组。创建一个已经存在的字符串的子串只需要6个word,因为字符数组是共享的。给Hashtable中放入Integer类型的key和value会需要64个字节(在Hashtable数组中会有4字节的额外空间占用):我会给你解释为什么。
注:这里为什么没有下文了呢?不知道
问题:初始化代码是按什么顺序来进行的?我应该将它们放到哪里?
实例变量可以在类的三个地方去初始化:
在定义实例变量的时候(在父类中) |
|
|
在构造函数中 |
|
|
在对象初始化块中。这是Java1.1的新特性。就行静态初始化块一样,只是没有static关键字 |
|
当你使用new C()所发生的过程是(不考虑内存溢出的情况):
通常,你可以任意选择哪个方式来初始化变量。我推荐:当一个变量的值是固定的可以使用第一种方式,当初始化很复杂时,使用第二种方式。剩下的使用第三种方式。
下面是一个例子:
Program:
|
Output:
|
Q:静态变量是如何初始化的?
区分静态变量初始化和实例化是很重要的。当你使用new来调用构造函数时实例会被创建。当第一次使用类C时,它会被初始化。在这时,对这个类的初始化方法会被调用,并且是基于文本的顺序的。这里有两种初始化代码:静态初始化块(static { ... }),类变量初始化(staticString var = ...)。
当是做下面的一些动作是会导致类变量被初始化:
例子:
Program:
|
Output:
|
问题:我有一个六个实例变量的类,每个都有可能被初始化,我是不是要写64个构造函数呢?
不,你不需要64(26)个构造函数. 让我们假设你有一个六个参数的类:
public class C { int a,b,c,d,e,f; } |
你可以这样做:
public C setA(int val) { a = val; return this; } ... new C().setA(1).setC(3).setE(5); |
优点:这是一个简单和合理的方法。类似的方法可以参见(BjarneStroustrop on page 156 ofThe Design and Evolution of C++.),缺点:你需要写所以变量的setter方法,这和JavaBean的规则是不兼容的,如果想同时设置两个变量,是不可能的。
new C() {{ a = 1; c = 3; e = 5; }} |
优点:很简单,没有多余的setter方法。缺点:这个类变量不能是private的,有一个额外的子类出现,你的对象没有真正拥有C(尽管你可以使用instanceof C)。很多人,包括有经验的Java程序员,也不理解这种写法。实际上,这很简单:你定义了一个新的,匿名的C的子类,这个子类中没有变量和方法,但是有一个初始化块来初始化a,c和e。当你定义类是,同时也创建了一个类实例。当我把这个方法告诉别人时,他说“heh, heh! 这是很cool,但是我不确定这种方法可以大面积使用”,他是对的。(但是,你可以用它来初始化vector,例如:new Vector(3) {{add("one"); add("two");add("three")}}.)
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中有keyword argument和可选参数一样:
C(a=10, c=30, e=50) # Construct an instance; use defaults for b and d. |
问题:什么时候我该使用构造函数,什么时候使用别的方法?
一个投机取巧的回答是,当时想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的哲学就是保持语言本身尽量简单。但是这不意味这你的类库必须减持这种方式。(本该标准类库也不应该只是使用new的)
问题: 我是不是要被创建对象和GC的开销所困扰?
假如有一个程序要处理很多3D的几何点。通常的做法是实现一个类Point,它有double类型的x,y,z坐标。但是分配和垃圾回收这些点的话,会导致性能问题。你可以通过建立自己的resource pool来缓解这种情况。而不是当你需要是再分配,而是在程序启动的时候,分配一大块的内存。这个内存可以当作是Point的工厂,但是实际上是一个可以循环使用的工厂。pool.point(x,y,z)方法调用会使用在数组中第一个没有使用的点,并且将设定它的坐标值,标记它已经被使用。所以作为一个程序员你的责任就是,当你不需要这个Point时,将它释放给资源池。这里有很多方法可以实现这种功能。最简单的方法是,当你希望分配一个Point,使用它时,这时你可以int pos = pool.mark()来标记在池中的位置,当你使用完成后,你可以pool.restore(pos)来将资源释放。如果有些Point你想保留下来,你只要从另外一个池中分配就可以了。资源池可以节省GC的花销(在资源释放是,你有一个很好的模型),但是不能节省对象创建的花销。你可以去看看Fortran,使用x,y,z三个数组,而不是一个单独的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; } |
You would use this class as follows:
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); ... } |
使用资源池分配一百万个点只需要0.5s,但是使用直接创建对象的方法需要6s。相较而言,有12倍的速度提升。
如果能将p1,p2和p_final当成Point使用而不是int的话,会更好。在C或C++中,你可以使用typedef,但是在Java中不能。如果你想试一试的话,你可以在Java编译之前,使用make文件让文件通过C的预编译,这时就可以使用#definePoint int.[注,这里我也不知道是什么意思,难道是使用JNI]
问题:我在循环中有一个负载的表达是,为了效率,我只想计算一次,但是为了可读性,我又想将它放到循环中,我该怎么办?
让我们使用正则表达式来作为例子,将字符串编译成一个有限状态机让match可以使用:
for(;;) { ... String str = ... match(str, compile("a*b*c*")); ... } |
因为Java没有宏定义,所以希望控制执行时间,你就被限制到了这里。一个可行的方法(尽管不很很优雅)是,使用内部interface的变量初始化块:
for(;;) { ... String str = ... interface P1 {FSA f = compile("a*b*c*);} match(str, P1.f); ... } |
当第一次使用P1时,P1.f会被初始化,它不会变,因为在interface中的变量,默认都是static final的。如果你不细化这样的话,你可以使用别的语言。在Common Lisp中,字符#.表示在编译时老计算表达式,而不是在运行时。所以:
(loop ... (match str #.(compile "a*b*c*")) ...) |
问题:还有没有别的操作是很慢的?
从哪里开始呢?这里有一些我们需要知道的。我写了一个统计时间的工具,来测试不同代码段的时间,并打印出统计结果。结果是每秒中循环多啊少千次和每次循环需要多少微秒。我是在Sparc 20主机,JDK 1.1.4 JIT编译器。
K/sec uSecs Code Operation ========= ======= ==================== =========== 147,058 0.007 a = a & 0x100; 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程序员(因为程序员通常是在在写代码而不是写作,我知道是因为我两个工作都干过)。在书里到处是错误,糟糕的建议,糟糕的程序。这样的书对初学者不利,但是如果你懂的别的语言的话,很容易可以分辨出这类书的。
好的:这里只有很少的好的Java的书籍。我个人喜欢official specification 和Arnold and Gosling,Marty Hall, andPeter van der Linden写的书. 为了参考的话,我喜欢Java in a Nutshell系列和Sun的官方文档 (我将javadoc APIs,languagespecification和amendments都放到我的本地硬盘上,并且在浏览器中打上mark,这样我就可以快速的访问它们了。)
不确定的:在这两者之间的是,草率的作者(他们应该理解的很好,但是没有花时间去真正的去探索Java是如何工作的,或者是因为出版日期的压力)。一个例子是Edward Yourdon's 的Java and the new Internet programming paradigm。Yourdon是这样书说的:
int[] a = {0,1, 2};
int[] b = a;
b[0] = 99;
这时a[0]的值是99,因为a和b指向的是同一个对象。
Translations:
Belorussiantranslation(白俄罗斯版) provided bymoneyaisle.
Peter Norvig
注:由于水平有限,希望大家指正(当然也包括错别字)。