[Java]关于"抽象类","抽象方法"等概念的理解的知识点总结 - Thinking in Java经典片段之三



<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 抽象类和抽象方法

在所有乐器的例子中,基类Instrument中的方法往往是“哑(dummy)”方法。若要调用这些方法,就会出现一些错误。这是因为Instrument类的目的是为它的所有导出类创建一个通用接口。

建立这个通用接口的唯一原因是,不同的子类可以用不同的方式表示此接口。它建立起一个基本形式,用来表示所有导出类的共同部分。另一种说法是将Instrument类称作“抽象基类”(或简称抽象类)。当我们想通过这个通用接口操纵一系列类时,就需创建一个抽象类。与任何基类所声明的签名相符的导出类方法,都会通过动态绑定机制来调用。(然而,正如前一节所讲,如果方法名与基类中的相同,但是参数不同,就会出现重载,这或许不是我们想要的)

如果我们只有一个像Instrument这样的抽象类,那么该类的对象几乎没有任何意义。也就是说,Instrument只是表示了一个接口,没有具体的实现内容;因此,创建一个Instrument对象没有什么意义,并且我们可能还想阻止使用者这样做。通过在Instrument的所有方法中打印出错误信息,就可以实现这个目的。但是这样做会将错误信息延迟到运行期才可获得,并且需要在客户端进行可靠、详尽的测试。所以最好是在编译期间捕获这些问题。
为此,Java提供一个叫做“抽象方法(abstract method)1”(对于C++程序设计员来说,这相当于C++语言中的纯虚函数)的机制。这种方法是不完整的;仅有声明而没有方法体。下面是抽象方法声明所采用的语法:
abstract void f();
包含抽象方法的类叫做“抽象类(abstract class)”。如果一个类包含一个或多个抽象方法,该类必须被限制为是抽象的。(否则,编译器就会报错)

如果一个抽象类不完整,那么当我们试图产生该类的对象时,编译器会怎样处理呢?由于为一个抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错信息。这里,编译器会确保抽象类的纯粹性,我们不必担心会误用它。
如果从一个抽象类继承,并想创建该新类的对象,那么我们就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限制修饰这个类。

我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果我们有一个类,让其包含任何abstract方法都显得没有实际意义,但是我们却想要阻止产生这个类的任何对象,那么这时这样做就很有用了。

Instrument类可以很容易地转化成抽象类。既然使某个类成为抽象类并不需要所有的方法都是抽象的,所以仅需将某些方法声明为抽象的即可。下面所示是它的模样:




我们可以看出,除了基类,实际上并没有什么改变。
创建抽象类和抽象方法非常有用,因为它们可以显化一个类的抽象性,并告诉用户和编译器怎样按照它所预期的方式来使用。



  • 构造器和多态


通常,构造器异与其他种类的方法。即使涉及到多态,也仍是如此。尽管构造器并不具有多态性(它们实际上是Static方法,只不过该Static声明是隐式的),但还是非常有必要理解构造器怎样通过多态在复杂的层次结构中运作。这一理解将有助于大家避免一些令人不快的困扰。

  • 构造器的调用顺序

基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确地构造。导出类只能访问它自己的成员,不能访问基类中的成员(基类成员通常是private类型)。只有基类的构造器才具有恰当的知识和权限对自己的元素进行初始化。因此,必须令所有构造器都得到调用,否则所有对象就不可能被正确构造。这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。在导出类的构造器主体中,如果我们没有明确指定调用某个基类构造器,它就会“默默”地调用缺省构造器。 如果不存在缺省构造器,编译器就会报错(若某个类没有构造器,编译器会自动合成出一个缺省构造器)。
<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>让我们来看下面这个例子,它展示了组合、继承以及多态的在构建顺序上的效果:


  1. //: Sandwich.java
  2. class Meal {
  3.     Meal() {System.out.println("Meal()");}
  4. }
  5. class Bread {
  6.     Bread() {System.out.println("Bread()");}
  7. }
  8. class Cheese {
  9.     Cheese() {System.out.println("Cheese()");}
  10. }
  11. class Lettuce {
  12.     Lettuce() {System.out.println("Lettuce()");}
  13. }
  14. class Lunch extends Meal {
  15.     Lunch() {System.out.println("Lunch()");}
  16. }
  17. class PortableLunch extends Lunch {
  18.     PortableLunch() {System.out.println("PortableLunch()");}
  19. }
  20. public class Sandwich extends PortableLunch {
  21.     private Bread b = new Bread();  //执行完Sandwich的构造器之后
  22.     private Cheese c = new Cheese();
  23.     private Lettuce l = new Lettuce();
  24.     public Sandwich() {
  25.         System.out.println("Sandwich()");
  26.     }
  27.     public static void main(String[] args) {
  28.         new Sandwich();
  29.     }
  30. }
运行结果与下:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
在这个例子中,用其他类创建了一个复杂的类,而且每个类都有一个它声明自己的构造器。其中最重要的类是Sandwich,它反映出了三层级别的继承(若将从Object的隐含继承也算在内,就是四级)以及三个成员对象。当在main()里创建一个Sandwich对象后,我们就可以看到输出结果。这也表明了这一复杂对象调用构造器要遵照下面的顺序:
1. 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等。直到最低层的导出类。
2. 按声明顺序调用成员的初始状态设置模块。
3. 调用导出类构造器的主体。

构造器的调用顺序是很重要的。当进行继承时,我们已经知道基类的一切,并且可以访问基类中任何声明为public和protected的成员。这意味着在导出类中,必须假定基类的所有成员都是有效的。一种标准方法是,构造动作一经发生,那么对象所有部分的全体成员都会得到构建。然而,在构造器内部,我们必须确保所要使用的成员都已经构建完毕。为确保这一目的,唯一的办法就是首先调用基类构造器。那么在进入导出类构造器时,在基类中可供我们访问的成员都已得到初始化。此外,在构造器中的所有成员必须有效也是因为当成员对象在类内进行定义的时候(比如上例中的b,c和l),我们应尽可能地对它们进行初始化(也就是,通过组合方法将对象置于类内)。若遵循这一规则,那么我们就能确定所有基类成员以及当前对象的成员对象都已初始化。但遗憾的是,这种做法并不适用于所有情况.
<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值