[关闭]
@lemonguge 2015-06-23T02:23:11.000000Z 字数 13124 阅读 391

Java的初始化与清理

JAVA


用构造器确保初始化

随着计算机革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一,初始化和清理正是涉及安全的两个问题。

在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。在创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。如果没有正确的构造器调用,编译器就不允许创建对象。

构造器又名构造函数,即构建创造对象时调用的函数。对象创建时,才会被调用且只调用一次,作用是可以给对象进行初始化。

如果在自定义的类中没有构造器,则编译器会自动帮你生成一个默认的构造器,但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。

这就好比,要是你没有提供任何构造器,编译器就会认为“你需要一个构造器,让我给你制造一个吧”;但假如你已写了一个构造器,编译器就会认为“啊,你已写了一个构造器,所以你知道你在做什么,你只是刻意省略了默认构造器”。

构造器,这种精巧的初始化机制,应该给了我们很强的暗示:初始化在Java中占用至关重要的地位。


方法重载

任何程序设计语言都具备的一项重要特征就是对名字的运用。

所谓方法则是给某个动作取的名字,通过使用名字,你可以引用所有的对象和方法。在日常生活中,相同的词可以表达出多种不同的含义——它们被“重载”了。

举个例子,你可以说“清洗衬衫”、“清洗车”、“清洗狗”。虽然都是“清洗”这个动作,但是我们可以明白这几个“清洗”的动作不一样,所以我们并不会说“以洗衬衫的方式洗衬衫”、“以洗车的方式洗车”、“以洗狗的方式洗狗”,这就是人类语言所具有的冗余性,映射到程序设计语言中——“清洗”被重载了。

在Java中,由于构造器的名字已经由类名所决定,就只能有一个构造器名。如果想要用多种方式创建一个对象时,就必须用到重载,构造器是强制重载方法名的另一个原因。

每一个重载的方法都必须有一个独一无二的参数类型列表

有时候定义一个重载的方法时,可能会通过参数顺序的不同来区分两个方法,不过,不建议这样做,因为会使代码难以维护!

不能通过方法的返回值来区分重载的方法!

有时候,我们调用一个方法可能并不关心方法的返回值,我们需要的是方法调用产生的效果,这时我们会忽略方法的返回值。

基本数据类型的重载

当基本数据类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦涉及到重载,可能会造成混淆。举个简单的例子:有两个重载的方法show(short x)show(int x),如果传入了一个char类型的参数,那么会调用哪个方法呢?

如果方法接受较小的基本数据类型作为参数,如果传入的实际参数较大,就得通过类型转换来执行窄化转换。举个简单的例子:有一个方法show(int x),如果想传入了一个long类型的参数x,那么需要先执行窄化转换(int)x。如果不这样做,编译器就会报错。


this关键字

方法中的this

首先我们可以了解一下this关键字的由来:在Java这种高级语言中,一个基本的特征是万物皆对象,如果有同一类型的两个对象,分别为ab,都有可能调用该类中的一个方法,把这方法看成是一个对象的话,那么这个方法如何知道是被a还是被b所调用呢?

  1. class Banana {
  2. // 对于这个方法,它是如何知道是被哪个对象所调用?
  3. void peel(int i) { }
  4. }
  5. public class BananaPeel {
  6. public static void main(String[] args) {
  7. // 结尾为","如果是";"代表语句的结束,则需要明确b的引用类型。
  8. Banana a = new Banana(),
  9. b = new Banana();
  10. a.peel(1);
  11. b.peel(2);
  12. }
  13. }

对于上面的这个问题,Java的编译器做了一些幕后的工作。它暗自把“所操作对象的引用”作为第一个参数传递给被调用的方法。

  1. // a和b调用peel的方法就如同下面,这是内部的表现形式,这样书写代码不能通过编译。
  2. Banana.peel(a, 1);
  3. Banana.peel(b, 1);

通过上面可以知道,方法的内部其实是应该有一个引用,而该引用就是调用该方法的那个对象的引用。那么我们就会想到给这个引用取一个名字来操作这个对象,在Java中,为此便有一个专门的关键字:this

this这个关键字在方法内部使用,表示对“调用方法的那个对象”的引用。

因为在方法内部要么直接使用该对象,要么就是使用这个对象的字段或方法。

