The Java IAQ: Infrequently Answered Questions (有关Java的不经常被问到的问题)

有关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方法有问题,请看看下面的一些地方是不是有问题:

  1. 你定义了错误的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

  }

}

  1. 你实现的equals方法没有遵守equals预定:对称,传递,自反。对称就是说如果a.equals(b)成立,那么b.equals(a)也成立(这是很多人会搞乱的地方)。传递就是说如果a.equals(b) 和 b.equals(c)成立,那么 a.equals(c)也成立。自反就是说a.equals(a)必须成立,这也是为什么上面的例子要使用(this == that)的原因。(这样做的好处有二,一效率提高,二有时可以打断相互引用)
  2. 你忘记了hashCode方法。只要你定义了equals方法,你应该定义hashCode方法。你必须保证两个相等的对象必须有一样的hashCode,如果你希望hashtable有更好的性能,你应该让大部分不想等的对象有不同的hashCode。一些类会将hashCode缓存起来,所以hashCode应该只计算一次。如果你使用if (this.hashSlot != that.hashSlot) return false,就就可以节省一些时间。
  3. 你没有处理好继承。首先,两个不同的类可以相等。在你说“不能,当然不能”之前。假设有一个Rectangle类,有两个属性width, height。和一个Box类,有三个属性,width,height,depth。是不是当Box中的depth==0然后就好Rectangle就相等了?你也许会说是的。 如果你在和一个non-final类打交道,这是很有可能你自己的类是子类,你又希望你自己的子类不很别人相染。在特殊的情况下,你可能会允许继承你的类,然后调用你的类的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);

  }

 

}

如果想让super.euqals工作的话,你必须仔细的定义和实现在C中的equals方法。例如,使用instanceof C,而不是that.getClass() == C.class。只有在你很确认连个对象是同一个类时才使用that.getClass() == C.class。

  1. 你没有很好的处理循环引用,例如:

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)我认为这里没有通用的规则去遵守,但是有一些点需要注意一下:

  • 继承一个没有源代码的类是很危险的,因为它的文档可能是不完全的。
  • 通过super方法来调用父类的方法,会使得你原先没有看到的问题显现出来
  • 不管是重载的方法还是没有重载的方法,你必须给予同样的重视。这是面向对象编程中的一个很大的谬论。使用继承你是可以写更少的代码。但是你也必须考虑那些你没有写的代码。
  • 如果子类改变类某些方法的契约或者是全部方法的契约。这种改变是很难意识到的,因为契约是非正式的(很多契约是出现中注释中的,还有一些是出现在方法签名上的)。在Properties例子中,我们不清楚契约是否已经破坏,因为我们不清楚default值是否被当作table中的一个entries了。

问题:对于继承我们还有别的什么方法来代替吗?

 

代理是一种。代理意味这你会维持对另外一个类的引用,然后调用这个类。通常代理比继承更安全,因为它强制你考虑每次调用之前的参数。因为是一个引用,它不强迫你接受父类的所以方法,这样你只需要提供对你自己有意义的方法就可以了。另一方面,它使你写更多的代码,并且很难重用(因为它不是子类)。

例如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。还有一些你需要注意的:

  1. 你不能在null上再调用方法了:x.m()会抛出异常,如果x是null并且m不是静态方法。
  2. 只有一个null,而不是一个类一个null。例如:((String) null == (Hashtable) null)
  3. 只要是方法签名允许的,你就可以将null作为方法参数传入。例如:System.out.println(null)是可以的,但是string.compareTo(null)不行。对于你自己写的方法,在你的javadoc中需要说明参数是否可以为null。
  4. 在JDK 1.1 到1.5,给匿名内部类的构造函数传入null会导致编译错误(e.g., new SomeClass(null) { ...})。但是如果传入的是表达式或者强制转换的null是可以的(new SomeClass((String) null) { ...})。
  5. null至少有三中意思:
    • 没有初始化。变量还没有被赋值
    • Non-existant/not applicable. For example, terminal nodes in a binary tree might be represented by a regular node with null child pointers.
    • 不存在/不适用。例如,用一个孩子指针是null的正常的节点来表示叶子节点。
    • 空。例如,你可以用null表示空的树。注意,这里和前者还是有一些区别的,尽管一些很总是将两者弄混。区别解释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() { }

}


问题:一个对象有多大?为什么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字节的额外空间占用):我会给你解释为什么。

注:这里为什么没有下文了呢?不知道


问题:初始化代码是按什么顺序来进行的?我应该将它们放到哪里?

实例变量可以在类的三个地方去初始化:

在定义实例变量的时候(在父类中)

class C {

    String var = "val";

在构造函数中

    public C() { var = "val"; }

在对象初始化块中。这是Java1.1的新特性。就行静态初始化块一样,只是没有static关键字

    { var = "val"; }

}

当你使用new C()所发生的过程是(不考虑内存溢出的情况):

  1. 调用C的父类的构造函数(除非C就是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();

    }

}

Output:

 1: a1

 2: a2

 3: A()

 4: b1

 5: B()

 6: b1 reset

 7: a2 reset

 8: c1

 9: c3

10: C()

11: c2

12: b2


Q:静态变量是如何初始化的?

区分静态变量初始化和实例化是很重要的。当你使用new来调用构造函数时实例会被创建。当第一次使用类C时,它会被初始化。在这时,对这个类的初始化方法会被调用,并且是基于文本的顺序的。这里有两种初始化代码:静态初始化块(static { ... }),类变量初始化(staticString var = ...)。

当是做下面的一些动作是会导致类变量被初始化:

  1. 通过调用构造函数来创建实例的;
  2. 调用了一个在C中定义的静态方法(没有继承)
  3. Assign or access a static variable that is declared (not inherited) in C. It does not count if the static variable is initialized with a constant expression (one involving only primitive operators (like + or ||), literals, and static final variables), because these are initialized at compile time.
  4. 读/写在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();

    }

}

