带着问题探索多态
package cn.partner4java.test;
public class A {
public A() {
System.out.println("Constructor A");
}
}
package cn.partner4java.test;
public class B extends A {
public B() {
System.out.println("Constructor B");
}
public static void main(String[] args) {
A a = new A();
B b = new B();
// 后台打印:
// Constructor A
// Constructor A
// Constructor B
}
}
package cn.partner4java.test;
/**
* 如果把构造器当做方法,那么B就是重写Override了A的构造器,那么就应该少答应一个“Constructor A”。
* 但是构造器Constructor是否可被override?
* 构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。
* 具体为什么,我们将进行学习。
* @author partner4java
*
*/
public class B extends A {
public B() {
System.out.println("Constructor B");
}
public static void main(String[] args) {
A a = new A();
B b = new B();
// 后台打印:
// Constructor A
// Constructor A
// Constructor B
}
}
如果把构造器当做方法,那么B就是重写Override了A的构造器,那么就应该少答应一个“Constructor A”。
但是构造器Constructor是否可被override?
构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。
具体为什么,我们将进行学习。
在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三个基本特性。
多态的作用则是消除类型之间的耦合关系。
多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要他们都是从同一个基类导出而来的。这种区别是根绝方法行为的区别而表现出来的,虽然这些方法都可以同一个基类来调用。
多态(也成做动态绑定、后期绑定或运行时绑定)。
--------------------------------------------------------------------------------------------------------
再论向上转型
对象既可以作为他自己本身的类型使用,也可以作为他的基类型使用。
把对某个对象的引用视为对其基类型的引用的做法成为:向上转型。
向上转型:
package cn.partner4java.problem;
public enum Note {
MIDDLE_C,C_SHARP,B_FLAT;
}
package cn.partner4java.problem;
public class Instrument {
public void play(Note note){
System.out.println("Instrument play");
}
}
package cn.partner4java.problem;
public class Wind extends Instrument {
@Override
public void play(Note note) {
System.out.println("Wind play " + note);
}
}
package cn.partner4java.problem;
public class Music {
public static void tune(Instrument instrument){
instrument.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind wind = new Wind();
tune(wind);
// 后台打印:
// Wind play MIDDLE_C
}
}
-----------------------------------------------------------------------------------
转机
1、方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。
若在程序执行之前绑定叫做前期绑定。
后期绑定,根绝运行时的对象类型进行绑定,也叫动态绑定或运行绑定。不管怎么样都需要在对象中安置某种“类型信息”。
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
Demo:
package cn.partner4java.problem;
public class Instrument {
public static void play(Note note){
System.out.println("Instrument play");
}
}
package cn.partner4java.problem;
public class Wind extends Instrument {
public static void play(Note note) {
System.out.println("Wind play " + note);
}
}
package cn.partner4java.problem;
public class Music {
public static void tune(Instrument instrument){
instrument.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind wind = new Wind();
tune(wind);
// 后台打印:
// Instrument play
}
}
final比较重要的一定就是可以“关闭”动态绑定,告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。但是,通常对性能不会有什么改观。
2、产生正确的行为
经典的“几何形状”的例子。
package cn.partner4java.shape;
/**
* 基类Shape,几何形状
* @author partner4java
*
*/
public class Shape {
public void draw() {
}
public void erase() {
}
}
package cn.partner4java.shape;
/**
* 圆“是一种几何形状”
* @author partner4java
*
*/
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Circle.draw()");
}
@Override
public void erase() {
System.out.println("Circle.erase");
}
}
package cn.partner4java.shape;
/**
* 方“是一种几何形状”
* @author partner4java
*
*/
public class Square extends Shape {
@Override
public void draw() {
System.out.println("Square.draw()");
}
@Override
public void erase() {
System.out.println("Square.erase");
}
}
package cn.partner4java.shape;
import java.util.Random;
public class RandomShapeGenerator {
private Random random = new Random(47);
public Shape next(){
switch (random.nextInt(2)) {
default:
case 0: return new Circle();
case 1: return new Square();
}
}
}
3、可扩展性
多态是一项让程序员“将改变的事务与未变的事务分离开来”的重要技术。
4、缺陷:“覆盖”私有方法
只要非private的方法才可以被覆盖;但是还需要密切注意覆盖private方法的现象,这时虽然编译器不会报错,但是也不会按照我们所希望的来执行。确切的说,在导出类中,对于基类的private方法,最好采用不同的名字。
5、缺陷:域与静态方法
package cn.partner4java.field;
public class Super {
public int field = 0;
public int getField() {
return field;
}
}
package cn.partner4java.field;
public class Sub extends Super {
public int field = 1;
public int getField() {
return field;
}
public int getSuperField(){
return super.field;
}
}
package cn.partner4java.field;
/**
* 当Sub对象转型为Super引用时,任何域访问操作都让由编译器解析,因此不是多态的。
* @author partner4java
*
*/
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub();
System.out.println("sup.field = " + sup.field +
" ,sup.getField = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub,field = " + sub.field +
" ,sub.getField = " + sub.getField() + ",sub.getSuperField = " +
sub.getSuperField());
// 后台打印:
// sup.field = 0 ,sup.getField = 1
// sub,field = 1 ,sub.getField = 1,sub.getSuperField = 0
}
}
如果某个方法是静态的,他的行为就不具有多态性:
package cn.partner4java.staticmethod;
public class StaticMethodSuper {
public static void staticMehtod(){
System.out.println("StaticMethodSuper.staticMehtod");
}
public void method(){
System.out.println("StaticMethodSuper.method");
}
}
package cn.partner4java.staticmethod;
/**
* 如果某个方法是静态的,他的行为就不具有多态性:
* @author partner4java
*
*/
public class StaticMethod extends StaticMethodSuper {
public static void staticMehtod(){
System.out.println("StaticMethod.staticMehtod");
}
public void method(){
System.out.println("StaticMethod.method");
}
public static void main(String[] args) {
StaticMethodSuper method = new StaticMethod();
method.staticMehtod();
method.method();
// 后台打印:
// StaticMethodSuper.staticMehtod
// StaticMethod.method
}
}
-------------------------------------------------------------------------------
构造器和多态
构造器并不具有多态性(他们实际上是static方法,只不过该static声明是隐式的)
1、构造器的调用顺序
基类的构造器总是导出类的构造过程中被调用,而且按照继承层次逐渐向上连接,以便基类的每个构造都能得到调用。
这样做是有意义的,因为构造器具有一项特殊任务:检查对象是否被正确的构造。
导出类只能访问它自己的成员,不能访问基类的成员(基类成员通常是private类型)。
只要基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。
因此,必须另所以构造器都得到调用,否则就不可能正确构造完整对象。
这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因。
在导出来的构造器主题中,如果没有明确指定调用某个基类构造器,他就会默默调用默认构造器。
如果不存在默认构造器,编译器就会报错。
package cn.partner4java.test;
public class A {
public A() {
System.out.println("Constructor A");
}
public A(String name){
System.out.println("Constructor A " + name);
}
}
package cn.partner4java.test;
/**
* 如果把构造器当做方法,那么B就是重写Override了A的构造器,那么就应该少答应一个“Constructor A”。
* 但是构造器Constructor是否可被override?
* 构造器Constructor不能被继承,因此不能重写Override,但可以被重载Overload。
* @author partner4java
*
*/
public class B extends A {
public B() {
System.out.println("Constructor B");
}
public B(String name){
System.out.println("Constructor B " + name);
}
public static void main(String[] args) {
A a = new A();
System.out.println("-----");
A a2 = new B();
System.out.println("-----------");
B b = new B();
System.out.println("-----");
A a3 = new B("Hello");
System.out.println("-----------");
B b2 = new B("Hello");
// 后台打印:
// Constructor A
// -----
// Constructor A
// Constructor B
// -----------
// Constructor A
// Constructor B
// -----
// Constructor A
// Constructor B Hello
// -----------
// Constructor A
// Constructor B Hello
}
}
上面的打印还表明了一个观点,父类或者说基类也就是A必须有无参的构造器或者默认构造器(如果没有默认构造器,那么子类的构造器就需要指明需要调用父类的哪个构造器,不然,会默认子类构造器第一行加一个父类无参的构造器,但是父类不存在的话,就会出现错误--Implicit super constructor A() is undefined. Must explicitly invoke another constructor)。
调用构造器遵循的顺序:
1、调用基类的构造器。这个步骤会不断的反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直至最底层的导出类。
2、按声明顺序调用成员的初始化方法。
3、调用导出类构造器的主体。
demo如下:
package cn.partner4java.order;
/**
* 膳食
* @author partner4java
*
*/
public class Meal {
public Meal() {
System.out.println("Meal()");
}
}
package cn.partner4java.order;
/**
* 面包
* @author partner4java
*
*/
public class Bread {
public Bread() {
System.out.println("Bread()");
}
}
package cn.partner4java.order;
/**
* 奶酪
* @author partner4java
*
*/
public class Cheese {
public Cheese() {
System.out.println("Cheese()");
}
}
package cn.partner4java.order;
/**
* 生菜
* @author partner4java
*
*/
public class Lettuce {
public Lettuce() {
System.out.println("Lettuce()");
}
}
package cn.partner4java.order;
/**
* 午餐
* @author partner4java
*
*/
public class Lunch extends Meal {
public Lunch() {
System.out.println("Lunch()");
}
}
package cn.partner4java.order;
/**
* 便携午餐
* @author partner4java
*
*/
public class PortableLunch extends Lunch {
public PortableLunch() {
System.out.println("PortableLunch()");
}
}
package cn.partner4java.order;
/**
* 三明治
* @author partner4java
*
*/
public class Sandwich extends PortableLunch {
private Bread bread = new Bread();
private Cheese cheese = new Cheese();
private Lettuce lettuce = new Lettuce();
public Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
}
//后台打印:
//Meal()
//Lunch()
//PortableLunch()
//Bread()
//Cheese()
//Lettuce()
//Sandwich()
2、继承和清理
通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器处理。如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法。
并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理工作,就必须在导出类中覆盖dispose()方法。
当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose();否则,基类的清理动作就不会发生。
3、构造器内部的多态方法的行为
package cn.partner4java.inner;
public class Glyph {
void draw(){
System.out.println("Glyph.draw()");
}
public Glyph() {
System.out.println("Glyph before draw()");
draw();
System.out.println("Glyph after draw()");
}
}
package cn.partner4java.inner;
public class RoundGlyph extends Glyph {
private int radius = 1;
public RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph ,radius = " + radius);
}
void draw(){
System.out.println("RoundGlyph.draw(),radius = " + radius);
}
public static void main(String[] args) {
new RoundGlyph(5);
}
}
//后台打印:
//Glyph before draw()
//RoundGlyph.draw(),radius = 0
//Glyph after draw()
//RoundGlyph ,radius = 5
初始化的实际过程是:
1、在其他任务发生之前,将分配给对象的存储空间初始化二进制的零。
2、如前所述那样调用构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0。
3、按照声明的顺序调用成员的初始化方法。
4、调用导出类的构造器主体。
因此,编写构造器有一条有效的准则:“就尽可能简单的方法使对象进入正常状态;如果可以的话,避免其他方法”。
在构造器内唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,他们自动属于final方法)。
这些方法不能被覆盖,因此也就不会出现上面这种情况。
-----------------------------------------------------------------------------------------
协变返回类型
package cn.partner4java.change;
public class Grain {
@Override
public String toString() {
return "Grain";
}
}
package cn.partner4java.change;
public class Wheat extends Grain {
@Override
public String toString() {
return "Wheat";
}
}
package cn.partner4java.change;
public class Mill {
Grain process(){
return new Grain();
}
}
package cn.partner4java.change;
public class WheatMill extends Mill{
Wheat process(){
return new Wheat();
}
}
package cn.partner4java.change;
/**
* Java SE5与Java较早版本之间的主要差异就是:
* 较早的版本强制process()的覆盖版本必须返回Grain,而不能返回Wheat,
* 尽管Wheat是从Grain导出来的,因而也应该是一种合法的返回类型。
* 协变返回类型允许返回更具体的Wheat类型。
* @author partner4java
*
*/
public class CovariantReturn {
public static void main(String[] args) {
Mill mill = new Mill();
Grain grain = mill.process();
System.out.println(grain);
mill = new WheatMill();
grain = mill.process();
System.out.println(grain);
}
}
//后台打印:
//Grain
//Wheat
---------------------------------------------------------------------
用继承进行设计
准则:用继承表达行为间的差异,并用字段表达状态上的变化。
向上转型的安全和向下转型的类型转换异常。