因为只要有一个方法被对象调用,那么当前的方法中的this引用会自动应用于同一类中的其他方法。这句话更容易理解的说就像下面这样:

  1. public class Apricot {
  2. void pick() { }
  3. void pit() {
  4. pick(); // 可以写成this.pick();
  5. }
  6. }

当有一个对象apricot调用pit()方法时,pit()方法中就获得了apricot这个对象的引用,那么pit()这个方法的this引用会自动应用与Apricot类的方法和字段。当调用pick()方法时,因为之前已经持有了指向apricot对象的引用,所以就可以省略this

构造器中的this

当为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码,this关键字可以做到这一点。

this添加了参数列表,那么就表示对符合此参数列表的某个构造器的明确调用。


super关键字

构造器中的super

在子类的构造函数中第一行有一个默认的隐式语句super();

子类中所有的构造函数默认都会访问父类中的空参数的构造函数。假如没有默认的基类构造器,或者想调用一个带参数的基类构造器,就必须用关键字super明确要调用父类中哪个构造函数,即super(..)方式。

那么我们可能会想到为什么子类实例化的时候要访问父类中的构造函数呢?

因为子类继承了父类,会获取到了父类中内容,所以在使用父类内容之前,要先看父类是如何对自己的内容进行初始化的。最明显的一个现象是子类对象中会有一个专门用于存储父类成员变量的空间,如果父类不进行初始化,那么继承过来的父类成员变量将毫无意义!即子类在构造对象时,必须访问父类中的构造器。

super语句必须要定义在子类构造函数的第一行。

因为父类的初始化动作要先完成。

在构造函数中,super(..)this(..)不能共存。

因为superthis都只能定义第一行,所以只能有一个。如果子类中的某个构造器有调用另一个构造器,虽然这个构造器不能定义super,但是被调用的那个构造器中会有super语句,就算被调用的那个构造器也定义了this来调用其他的构造器,可以肯定的是一定存在一个构造函数来访问父类的构造函数。

方法中的super

super.成员变量

堆内存中是没有父类对象的,所以子类对象在堆内存创建时就会分出两块区域分别用于放置子类的成员变量和父类的成员变量,即使碰到父类的成员变量和子类的成员变量的名称相同的情况,这也不会造成任何的影响,因为这两个同名的成员变量会位于子类对象在堆内存中的不同区域。

成员变量不能被覆盖。

如果子类的某个方法中,直接使用成员变量时,则默认优先访问子类的成员变量,如果子类自身的成员变量中没有,才会到存储父类的成员变量的区域进行访问。当父类的成员变量和子类的成员变量的名称相同,若想访问父类的成员变量,则需要super.成员变量来显式访问父类的成员变量。

super.成员函数

当我们通过new关键字在堆内存中创一个子类对象时,经过初始化之后,如果子类对象调用了一个含有super.成员函数的方法时,我们要知道此时堆内存中是没有父类对象的,因为没有通过new来创建父类对象。那么我们应该会想到super关键字是如何来访问父类的成员函数呢,回想一下JVM中的五块存储空间,只有方法区才会存储了每个类的信息,子类和父类的信息都存储在方法区,其中子类的super指向父类在方法区中的位置中,若要访问父类的成员函数,则会先通过super来找到父类在方法区中的位置,然后再调用父类的方法。

通过上面的介绍:在方法中,this通常在栈中代表一个对象的引用,而super通常在方法区中代表一个父类的空间。

当子父类中出现方法一模一样的情况时,父类的方法是可以被子类的所覆盖的。

对第一条中的权限需要向大家进行一下解释,因为很容易产生误区。

  1. public class Fu {
  2. private void show() {
  3. System.out.println("Fu : ");
  4. }
  5. }
  6. class Zi extends Fu {
  7. //大家千万不要被输出的内容而迷惑,以为Zi的show()覆盖了父类的方法。
  8. public void show() {
  9. System.out.println("Zi : 0");
  10. }
  11. public static void main(String[] args) {
  12. Zi z = new Zi();
  13. z.show();
  14. }
  15. } /* Output:
  16. Zi : 0
  17. *///:~

为了证明父类的private权限方法不能覆盖,我们只需要在Fu类的方法前加入final关键字(final关键字将方法锁定,并且不会被覆盖)。

  1. public class Fu {
  2. private final void show() {
  3. System.out.println("Fu : ");
  4. }
  5. }
  6. class Zi extends Fu {
  7. //编译器并没有报错,说明了子类的show()方法并不是覆盖父类的方法
  8. public void show() {
  9. System.out.println("Zi : 1");
  10. }
  11. public static void main(String[] args) {
  12. Zi z = new Zi();
  13. z.show();
  14. }
  15. } /* Output:
  16. Zi : 1
  17. *///:~

