Java编程思想学习笔记(8)
Java多态
多态通过分离做什么和怎么做,从另一个角度将接口和实现分离开来。
同时多态也是面向对象设计中,继抽象以及继承之后的第三大特性。
封装,是合并属性和行为创建一种新的数据类型,继承是建立数据类型之间的某种关系(is-a),而多态就是这种关系在实际场景的运用。
多态就是把做什么和怎么做分开了;其中,做什么是指调用的哪个方法,我是去吃饭(方法a)还是去睡觉(方法b),怎么做是指实现方案,如果我选择吃饭,那么我是吃米饭还是吃面条,”分开了“则是指两件事不在同一时间确定。
向上转型
对象既可以作为它本身的类型使用,也可以作为它的基类型使用,而这种把对某个对象的引用视为对其基类型的引用的做法就是向上转型。
例子:
public enum Note {
// 演奏乐符
MIDDLE_C, C_SHARP, B_FLAT;
}
public class Instrument {
// 乐器基类
public void play(Note n) {
print("Instrument.play()");
}
}
public class Wind extends Instrument{
// Wind是一个具体的乐器
// Redefine interface method:
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
public class Music {
// 乐器进行演奏
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // 向上转型
}
}
Nusic,tune()接受一个Instrument引用,同时也接受任何到出自Instrument的类,在main()中,当一个Wind引用传递到tune时,不需要任何类型转换,这是因为Wind从Instrument继承而来。
直接用对应的对象类型
在上面例子中,如果让tune方法接受一个Wind引用作为自己的参数,似乎看起来更为直观,但是会引发一个问题:这个时候你就需要为系统中Instrument的每种类型都编写一个新的tune方法。
例子:
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
}
class Brass extends Instrument {
public void play(Note n) {
print("Brass.play() " + n);
}
}
public class Music2 {
public static void tune(Wind i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Stringed i) {
i.play(Note.MIDDLE_C);
}
public static void tune(Brass i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(flute); // No upcasting
tune(violin);
tune(frenchHorn);
}
}
可以看出,这么做的话你就需要更多的编程,每次添加类似tune方法的时候你都需要做大量的工作。
所以我们只写一个简单的方法,仅仅接收基类作为参数,而不是特殊的导出类,这么做情况不是变得更好吗。
深入理解
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
在上面这个方法中,它接收一个Instrument引用,那么在这种情况下,编译器怎么样才能知道这个instrument引用指向的是Wind对象呢?
实际上,编译器无法得知。
绑定
将一个方法调用同一个方法主体关联起来称为绑定
若在程序执行前进行绑定,就是前期绑定,比如C语言就只有一种方法调用,就是前期绑定。
而在运行时根据对象的类型进行绑定就是后期绑定,也叫做动态绑定或者运行时绑定。
Java中除了static方法和final方法之外,其它所有方法都是后期绑定,这意味着通常情况下,我们不必判定是否应该进行后期绑定---它会自动发生。
经典例子
在面向对象程序设计中,有一个经典例子就是“几何形状”,在这个例子中,有一个基类Shape,以及多个导出类,Circle,Square,Triangle。
继承图:
想要向上转型,只需:
Shape s = new Circle();
这里创建了一个Circle对象,并把得到的引用赋值给Shape,这样看似错误,但实际是OK的,因为通过继承,Circle就是一种Shape。
例子:
public class Shape {
public void draw() {}
public void erase() {}
}
public class Circle extends Shape {
public void draw() { print("Circle.draw()"); }
public void erase() { print("Circle.erase()"); }
}
public class Square extends Shape {
public void draw() { print("Square.draw()"); }
public void erase() { print("Square.erase()"); }
}
public class Triangle extends Shape {
public void draw() { print("Triangle.draw()"); }
public void erase() { print("Triangle.erase()"); }
}
public class RandomShapeGenerator {
private Random rand = new Random(47);
// 向上转型是在return语句发生的,每个return语句获得一个指向具体图形的引用
// 并将其以Shape类型从next方法中发生出去
// 所以无论什么时候调用next方法,我们都不可能知道具体类型到底是什么
// 因为我们获取的是一个通用的Shape引用
public Shape next() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
}
public class Shapes {
private static RandomShapeGenerator gen =
new RandomShapeGenerator();
public static void main(String[] args) {
Shape[] s = new Shape[9];
// Fill up the array with shapes:
for(int i = 0; i < s.length; i++)
s[i] = gen.next();
// Make polymorphic method calls:
for(Shape shp : s)
shp.draw();
}
}
在main方法中,我们只知道拥有一些Shape,除此之外不会知道其他更多的了。当我们遍历数组,调用draw方法时,与类型有关的特定行为就会自动正确发生。
上面这个例子说明,在编译时,编译器不需要获得任何特殊信息就能进行正确的调用,对draw方法的所有调用都是通过动态绑定进行的。
扩展性
例子:
class Instrument {
void play(Note n) { print("Instrument.play() " + n); }
String what() { return "Instrument"; }
void adjust() { print("Adjusting Instrument"); }
}
class Wind extends Instrument {
void play(Note n) { print("Wind.play() " + n); }
String what() { return "Wind"; }
void adjust() { print("Adjusting Wind"); }
}
class Percussion extends Instrument {
void play(Note n) { print("Percussion.play() " + n); }
String what() { return "Percussion"; }
void adjust() { print("Adjusting Percussion"); }
}
class Stringed extends Instrument {
void play(Note n) { print("Stringed.play() " + n); }
String what() { return "Stringed"; }
void adjust() { print("Adjusting Stringed"); }
}
class Brass extends Wind {
void play(Note n) { print("Brass.play() " + n); }
void adjust() { print("Adjusting Brass"); }
}
class Woodwind extends Wind {
void play(Note n) { print("Woodwind.play() " + n); }
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn’t care about type, so new types
// added to the system still work right:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}
为乐器系统添加更多的类型,而不用改动tune方法。tune方法完全可以忽略它周围代码所发生的全部变化,依旧正常运行,
“覆盖”私有方法
下面这个例子:
public class PrivateOverride {
private void f() { print("private f()"); }
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() { print("public f()"); }
}
期望输出的是public f(),但是private方法被自动修饰为final,而且对导出类是屏蔽的,所以在Derived类中的f方法是一个全新的方法。既然基类中的f方法在在子类Derived中不可见,那么也不能被重载。
域与静态方法
例子:
public class Super {
public int field = 0;
public int getField() { return field; }
}
public class Sub extends Super{
public int field = 1;
public int getField() { return field; }
public int getSuperField() { return super.field; }
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
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());
}
}
上面这个例子中,当Sub对象转型为Super引用时,任何域的访问操作都是由编译器解析的,因此不是多态的。
如果某个方法是静态的,那么它就不具有多态性
public class StaticSuper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
public class StaticSub extends StaticSuper{
public static String staticGet() {
return "Derived staticGet()";
}
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSuper sup = new StaticSub(); // Upcast
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}
构造器与多态
通常,构造器不同于其它方法,涉及到多态时也是如此。构造器是不具有多态性的
构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接。使得每个基类的构造器都能得到调用。
例子:
public class Meal {
Meal() { print("Meal()"); }
}
public class Cheese {
Cheese() { print("Cheese()"); }
}
public class Lettuce {
Lettuce() { print("Lettuce()"); }
}
public class Bread {
Bread() { print("Bread()"); }
}
public class Lunch extends Meal{
Lunch() { print("Lunch()"); }
}
public class PortableLunch extends Lunch{
PortableLunch() { print("PortableLunch()");}
}
public class Sandwich extends PortableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() { print("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
}
例子中最重要的是Sandwich,它反映了三层继承,以及三个成员对象。
可以看到复杂对象调用构造器要遵照下面的顺序:
1 调用基类构造器,不断反复递归下去
2 按照声明顺序调用成员的初始化方法
3 调用导出类构造器的主体
构造器内部的多态方法的行为
构造器调用的层次结构带来一个问题:如果在一个构造器内部调用正在构造的对象的某个动态绑定方法,会发生什么?
例子:
public class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
public class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
输出:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
在Glyph的构造器中,我们调用了draw方法,因为这个是动态绑定方法的缘故,我们就会调用导出类RoundGlyph中的draw方法,但是这个方法操纵的成员radius还没初始化,所以就体现出问题了,结果中第一次输出radius为0。
所以初始化的实际过程是:
1 在其他任何事物之前,将分配给对象的存储空间初始化成二进制的零
2 如前所述调用基类构造器
3 按照声明的顺序调用成员的初始化方法
- 4 调用导出类的构造器主体
协变返回类型
在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更 "狭窄" 的类型。
Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型。
例子:
import java.io.ByteArrayInputStream;
import java.io.InputStream;
class Base
{
//子类Derive将重写此方法,将返回类型设置为InputStream的子类
public InputStream getInput()
{
return System.in;
}
}
public class Derive extends Base
{
@Override
public ByteArrayInputStream getInput()
{
return new ByteArrayInputStream(new byte[1024]);
}
public static void main(String[] args)
{
Derive d=new Derive();
System.out.println(d.getInput().getClass());
}
}
/*程序输出:
class java.io.ByteArrayInputStream
*/