多态
多态释义:
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论
在最初创建项目时还是在添加新特性时都可以“生长”的程序。
多个派生类可以被当做一个基类的类型来处理,但是本身派生类的方法又表现出和基类方法的不同行为以此消除类型之间的耦合。
但是这样会带来一个问题,那就是具体类型信息是程序运行时才确定的,而非编译期。
动态绑定
动态绑定(也称为后期绑定,运行时绑定),大白话就是具体类型确定的的时间。如下例子:
package polymorphism.shape;
import java.util.*;
//例子来自于 《on java 8》
public class RandomShapes {
private Random rand = new Random(47);
public Shape get() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public Shape[] array(int sz) {
Shape[] shapes = new Shape[sz];
// Fill up the array with shapes:
for (int i = 0; i < shapes.length; i++) {
shapes[i] = get();
}
return shapes;
}
}
从上述例子可以看出 RandomShapes的get方法随机返回一个Shape类型的派生类,然后放进集合中。虽然我们知道集合类型必定是Shape[]但是具体是 Circle还是Square要在get方法执行后才能知道。编译期不会去执行get方法,只会将其翻译成对应的字节码,所以只有等到运行期才能知道具体类型,因此可以理解为这是动态的。
动态性就能保证灵活性,如果在编译器就要确定类型的话,确实可以提升效率。但是考虑一下,如果每个Shape的派生类都继承实现了draw()方法。此时一个方法要调用派生类的draw()方法,动态性可以让我们根据传入的派生类去调用具体派生类的draw()。如果编译器要确定恐怕你就要为每个派生类编写不同参数类型的方法,而且一旦有新的派生类,你就要添加新的方法。但是动态绑定就可以让你有新的派生类的时候不用再编写代码。
注意:
如果基类方法被指定为private ,final或static则不具有多态性。final 方法已经指定了不可重写,而private实际为隐式的final。即使子类有同名的方法,那也只是同名的不同的方法而已,而static则是和类绑定与具体对象无关。
构造器和多态
构造方法会被看做隐式的静态方法,因此构造方法也不具有多态性。
构造器调用顺序
看下例:
// 示例来自 《on java 8》
class Meal {
Meal() {
System.out.println("Meal()");
}
}
class Bread {
Bread() {
System.out.println("Bread()");
}
}
class Cheese {
Cheese() {
System.out.println("Cheese()");
}
}
class Lettuce {
Lettuce() {
System.out.println("Lettuce()");
}
}
class Lunch extends Meal {
Lunch() {
System.out.println("Lunch()");
}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
}
输出:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
构造器调用顺序如下:
- 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
- 按声明顺序初始化成员。
- 调用派生类构造器的方法体。
为什么是这个顺序呢?
个人理解,当使用继承的时候就意味着派生类已经知道基类的一切。从向上转型是安全的可以看出,因为派生类拥有基类的全部信息,才能保证向上转型的安全。
所以理所应当在继承是应当知道基类的所有信息,而构造器可能包含基类成员的初始化。如果基类成员没有初始化,派生类就可能不知道基类全部信息。
使用继承设计
其实还是讨论继承和组合的选择。继承要求必须在编译时知道确切类型,其实这就已经丧失了灵活性。
但是组合可以灵活动态的选择类型。
如下例:
class Actor {
public void act() {}
}
class HappyActor extends Actor {
@Override
public void act() {
System.out.println("HappyActor");
}
}
class SadActor extends Actor {
@Override
public void act() {
System.out.println("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();
}
}
输出:
HappyActor
SadActor
Stage 对象中包含了 Actor 引用,该引用被初始化为指向一个 HappyActor 对象,这意味着 performPlay()
会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对 SadActor 的引用,performPlay()
的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式)。与之相反,我们不能在运行时决定继承不同的对象,那在编译时就完全确定下来了。(引自 原文)
有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承得到的两个不同类在 act() 方法中表达了
不同的行为,Stage 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。
___ on java 8
小结
为了在程序中有效地使用多态乃至面向对象的技术,就必须扩展自己的编程视野,不能只看到单一类中的成员和消息,而要看到类之间的共同特性和它们之间的关系。尽管这需要很大的努力,但是这么做是值得的。它能带来更快的程序开发、更好的代码组织、扩展性更好的程序和更易维护的代码。
随笔
我们都对未来抱着美好的期望,可是因为害怕得不到后失落,总是会降低期望然后对别人说自己自己不报期望。然后心里默默期待美好的出现。