如果将Fu类中的show()的权限修饰词private改为包访问权限。

  1. public class Fu {
  2. final void show() {
  3. System.out.println("Fu : ");
  4. }
  5. }
  6. class Zi extends Fu {
  7. // Cannot override the final method from Fu
  8. // 编译器报错,子类的show()不能覆盖父类的final方法
  9. // ! public void show() { System.out.println("Zi : 2"); }
  10. }

通过上面这个三个小例子,这只是展示了一个小现象。这很容易进行解释:对于那些子类没有权限访问的基类方法,可以理解为,子类根本都不知道父类有这些方法,所以也谈不上是覆盖,覆盖应该是面向那些子类可以有权限能访问的父类方法。下面这个例子相信读者能够对笔者这段话有更容易理解。

  1. package access.cookie2;
  2. // Cookie类在access.cookie2包中
  3. public class Cookie {
  4. public Cookie() {
  5. System.out.println("Cookie constructor");
  6. }
  7. //final的包访问权限show()方法
  8. final void show(){
  9. System.out.println("Cookie show..");
  10. }
  11. }
  12. package access;
  13. import access.cookie2.Cookie;
  14. // ChocolateChip2类在access包中,与Cookie类不在同一个包
  15. public class ChocolateChip2 extends Cookie {
  16. public ChocolateChip2() {
  17. System.out.println("ChocolateChip2 constructor");
  18. }
  19. //不是覆盖,ChocolateChip2类根本就无法通过super来访问Cookie类在方法区的show()方法
  20. public void show(){
  21. System.out.println("ChocolateChip2 show..");
  22. }
  23. public static void main(String[] args) {
  24. ChocolateChip2 x = new ChocolateChip2();
  25. x.show();
  26. }
  27. } /* Output:
  28. Cookie constructor
  29. ChocolateChip2 constructor
  30. ChocolateChip2 show..
  31. *///:~

清理:终结处理和垃圾回收

垃圾回收器只知道释放那些经由new分配的内存。

想想这样一个问题,假定你的对象(并非使用new)获得了一块“特殊”的内存区域,而垃圾回收器却不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize()的方法。

  1. // 在Object类中有下面的方法。
  2. protected void finalize() throws Throwable { }

这个方法的工作原理可以这样理解:一旦垃圾回收器准备好释放对象所占用的存储空间,垃圾回收器将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象所占用的内存。

Java里的对象并非总是会被垃圾回收。

  1. 对象可能不被垃圾回收;
  2. 垃圾回收只与内存有关。

可能大家会考虑到一个问题:对象为什么可能不被垃圾回收?

因为垃圾回收本身也是有开销的,只要程序没有濒临存储空间用完的那一刻,对象可以不被释放,随着程序的退出,即使垃圾回收器一直都没有释放你创建的任何对象的存储空间,那些资源也会被全部交还给操作系统。

接下来大家又可能会想到,通过以new关键字创建对象之外的方式来为对象分配了存储空间,这种特殊情况是怎么回事呢?

这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放,从而造成了内存泄露。当然free()是C和C++的函数,所以需要在finalize()中用本地方法调用它,即在我们自定义类中的finalize()方法通常需要有super.finalize();的一段代码来调用父类的本地方法释放内存。

通常,不能指望finalize(),必须创建其他的“清理”方法,并且明确地调用它们。所幸的是我们绝大多数情况下是使用new关键字来创建对象的方式,而通过这种方式创建的对象,Java的垃圾回收器会帮助我们释放存储空间。

由于对象有可能被垃圾回收器回收,那么只要对象被回收,就一定会调用finalize方法,因此我们可以在该方法里来验证对象的终结条件。

