封装、继承、多态是面向对象语言的三大特性。多态本质上是将接口与实现细节分离,在具体的程序设计中是实现高扩展性的关键。特别是一些框架类型的项目,框架定义接口并提供默认的接口实现,开发者也可以提供自己的接口实现,从而实现框架的高可扩展性。
向上转型(upcasting),就是将派生类当成基类使用,这个理论上很容易理解。因为基类拥有的方法、成员,派生类一定也有,派生类中的方法、成员只可能比基类多不可能比基类少,因此把派生类当成基类用完全行得通,向上转换也是安全的。
class Instrument {
void play(String song) {
System.out.println("Instrument.play():" + song);
}
}
class Wind extends Instrument {
void play(String song) {
System.out.println("Wind.play():" + song);
}
void sing(String song) {
System.out.println("Wind.sing():" + song);
}
}
class Brass extends Instrument {
void play(String song) {
System.out.println("Brass.play():" + song);
}
void sing(String song) {
System.out.println("Brass.sing():" + song);
}
}
public class Music {
static void tune(Instrument i, String song) {
i.play(song);
// 不可以,Instrument类没有这个方法
// i.sing(song);
}
public static void main(String[] args) {
Instrument ins;
if ("wind".equals(args[0])) {
ins = new Wind();
} else if ("brass".equals(args[0])) {
ins = new Brass();
} else {
System.out.println("args error");
return;
}
tune(ins, "hello");
}
Instrument类是所有乐器类的父类,在这里相当于接口,它有一个paly()方法。Wind与Brass是Instrument类的子类,在这里可以看成是具体实现,分别重新实现了play()方法,并且每个乐器类都有自己新增加的方法sing()。
Music类中的静态方法tune接受一人Instrument类型的句柄与一个代表具体曲目的字符串,然后调用Instrument的play()方法演奏曲目。
在main方法中,根据传入的参数,将Instrument类型的句柄指向一个Wind实例或者Brass实例。这个时候就发生了向上转型,无论是Wind或者Brass实例,都被当成Instrument类型使用,刚才说过了,这是安全的,没有问题的。
Music类中的tune方法实现了多态,当ins句柄指向Wind时,它就调用Wind实例的play()方法,同理当ins指向Brass时,就调用Brass实例的play()方法。
因此,当想新增加一种Instrument的实现,比如Stringed时,只需要增加Stringed类,它从Instrument继承而来,并在main函数中增加一个分支就可以。原来的Instrument接口,Wind、Brass类的实现,以及Music中的tune()方法都不需要修改,这样很方便的就实现了扩展。
在上例中,main方法中具体为ins创建那种类型的实例是由传入main方法的参数决定的。而在Spring等框架中,有更好的实现方式,它把创建实例的相关信息保存在文件中,一般是xml文件,然后通过处理这个文件决定创建那种类型的实例,典型代码如下:
public static void main(String[] args) {
// 创建那种类型的实例,相关配置保存在applicationContext.xml配置文件中
// res实例从配置文件读入配置并保存
ClassPathResource res = new ClassPathResource("applicationContext.xml");
// 基于res代表的配置,创建工厂类实例
XmlBeanFactory factory = new XmlBeanFactory(res);
// 通过工厂为ins创建实例,后边的字符串"instrument"字符串可能代表了Wind,
// 也可能代表Brass或者任何Instrument的实现,取决于配置。
Instrument ins = (Instrument)factory.getBean("instrument");
tune(ins, "hello");
}
可以看到,在Spring框架中,具体创建那种类型的乐器,由写在代码中变成写在配置文件中,这就是所谓的IOC控制反转机制。当然Spring的配置文件有点复杂,因为一个对象可能是由多种对象组合而成的,这个涉及到依赖管理。
当然现在来看,Music中tune()方法的实现也产生了一些问题,通过向上转型,比如将Wind转换成Instrument,实际上缩小了Wind的接口,在tune()方法中只能调用play()方法而不能调用sing()方法。
在上例中已经实现了多态,这里看一下Java中实现多态的机制。首先涉及到一个概念“绑定”,“绑定”的意思是将方法名与方法的具体实现连接起来。它有两种,一种是编译期绑定也称为早期绑定。在这种方式中,编译器看到一个方法名,它就知道这个方法对应的具体实现是什么,从而实现绑定。
但是在上例中的Music类中的tune方法中,编译器并不知道为方法名paly()绑定那个具体的实现,可能是Wind类中的play,也可能是Brass类中的paly,这个要等到程序运行以后才能确定,这个就是运行期绑定也就后期绑定。
在Java中,当方法用final修饰时,代表这个方法不可以被修改,它只能有一种实现,不可能有其它实现,这种方法就用编译期绑定。除此之外的其它方法都用运行期绑定,即使这个方法目前只有一种实现。很显然运行期绑定的效率相对于编译期绑定要差一些,这也是使用final关键字的原因之一。
我们知道在C++中,实现上每个对象实例都内置了一个v-table表,用这个表来记录方法名到底代表那种实现,Java中可能也采用了类似的机制。总之在运行期,在调用方法的时候,多了一个查表的过程,因此才说运行期绑定的开销要大一些。
覆盖与重载是两个不同的概念,覆盖代表将父类中的某个方法重新实现,父类中原来的实现会失效,当然在子类中方法的名称与参数列表要与父类中的完全一样。
而重载的意思是方法名称一样,但参数列表不一样,实际上是不同的方法。因此,在子类中有可能将覆盖与重载搞混,而编译器并不会提示这种问题,因为无论覆盖与重载都是合法的。将最开始的示例代码改一下:
class Instrument {
void play(String song) {
System.out.println("Instrument.play():" + song);
}
}
class Wind extends Instrument {
// 这是对Instrument父类中play方法的“覆盖”,因为方法名与参数列表完全一致,
// Instrument中play()方法的实现已经无效
void play(String song) {
System.out.println("Wind.play():" + song);
}
// 这个是在子类中新增加的方法。
void sing(String song) {
System.out.println("Wind.sing():" + song);
}
}
class Brass extends Instrument {
// 这个是对Instrument父类中paly方法的重载,方法名一样,但参数列表不一样,Instrument中的play()方法仍然有效
void play(int song) {
System.out.println("Brass.play():" + song);
}
// 这个是在子类中新增加的方法。
void sing(String song) {
System.out.println("Brass.sing():" + song);
}
}
public class Music {
static void tune(Instrument i, String song) {
i.play(song);
// 不可以,Instrument类没有这个方法
// i.sing(song);
}
public static void main(String[] args) {
Instrument ins = new Wind();
// 这个会调用Wind中play()方法的实现
ins.play("hello");
ins = new Brass();
// 这个会调用Instrument中play()的实现,注意传入的参数类型是字符串,
ins.play("Hello");
}
}
在Brass中,有可能原始的意思是覆盖Instrument中的play()方法,但可能由于粗心,Brass中的play()方法与Instrument中的play方法名称当然一样,参数个数也一样,但参数类型有区别,Brass是int但Instrument中String,此时“覆盖”就变成了重载,而编译器也不会提示错误。
在main方法中,当ins指向Brass类实例时,调用ins.play("hello")实际调用的是Instrument中play的实现而不是Brass中play的实现,这可能与预期就不符合了。当然这个问题只要稍加注意就可以了。
在上边的例子中,Instrument是一个普通类。但我们真正的意图其实是在Instrument中定义接口,并不是提供具体实现。然后让派生类实现接口,从而达到接口的定义与具体的实现细节分离的目的。
另外因为Instrument的意图只是定义接口,所以它应该可以不涉及具体实现,或者即使有具体实现,也不会实现什么逻辑。因此,应该想办法阻止用户实例化Instrument类型的实例。
Java中通过提供abstract关键字可以实现这种意图。
abstract关键字可以修饰方法,如下所示:
abstract class Instrument {
abstract void play(String song);
void sing (String song) {
System.out.println("Instrument.sing():" + song);
}
}
上例中,paly()方法加了abstract修饰符,它就变成抽象方法了,抽象方法不可以有实现,否则编译器会提示错误。可以看到新加了一人sing方法,它是普通方法,因此它可以有具体的实现。也就是说抽象方法与普通方法可以在一个类定义中共存。
只要类中有一个方法中抽象方法,那么这个类一定要是抽象类,也就是要在类定义时在前边加上abstract,否则编译报错。这样Instrument类就由变通类变成抽象类,而抽象类是不允许实例化的,如Instrument ins = new Instrument();,编译器不允许这样做,当然这也正是抽象类的意图,在编译阶段就阻止用户实例化没有意义的抽象类。
接下来是派生类,子类一定要实现(或者说覆盖)父类中所有的抽象方法,只要有一个没有实现,那么子类也会是抽象类,也需要在类定义时加上abstract关键字,也不能实例化。示例:
abstract class Instrument {
abstract void play(String song);
void sing (String song) {
System.out.println("Instrument.sing():" + song);
}
}
class Wind extends Instrument {
// 实现了父类中唯一的抽象方法,Wind是普通类
void play(String song) {
System.out.println("Wind.play():" + song);
}
// 覆盖父类中的sing方法
void sing(String song) {
System.out.println("Wind.sing():" + song);
super.sing(song);
}
}
abstract class Brass extends Instrument {
// 虽然与父类中的抽象方法play同名,但参数列表不一样,是重载不是覆盖,因此Brass类仍然是抽象类,不能实例化
void play(int song) {
System.out.println("Brass.play():" + song);
}
void sing(String song) {
System.out.println("Brass.sing():" + song);
}
}