Output:

 1: a1

 2: a2

 3: b1

 4: B()

 5: b1 reset

 6: a2 reset

 7: c1

 8: c3

 9: C()

10: c2

11: b2


问题:我有一个六个实例变量的类,每个都有可能被初始化,我是不是要写64个构造函数呢?

不,你不需要64(26)个构造函数. 让我们假设你有一个六个参数的类:

public class C { int a,b,c,d,e,f; }

你可以这样做:

  1. 估计你最经常用的变量组合,然后只提供这些常用组合的构造函数。优点:我们经常这么干。缺点:经常估计错误,会出现很多冗余代码。
  2. 定义一个setter方法,然后在该方法中把this返回。例如:

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的规则是不兼容的,如果想同时设置两个变量,是不可能的。

  1. 在默认的构造函数中使用匿名子类的非静态初始化方法:

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")}}.)

  1. 你可以使用直接支持方法3的语言,例如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中有keyword argument和可选参数一样:

C(a=10, c=30, e=50)            # Construct an instance; use defaults for b and d.


问题:什么时候我该使用构造函数,什么时候使用别的方法?

一个投机取巧的回答是,当时想new一个对象时,使用构造函数,这就是new所代表的意思。但是在什么时候该使用,必须使用多少时,通常构造函数经常被滥用,例如:

  • Setter:就想我们在上个问题中看到的,提供很多构造函数。通常我们只提供很少的构造函数,然后提供setter方法,来完成后续的初始化工作。不这样的话,你将需要以堆语句来完成初始化。Setter方法是一种很好的实践,因为你在构造函数中想该的东西和在setter方法中要该的东西通常是一样的,那么我们为什么要在两个地方使用通用的代码呢。
  • 工厂:通常你你希望创建的是一些类或接口的实例,但是你不关心哪个子类来完成工作或者想推迟到运行时来决定。例如,你写了一个计算器的applet,你希望调用Number(string),如果字符串是浮点型格式的话,返回Double,如果是整形的话,返回Long。但是你不能这么做,因为Number是一个抽象类,你不能直接使用它的构造函数任何调用构造函数的地方必须返回Number,而不是它的子类。工厂可以完成这样的工作,Java没有内置支持或者惯例来实现工厂,但是你自己可以实现。
  • 缓存和循环使用: 构造函数就是创建一个新的对象。但是创建一个新的对象是很昂贵的操作。在显示的世界中,你可以使用Recycling老避免GC的花销。例如new Boolean(x),创建一个新的Boolean,但是你可以使用x ? Boolean.TRUE : Boolean.FALSE来代替,后者循环使用了已经存在的Boolean值,而不是创建一个新的Boolean。Java如果推广这种方法,而不是推广构造函数的话,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[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编译器。

  • 所以这些都是在1998年之前,编译器在这之后已经变了
  • 往下计数(i.e. for (int i=n; i>0; i--))比往上计数的快两倍:在我的机器上一秒内可以往下计数到144百万,而往上只能到72百万。
  • Math.max(a,b)比(a > b) ? a : b慢7倍。主要是花在函数调用上。
  • 数组比Vector快15到30倍,Hashtable的速度是Vector的2/3.
  • bitset.get(i)比bits & 1 << i慢60倍。主要花在异步调用上。当然,如果你想使用多于64bit的位运算,你不能使用后者。下面是不同的数据结构的getting和setting的时间:

  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编译器很难将循环中的常量表达式提出来。C/Java的循环是一个很糟的实现,因为在大多数情况下,它鼓励大家重新计算结尾的值。所以for(int i=0; i

问题:我如何从关于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是这样书说的:

  • “函数已经被消灭”,确实在Java中没有function关键字。Java称之为method(Perl叫subroutines, Scheme叫procedures,但是你不能说这些语言没有function)。一个合理的说法是,Java中没有全局的函数。但是更确切的说法是:Java是有全局性的函数的。只是需要被定义到类中,然后通过static methodC.f来调用而不是function f。
  • “自动不同类型转换已经被消灭”。确实在Java中这种相信很少,但是远远没有消除。例如你可以使用(1.0 + 2),这是2自动被转换成double,使用("one" + 2),2自动被转换成string。
  • “指针和指针计算已经被消除”。确实在Java中显式的指针计算已经没有的(这点很好)。但是指针还是有的,事实上,每个对象的引用就是一个指针。(这也是为什么会有NullPointerException)不知道这点的不是一个合格的Java程序员,每个Java程序员都应该知道:

    int[] a = {0,1, 2};

    int[] b = a;

    b[0] = 99;

这时a[0]的值是99,因为a和b指向的是同一个对象。

  • “因为结构体已经没有了,数组和字符串也是对象,所以指针的需求已经很多部分消除了”。这也会让人误解的。首先,结构体没有消失,只不过是被称为classes。去除的是程序员不需要关系结构体/类是在heap上或stack上分配的了。在Java中,所有的对象都是在heap中分配的。这也是为什么我们对指针不需要使用句法标记(例如 *)的原因。Yourdan说在C或者汇编中有指针可以指向数组或者字符串的中间是一个好的实践的对的。但是在别的语言中它既不被支持也不被遗漏。[不知道这句话什么意思]
  • Yourdan又说数组有length()方法(而不是length成员变量);可修改的字符串应该用StringClass来表示(而不是StringBuffer).这些都很烦人,它们不会有害但是也不全对。

Translations:

Belorussiantranslation(白俄罗斯版) provided bymoneyaisle.


Peter Norvig


注:由于水平有限,希望大家指正(当然也包括错别字)。

你可能感兴趣的:(Java)