首页 > 代码库 > JAVA编程思想(4) - 多态(三)

JAVA编程思想(4) - 多态(三)

若干个对象共享

  • 例如Frog对象拥有其自己的对象,并且知道他们的存活多久,因为Frog对象知道何时调用dispose()去释放其对象。然而,如果这些成员对象中存在于其他一个或多个对象共享的情况,问题将不再简单,不再能简单的调用dispose()了。在这种情况下,我们也许需要引用计数来跟踪依旧访问着共享对象的数量。
//: polymorphism/ReferenceCounting.java
// Cleaning up shared member objects.
import static net.mindview.util.Print.*;

class Shared {
  private int refcount = 0;
  private static long counter = 0;
  private final long id = counter++;
  public Shared() {
    print("Creating " + this);
  }
  public void addRef() { refcount++; }
  protected void dispose() {
    if(--refcount == 0)
      print("Disposing " + this);
  }
  public String toString() { return "Shared " + id; }
}

class Composing {
  private Shared shared;
  private static long counter = 0;
  private final long id = counter++;
  public Composing(Shared shared) {
    print("Creating " + this);
    this.shared = shared;
    this.shared.addRef();
  }
  protected void dispose() {
    print("disposing " + this);
    shared.dispose();
  }
  public String toString() { return "Composing " + id; }
}

public class ReferenceCounting {
  public static void main(String[] args) {
    Shared shared = new Shared();
    Composing[] composing = { new Composing(shared),
      new Composing(shared), new Composing(shared),
      new Composing(shared), new Composing(shared) };
    for(Composing c : composing)
      c.dispose();
  }
} /* Output:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
*///:~
  • static long counter跟踪所创建的Shared的实例的数量,idfinal的,因为我们不希望它的值在对象生命周期中被改变。
  • 在将一个共享对象附着到类上时必须记住调用addRef(),但是dispose()方法跟踪引用数,并决定何时执行清理。

构造器内部的多态方法行为

  • 如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法是会发生什么?
  • 在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。
  • 如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而,这个调用效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就被调用。这可能会造成一些难于发现的错误。
  • 简单来说就是在基类的构造器中调用基类中函数,这个函数也在导出类中被覆盖,当在导出类调用基类构造器的时候,那么因为一个动态绑定的方法调用会向外深入继承层次结构内部,它可以调用导出类里的方法。那么可能会调用某个方法,而这个方法所操作的成员可能还未进行初始化。
//: polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don‘t produce what you might expect.
import static net.mindview.util.Print.*;

class Glyph {
  void draw() { print("Glyph.draw()"); }
  Glyph() {
    print("Glyph() before draw()");
    draw();
    print("Glyph() after draw()");
  }
}    

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    print("RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    print("RoundGlyph.draw(), radius = " + radius);
  }
}    

public class PolyConstructors {
  public static void main(String[] args) {
    new RoundGlyph(5);
  }
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*///:~
  • Glyph.draw()方法设计将会被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对RoundGlyph.draw()的调用,这似乎是我们的目的,但是输出的结果却不是我们预计的,我们发现当Glyph()的构造器调用draw()方法时,radius不是默认初始值1,而是0。
  • 我们先来看看初始化的实际过程:
    1. 在其他任何食物发生之前,将分配给对象的存储空间初始化为二进制的零。
    2. 如以前说的一样会调用基类构造器。此时,调用被覆盖的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0。
    3. 按照声明的顺序调用成员的初始化方法。
    4. 调用导出类的构造器主体。
  • 这样做有一个优点,那就是所有东西都至少初始化为零。而不是仅仅留作垃圾。对象的引用通常为null,所以如果忘记为该引用进行初始化,就会在运行时出现异常。
  • 因此,编写构造器的时候有一条准则:“用尽可能简单的方法使对象进入正常状态,可以的话,避免调用其他方法。”在构造器唯一能够安全调用的那些方法是基类中的final方法。

用继承进行设计