举个例子:图书馆的每一本书被借出的时候都应该被登记,图书馆有可能会对书进行统计,借出的书要被知道。

  1. class Book extends Object {
  2. // 记录被垃圾回收器所所收Book类型对象的个数
  3. static int i;
  4. // 是否被借出,书一开始都是在图书馆里,所以默认为false
  5. boolean isOut = false;// 可省,因为对象一旦在堆内存中创建,boolean类型的成员变量默认初始化为false。
  6. // 书名
  7. String name;
  8. Book(String name) {
  9. this.name = name;
  10. }
  11. // 书只要被借出,就置为true
  12. void borrow() {
  13. isOut = true;
  14. }
  15. protected void finalize() throws Throwable {
  16. i++;
  17. System.out.println("gc...clear " + i + " now:" + name);
  18. if (isOut)
  19. System.out.println(name + "书被借出"); // 如果借出,输出未归还
  20. super.finalize(); // 调用本地方法来释放存储空间
  21. }
  22. }
  23. public class TerminationCondition {
  24. public static void main(String[] args) {
  25. Book novel = new Book("A");
  26. novel.borrow(); // A书被借出
  27. new Book("B"); // B书仍在图书馆
  28. // 运行垃圾回收器
  29. System.gc();
  30. }
  31. } /* Output:可能有五种情况
  32. 1、没有结果
  33. 2、gc...clear 1 now:B
  34. 3、gc...clear 1 now:A
  35. A书被借出
  36. 4、gc...clear 1 now:A
  37. A书被借出
  38. gc...clear 2 now:B
  39. 5、gc...clear 1 now:B
  40. gc...clear 2 now:A
  41. A书被借出
  42. 垃圾回收器的线程是一个新的线程,所以A和B不一定谁先被回收。
  43. *///:~

对象的初始化顺序

  1. 当需要装载某个类(首次创建该类的对象或者是访问了该类的静态方法/静态域),如果有基类的话,编译器会先在类路径classpath下查找到其基类.class文件,然后将基类的字节码载入方法区,接下来编译器才会对这个类重复以上的动作。有关静态初始化的所有动作都会被顺序执行,先默认初始化后显式初始化、静态代码块初始化。
  2. 然后将这个类的字节码从方法区加载到堆内存中,这将创建该类的class对象,可以通过这个class对象创建任何的该类对象,所以也将这个类对象称为对象的蓝图。
  3. 当使用new关键字在堆内存中创建对象时,首先为该对象分配足够的存储空间,这块存储空间会被清零,该对象的成员变量(包括从基类继承过来的成员变量)将会被默认初始化。
  4. 由于父类的初始化动作要先完成,之后该类的构造函数进栈,再通过指明的或者默认的父类构造器来对父类进行初始化,即父类构造器也进栈。在子类对象里有一块专门用于存储父类的成员变量的内存空间,首先会对这块区域里的父类成员变量进行显式初始化,构造代码块初始化,接下来由父类的构造器来进行初始化,完成之后父类构造器出栈。
  5. 该对象的成员变量开始进行显式初始化。
  6. 由构造器进行初始化,完成之后该类构造器出栈。
  1. class Fu {
  2. int i = 5;
  3. {
  4. System.out.println("Fu .. i = " + i); // 3
  5. i = 6;
  6. }
  7. public Fu() {
  8. System.out.println("Fu Constructor : " + i); // 4
  9. }
  10. static {
  11. System.out.println("Fu.class running .."); // 1
  12. }
  13. }
  14. class Zi extends Fu {
  15. int i = 10;
  16. {
  17. System.out.println("Zi .. i = " + i + ",Fu .. i = " + super.i); // 5
  18. i = 12;
  19. }
  20. static {
  21. System.out.println("Zi.class running .."); // 2
  22. }
  23. public Zi() {
  24. System.out.println("Zi Constructor : " + i + ",Fu .. i = " + super.i); // 6
  25. }
  26. }
  27. public class Constructor {
  28. public static void main(String[] args) {
  29. new Zi();
  30. System.out.println("-------");
  31. new Zi();
  32. }
  33. } /* Output:
  34. Fu.class running ..
  35. Zi.class running ..
  36. Fu .. i = 5
  37. Fu Constructor : 6
  38. Zi .. i = 10,Fu .. i = 6
  39. Zi Constructor : 12,Fu .. i = 6
  40. -------
  41. Fu .. i = 5
  42. Fu Constructor : 6
  43. Zi .. i = 10,Fu .. i6
  44. Zi Constructor : 12,Fu .. i = 6
  45. *///:~

不知道大家对第四句的输出结果Fu Constructor : 6有什么想法?

