做百度手机网站优化,优质院校建设网站,徐州网站制作怎样,开发公司安全生产管理制度CONTENTS 1. 方法调用绑定2. 尝试重写Private方法3. 字段访问与静态方法的多态4. 构造器内部的多态方法行为 1. 方法调用绑定
我们首先来看下面这个例子#xff1a;
package com.yyj;enum Tone {LOW, MIDDLE, HIGH;
}class Instrument {public void play(Tone t) {System.ou… CONTENTS 1. 方法调用绑定2. 尝试重写Private方法3. 字段访问与静态方法的多态4. 构造器内部的多态方法行为 1. 方法调用绑定
我们首先来看下面这个例子
package com.yyj;enum Tone {LOW, MIDDLE, HIGH;
}class Instrument {public void play(Tone t) {System.out.println(Instrument.play() t);}
}class Piano extends Instrument {Overridepublic void play(Tone t) {System.out.println(Piano.play() t);}
}class Guitar extends Instrument {Overridepublic void play(Tone t) {System.out.println(Guitar.play() t);}
}public class Music {public static void tune(Instrument i, Tone t) {i.play(t);}public static void main(String[] args) {Piano p new Piano();Guitar g new Guitar();tune(p, Tone.MIDDLE); // 向上转型输出Piano.play() MIDDLEtune(g, Tone.HIGH); // Guitar.play() HIGH}
}在 main() 方法中我们将 Piano 引用传递给了 tune()且不需要任何强制类型转换。这是因为 Instrument 中的接口必定存在于 Piano 中因为 Piano 继承了 Instrument。从 Piano 向上转型到 Instrument 可以“缩小”该接口但不会小于 Instrument 的完整接口。
那么编译器怎么可能知道这个 Instrument 引用在这里指的是 Piano而不是 Guitar为了更深入地了解这个问题有必要研究一下绑定binding这个问题。
将一个方法调用和一个方法体关联起来的动作称为绑定。在程序运行之前执行绑定如果存在编译器和链接器的话由它们来实现称为前期绑定。你之前可能没有听说过这个术语因为在面向过程语言中默认就是前期绑定的。例如在 C 语言中只有一种方法调用那就是前期绑定。
解决这个问题的方案称为后期绑定这意味着绑定发生在运行时并基于对象的类型。后期绑定也称为动态绑定或运行时绑定当一种语言实现后期绑定时必须有某种机制在运行时来确定对象的类型并调用恰当的方法。也就是说编译器仍然不知道对象的类型但方法调用机制能找到并调用正确的方法体。
Java 中的所有方法绑定都是后期绑定除非方法是 static 或 final 的private 方法隐式为 final。这意味着通常不需要你来决定是否要执行后期绑定因为它会自动发生。
2. 尝试重写Private方法
看一下下面这段代码
package com.yyj;public class PrivateOverride {private void f() {System.out.println(Private f());}public static void main(String[] args) {PrivateOverride p new Derived(); // 创建Derived对象p.f(); // Private f()}
}class Derived extends PrivateOverride {public void f() { // 你以为重写了父类中的f()System.out.println(Public f());}
}可能会很自然地认为输出应该为 Public f()但 private 方法自动就是 final 的并且对子类也是隐藏的所以 Derived 的 f() 在这里是一个全新的方法它甚至没有重载因为 f() 的基类版本在 Derived 中是不可见的。
这样的结果就是只有非 private 的方法可以被重写但要注意重写 private 方法的假象它不会产生编译器警告但也不会执行你可能期望的操作如果使用了 Override 注解那么这个问题就会被检测出来。
3. 字段访问与静态方法的多态
现在你可能会开始认为一切都可以多态地发生但是只有普通的方法调用可以是多态的。例如如果直接访问一个字段则该访问会在编译时解析
package com.yyj;class Super {public int x 0;public int getX() { return x; }
}class Sub extends Super {public int x 1;Override public int getX() { return x; }public int getSuperX() { return super.x; }
}public class GetField {public static void main(String[] args) {Super sup new Sub(); // 向上转型System.out.println(sup.x sup.x , sup.getX() sup.getX());Sub sub new Sub();System.out.println(sub.x sub.x , sub.getX() sub.getX() , sub.getSuperX() sub.getSuperX());/** sup.x 0, sup.getX() 1* sub.x 1, sub.getX() 1, sub.getSuperX() 0*/}
}当 Sub 对象向上转型为 Super 引用时任何字段访问都会被编译器解析因此不是多态的。在此示例中Super.x 和 Sub.x 被分配了不同的存储空间因此Sub 实际上包含两个被称为 x 的字段它自己的字段和它从 Super 继承的字段。然而当你在 Sub 中引用 x 时Super 版本并不是默认的那个要获得 Super 的字段必须明确地使用 super.x。
现在我们再来看一下静态方法如果一个方法是静态的那它的行为就不会是多态的因为静态方法与类相关联而不是与单个对象相关联
package com.yyj;class StaticSuper {public static void staticPrint() {System.out.println(Super staticPrint());}public void dynamicPrint() {System.out.println(Super dynamicPrint());}
}class StaticSub extends StaticSuper {public static void staticPrint() {System.out.println(Sub staticPrint());}Overridepublic void dynamicPrint() {System.out.println(Sub dynamicPrint());}
}public class StaticPolymorphism {public static void main(String[] args) {StaticSuper sup new StaticSub(); // 向上转型StaticSub.staticPrint(); // Sub staticPrint()sup.dynamicPrint(); // Sub dynamicPrint()StaticSuper.staticPrint(); // Super staticPrint()}
}4. 构造器内部的多态方法行为
构造器调用的层次结构带来了一个难题对于正在构造的对象如果在构造器中调用它的动态绑定方法会发生什么
在普通方法内部动态绑定调用是在运行时解析的这是因为对象不知道它是属于该方法所在的类还是其子类。如果在构造器内调用动态绑定方法就会用到该方法被重写后的定义。但是这个调用的效果可能相当出乎意料因为这个被重写的方法是在对象即子类对象完全构造之前被调用的因为是从外到内即从基类到子类执行构造器的这可能会带来一些难以发现的错误。如下面这段代码所示
package com.yyj;class A {void f() {System.out.println(A.f());}A() {System.out.println(A() before A.f());f(); // 其实是调用子类重写后的f()System.out.println(A() after A.f());}
}class B extends A {private int x 1; // 子类对象的默认初始值B(int x) {this.x x;System.out.println(B(), x x);}Overridevoid f() {System.out.println(B.f(), x x);}
}public class PolyConstructors {public static void main(String[] args) {new B(5);/** A() before A.f()* B.f(), x 0* A() after A.f()* B(), x 5*/}
}A.f() 是为重写而设计的这个重写发生在 B 中但是在 A 的构造器调用了这个方法而这个调用实际上是对 B.f() 的调用。输出显示当 A 的构造器调用 f() 时B.x 的值甚至不是默认的初始值1而是0。
因此类的完整初始化过程如下
在发生任何其他事情之前为对象分配的存储空间会先被初始化为二进制零。如前面所述的那样调用基类的构造器此时被重写的 f() 方法会被调用是的这发生在 B 构造器被调用之前由于第1步的缘故此时会发现 B.x 值为零。按声明的顺序来初始化成员。执行子类构造器的主体代码。
这样做有一个好处一切至少都会初始化为零或对于特定数据类型来说是任何与零等价的值而不仅仅是被视为垃圾。这包括通过组合嵌入在类中的对象引用这些引用默认为 null。因此如果忘记初始化该引用在运行时就会出现异常。
因此编写构造器时有一个很好的准则用尽可能少的操作使对象逬入正常状态如果可以避免的话请不要调用此类中的任何其他方法。只有基类中的 final 方法可以在构造器中安全调用这也适用于 private 方法它们默认就是 final 的这些方法不能被重写因此不会产生这种令人惊讶的问题。