  • 多态并不是用来使得任何东西都可以被继承的,因为这反倒会加重我们的设计负担,使事情变得不必要的复杂起来。
  • 更好的方式是选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合更加灵活,因为它可以动态选择类型(因此也就选择了行为);相反,继承在编译时需要知道确切类型。
//: polymorphism/Transmogrify.java
// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).
import static net.mindview.util.Print.*;

class Actor {
  public void act() {}
}

class HappyActor extends Actor {
  public void act() { print("HappyActor"); }
}

class SadActor extends Actor {
  public void act() { print("SadActor"); }
}

class Stage {
  private Actor actor = new HappyActor();
  public void change() { actor = new SadActor(); }
  public void performPlay() { actor.act(); }
}

public class Transmogrify {
  public static void main(String[] args) {
    Stage stage = new Stage();
    stage.performPlay();
    stage.change();
    stage.performPlay();
  }
} /* Output:
HappyActor
SadActor
*///:~
  • 在这里,Stage对象包含一个对Actor的引用,而Actor被初始化为HappyActor对象。这意味着performPlay()会产生某种特殊的行为。而当我们替换actor的对象引用的话,就可以改变我们的行为了。
  • 一条准则是:“用继承表达行为的差异,并用字段来表达状态上的变化。”

纯继承与扩展

  • 采用“纯粹”的方式("is-a")来创建继承层次结构似乎是最好的方式。也就是说,只有基类已经建立的方法才可以在导出类中被覆盖。也可以说是导出类“只有”覆盖基类的函数没有其他的函数。
  • 因为一个类的接口已经确定了它应该是什么,继承可以确保所有的导出类具有基类的接口,且绝不会少。“纯粹”的继承,导出类将具有和基类一样的接口。
  • 也可以认为这是一种纯替代,因为导出类可以完全代替基类,而在使用它们时,可以不需要知道关于子类的任何额外信息。也就是说,基类可以接收发送给导出类的任何信息,因为二者具有完全相同的接口。我们只需知道从导出类向上转型,永远不需要知道正在处理的对象的确切类型。这都是通过多态来处理的。
  • 但是导出类往往不是只单纯的只具有从基类继承的接口,它还有自己的额外函数,我们可以称为是(is-like-a)(像一个)关系,因为导出类就像是一个基类——它有着相同的接口,但是它还具有由额外方法实现的其他特性。
  • ”像一个“关系也是有缺点的。导出类中接口的扩展部分不能被基类访问。因此,一旦我们向上转型,就不能调用那些新方法。在这种情况下,我们一般是重新查看对象的确切类型,以便我们能够访问该类型所扩充的方法。

向下转型与运行时类型识别

  • 由于向上转型会丢失具体的类型信息,所以我们想,通过向下转型来获取类型信息。然后我们知道向上转型是安全的,因为基类的接口是不会大于导出类的接口的。因此,我们通过基类的接口发送的信息保证都能被接受。但是对于向下转型,我们无法知道我们它将转型是哪种类型。
  • 在Java中,所有转型都会得到检查!所以即使我们只是进行一次普通的加括弧形式的类型转型,在进入运行时仍会进行检查,如果不是正确的类型,会返回一个ClassCastException(类转型异常)。这种在运行时期对类型进行检查的行为称作“运行时类型识别”(RTTI)。
//: polymorphism/RTTI.java
// Downcasting & Runtime type information (RTTI).
// {ThrowsException}

class Useful {
  public void f() {}
  public void g() {}
}

class MoreUseful extends Useful {
  public void f() {}
  public void g() {}
  public void u() {}
  public void v() {}
  public void w() {}
}    

public class RTTI {
  public static void main(String[] args) {
    Useful[] x = {
      new Useful(),
      new MoreUseful()
    };
    x[0].f();
    x[1].g();
    // Compile time: method not found in Useful:
    //! x[1].u();
    ((MoreUseful)x[1]).u(); // Downcast/RTTI
    ((MoreUseful)x[0]).u(); // Exception thrown
  }
} ///:~

JAVA编程思想(4) - 多态(三)