当子类的构造器进栈时,构造函数中也会持有一个this的引用,这个引用指向堆内存中的子类对象。接下来父类的构造器被调用也进栈,该父类的构造函数中也会持有一个this引用,同样指向堆内存中的子类对象。当轮到父类的构造器进行初始化的时候,System.out.println("Fu Constructor : " + i);中的i应该是this.i,即子类对象的成员变量i。由于成员变量不能被覆盖,且访问的时候,默认优先访问子类的成员变量,此时子类的i只经过了默认初始化,所以不应该输出Fu Constructor : 0的吗?

之所以输出的是6而不是0,是因为:子类对象在堆内存中被创建时,会开辟出两块区域分别存放子类的成员变量和父类的成员变量,我们给这两块区域取一个名字,存放子类的成员变量的区域称为“Zi”,存放父类的成员变量的区域称为“Fu”。子类的构造器进栈时,它可以给这两块区域进行初始化;当父类的构造器进栈时,它只能“Fu”这块区域初始化,它不能访问到“Zi”区域,所以this.i是子类对象中存放父类成员变量的区域里的i

  1. class Fu {
  2. int i = 5;
  3. public Fu() {
  4. System.out.println("Fu Constructor : " + i);
  5. }
  6. }
  7. class Zi extends Fu {
  8. public Zi() {
  9. i = 10; // 从Fu继承的成员变量
  10. System.out.println("Zi Constructor : " + i + ",Fu .. i = " + super.i);
  11. }
  12. }
  13. public class Constructor {
  14. public static void main(String[] args) {
  15. new Zi();
  16. }
  17. } /* Output:
  18. Fu Constructor : 5
  19. Zi Constructor : 10,Fu .. i = 10
  20. *///:~

i是从Fu继承过来的成员变量,在子类的构造器中将i重新赋值为10,说明了子类的构造器可以给这两块区域进行初始化。

  1. class Fu {
  2. int i = 5;
  3. public Fu() {
  4. System.out.println("Fu Constructor : " + i);
  5. }
  6. }
  7. class Zi extends Fu {
  8. int i = 8;
  9. public Zi() {
  10. i = 10; // Zi的成员变量
  11. System.out.println("Zi Constructor : " + i + ",Fu .. i = " + super.i);
  12. }
  13. }
  14. public class Constructor {
  15. public static void main(String[] args) {
  16. new Zi();
  17. }
  18. } /* Output:
  19. Fu Constructor : 5
  20. Zi Constructor : 10,Fu .. i = 5
  21. *///:~

在子类的构造器中将i重新赋值为10,而父类的成员变量i并没有变,说明了子类的构造器优先给存放子类的成员变量的区域进行初始化。如果想给父类的成员变量进行初始化只需要用super显式声明即可,如下所示:

  1. class Fu {
  2. int i = 5;
  3. public Fu() {
  4. System.out.println("Fu Constructor : " + i);
  5. }
  6. }
  7. class Zi extends Fu {
  8. int i = 8;
  9. public Zi() {
  10. super.i = 10; // 显式的来给子类对象中的存放父类成员变量的区域进行初始化
  11. System.out.println("Zi Constructor : " + i + ",Fu .. i = " + super.i);
  12. }
  13. }
  14. public class Constructor {
  15. public static void main(String[] args) {
  16. new Zi();
  17. }
  18. } /* Output:
  19. Fu Constructor : 5
  20. Zi Constructor : 8,Fu .. i = 10
  21. *///:~

任何域访问操作不是多态,因此看起来就像是由编译器解析。

因为成员变量不能被覆盖,当使用父类引用访问成员变量时,在编译期父类对象一定要有该成员变量,否则将不能通过编译,且任何子类对象中也一定有一块区域用于存储父类的成员变量,所以根本不需要去考虑子类对象,而会直接去访问子类对象中那块存储父类的成员变量的区域。

  1. class Fu {
  2. int i = 5;
  3. }
  4. class Zi extends Fu {
  5. int i = 8;
  6. }
  7. public class Constructor {
  8. public static void main(String[] args) {
  9. Fu f = new Zi();
  10. // 直接去访问了子类对象中那块存储父类的成员变量的区域
  11. System.out.println(f.i); // 输出5
  12. }
  13. }

方法调用机制

将“一个方法调用”同“一个方法主体”关联起来被称作绑定

若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。我们也许是第一次听到这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。例如,C只有一种方法调用,那就是前期绑定。

Java是面向对象的语言,所以在Java中我们会接触到另一个术语,叫做后期绑定。它的含义是:在运行时根据对象的类型进行绑定。相对而言,如果是根据引用的类型那便是前期绑定

