A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© Yeauty 中级黑马   /  2016-11-5 09:16  /  970 人查看  /  4 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

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



4 个回复

倒序浏览
精辟,原先压根没考虑过的呢长知识
来自宇宙超级黑马专属苹果客户端来自宇宙超级黑马专属苹果客户端
回复 使用道具 举报
回复 使用道具 举报
哇哦……又长见识了。
来自宇宙超级黑马专属安卓客户端来自宇宙超级黑马专属安卓客户端
回复 使用道具 举报
。。。有些看不懂啊,有点深奥了
来自宇宙超级黑马专属苹果客户端来自宇宙超级黑马专属苹果客户端
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马