后期绑定也叫做动态绑定运行时绑定

在Java中除了staticfinalprivate方法外,几乎都是后期绑定。(后面对“几乎”进行会介绍)

这句话就意味着:在通常情况下,我们不必判断是否应该进行后期绑定——它会自动发生。

这个时候我们肯定就会想为什么staticfinalprivate方法是前期绑定?

类中所有的private方法相当于隐式的指定为final

多态

多态的表现为父类的引用类型指向子类的对象,作用是前期定义的代码可以使用后期的内容,消除了类型之间的耦合关系。

弊端是前期定义的内容不能使用(调用)后期子类的特有内容,如果想访问导出类的特有内容,可以向下转型来实现。

在使用多态时,使用的是覆盖的方法,更多的运用是后期绑定的方法。

  1. public class Fu {
  2. int i = 5;
  3. void show() {
  4. System.out.println("Fu : "+i);
  5. }
  6. public static void main(String[] args) {
  7. Fu f = new Zi();
  8. f.show(); // 输出Zi : 8
  9. }
  10. }
  11. class Zi extends Fu {
  12. int i = 8;
  13. void show() {
  14. System.out.println("Zi : "+i);
  15. }
  16. }

根据方法调用机制,首先对编译期show()方法进行判断,该show()方法属于Fu的引用类型,方法的前面没有staticfinalprivate关键字,即后期绑定。在运行期根据具体的对象进行调用,而对象是通过new Zi()创建的。会从方法区中调用Zi类的show()方法进栈,所以输出了Zi : 8Zishow()方法将Fushow()方法覆盖,而且方法的调用属于后期绑定,这是我们在平常开发中常常碰到的情况。

  1. public class Fu {
  2. int i = 5;
  3. private void show() {
  4. System.out.println("Fu : "+i);
  5. }
  6. public static void main(String[] args) {
  7. Fu f = new Zi();
  8. f.show(); // 输出Fu : 5
  9. }
  10. }
  11. class Zi extends Fu {
  12. int i = 8;
  13. void show() {
  14. System.out.println("Zi : "+i);
  15. }
  16. }

这段代码与前面的代码的差别只是在Fushow()方法的前面加入了private关键字。根据方法调用机制,首先对编译期show()方法进行判断,该show()方法属于Fu的引用类型,方法的前面有private关键字,即前期绑定。将根据引用类型进行调用,即Fu类中的show()方法,此时show()方法中持有一个this引用,指向的是通过new关键字创建的子类对象。在方法区中,通过子类的super找到向父类在方法区中的位置,然后将父类的show()方法载入栈中。show()里有一个i,即this.i,由于域的访问不是多态,而是直接去访问了子类对象中那块存储父类的成员变量的区域,所以输出的是i是5。Zishow()方法将Fushow()方法覆盖,但是方法的调用属于前期绑定,这种情况我们会很少碰到。

大家可能会想,为什么要说“几乎都是后期绑定”,看看下面这个例子:

  1. package access.cookie2;
  2. import access.ChocolateChip2;
  3. // Cookie类在access.cookie2包中
  4. public class Cookie {
  5. void show() {
  6. System.out.println("Cookie..");
  7. }
  8. public static void main(String[] args) {
  9. Cookie x = new ChocolateChip2();
  10. x.show();
  11. }
  12. } /* Output:
  13. Cookie..
  14. *///:~
  15. package access;
  16. import access.cookie2.Cookie;
  17. // ChocolateChip2类在access包中,与Cookie类不在同一个包
  18. public class ChocolateChip2 extends Cookie {
  19. public void show() {
  20. System.out.println("ChocolateChip2..");
  21. }
  22. }

输出的是“Cookie..”,并不是大家认为的“ChocolateChip2..”,大家可能会觉得奇怪,这不应该是后期绑定的方法吗?ChocolateChip2类中的show()方法根本就没有覆盖掉Cookie类中的show()方法!想想之前说过的覆盖,覆盖是面向那些子类可以有权限能访问的父类方法。在运行期,由于引用变量x所指向的对象中的show()方法无法对Cookie类中的show()方法进行覆盖,所以为前期绑定。

总结一下,如果方法被判断出:与将来的具体对象没有任何关联(static)或者引用所指向的对象无法覆盖这个方法(privatefinal和那些子类没有权限访问的父类方法),即在运行时期的调用方式为前期绑定。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注