第六课类与面向对象编程2-2

二、面向对象三大特性

1. 继承

  继承(Inheritance)是面向对象编程的重要特性,就是子类继承父类的特征和和行为,使得子类能够拥有父类的成员变量与方法,还可以在父类基础上添加新成员变量和方法来满足新需求。

  在上面的代码中PlayerBase是父类(parent class),也称为基类(base class)、超类(super class),而JuniorPlayer与SeniorPlayer则是子类(child class),也称为扩展类(subclass)、派生类(derived class)。在JuniorPlayer与SeniorPlayer这两个子类中都继承其父类,即PlayerBase类中的所有成员变量与方法。

修饰符

  虽然子类继承了父类,但并不是在子类中可以访问父类所有的成员,子类只能访问父类中访问修饰符为public或protected的成员变量与方法,修饰符为private的成员为父类私有,不能在子类中进行访问。类成员的访问修饰符有3种选择,private、protected与public。private只能本类中访问,protectd可以在本类与子类中访问,public则不仅可以在本类、子类中访问,还可以类的外部被调用。在上面的代码中isClosed原来是private的,但放在父类PlayerBase中后,为了使子类能够访问到,改为了protected。子类不仅能继承父类的实例成员,也能继承父类的静态成员。在外部调用时,可用子类名加.的方式调用父类的public静态成员。

  如果一个类中有抽象方法,该类必须加上abstract修饰符成为抽象类。加上final修饰符的类不能被继承,其它类都可以被继承。 

子类构造方法

  如果父类中有默认构造方法(没有参数列表的即为默认构造方法,父类没写构造方法,编译时会自动生成一个),子类会继承父类的默认构造方法。父类中只有带参数的构造方法,子类中必须拥有一个自己的构造方法。子类中如果有自己的构造方法,实例化时就只能使用自己的构造函数。在子类构造方法中,可以用super(params…)(括号里的参数根据父类构造方法的规定)来调用父类构造方法。

继承的意义

  继承可以让子类共享父类的代码。看一个子类,应该把它与其父类合到一起才是完整的。

继承的类属关系

  在给有继承关系的类创建对象时,子类可以实例化为父类的对象。因为继承的父类与子类之间是一个从抽象到具体的关系,比如说“动物”是父类,而“猫”、“狗”同为“动物”的子类,而“猫”、“狗”都属于“动物”,因此它们的子类自然可以实例化父类的对象。反之,则父类不能实例化子类的对象,同级的子类不能成为彼此的实例,子类只能作为父类、祖父类的实例,而不能成为“叔父”类、“伯父”类的实例。我们以下图为例:

  在图中,B、C都是A的子类,而D是B的子类,也就是A的孙子类。B类可以实例化为A类的对象,D类可以实例化为B类、A类的对象,B类不能实例化C类的对象,D类也不能实例化为C类的对象。

  为了保证类型转换的安全性,可以使用instanceof运算符。用法如下:

    B b = new B();    A a = null;    if ( b instanceof A ){        a = b;        // ...    }

  子类对象转换为父类对象的类型转换是自动进行的。如果拿到的虽然是父类的对象,但是有把握它其实是子类实例化而成的,再把该对象转换成子类则需要强制类型转换。比如下列代码:

    A a = new B();    B b = null;    if (a instanceof B){        b = (B)a;        // ...    }

重写

  子类对父类的允许访问的方法的实现过程进行重新编写,并不改变返回值和参数列表,称为“重写”(override,又称为覆盖)。父类中所有的protected和public方法都可以在子类中被重写。在我们的代码中父类PlayerBase中的wantMore方法只有声明没有实现,这是一个抽象方法。而抽象方法必须在子类中被重写。但是如果想不允许子类重写方法,在父类中可以给方法加上final修饰符。

  在重写父类方法时,如果该方法在父类中有实现,可以通过super.的方式来调用父类中的实现。比如在父类PlayerBase中getSum方法已经实现了,如果在子类中重写它,可以先用super.getSum()来使用父类中已有的代码,而不需要全部重写。当然super.的方式可以在子类任何地方,用于调用父类的方法。在我们的代码中重写的wantMore方法前加了@Override注解。这个注解并不是必须的,但是推荐大家加上,因为它除了可以标明这是重写方法帮助理解代码之外,还能够让编译器检查父类中是否有同名方法,如果没有则报错,这样就能够避免拼写错误。 

  子类在重写父类方法时不能对父类方法的访问性进行“降级”,比如父类的方法是public时,子类不能重写为protected或private的。相反地,如果是升级则是可以的。这从不同访问修饰符的成员在继承中的可访问性就很好理解。提一个问题:如果父类有一个private方法,在子类中也声明一个完全相同的方法,这并不算是重写,为什么?同学们可以自己思考一下。

  重写的意义在于可以让子类在不改变父类方法声明(返回值、方法名、参数列表)的同时提供不同的具体实现。不改变父类方法声明,外部调用父类实例方法的代码可以保持稳定。在子类中重写父类方法,将变化进行了封装。通过赋给父类变量不同的子类实例,即实现功能的差异。父类可以保持稳定,子类则封装了变化。 

2. 多态

  在我们的代码里,PlayerBase是父类,它的变量用子类来实例化。它的方法wantMore在子类中被重写,通过父类的变量来调用wantMore方法,运行的将是在子类中被重写的代码。这种同类变量调用同名方法时出现的行为差异称为多态,英文Polymorphism。

  我们再看一下上面程序中的测试代码,

    Pokers pokers = new Pokers();    PlayerBase p1 = new JuniorPlayer();    PlayerBase p2 = new SeniorPlayer(pokers);
for (int i=0; i<12; i++){ if (p1.getIsClosed() && p2.getIsClosed()) break;
if(p1.wantMore(p2)) p1.wantPoker(pokers.getNextPoker()); if (p2.wantMore(p1)) p2.wantPoker(pokers.getNextPoker()); }

  我们可以看到p1、p2都是PlayerBase的变量,分别用它的子类JuniorPlayer与SeniorPlayer来实例化。虽然p1与p2是同一类型,但是它们分别调用wantMore方法时的行为是不同的。这种行为的差异不是因为变量的状态不同(即成员变量的值不同)造成的,而是因为调用时实际运行的逻辑代码不同,根源是父类的方法被子类重写了。

  重写引起的多态有非常重要的实用性,它可以让外部调用代码保持稳定。如果要用新的行为代替旧的行为时,只需要给原有类型的变量赋上新的子类的实例。这样由于类的继承机制而获得的代码的可扩展性,带来了多态,也进一步增强了可扩展性。

  父类与子类之间由于重写方法实现了多态。多态还有一种形式是在一个类里实现的,类里存在名称相同,参数列表不同,返回值可以相同也可以不同的情况时,称为重载(英文Overload)。重载可以扩大方法的适用范围,它省去了外部调用方法之前转换数据类型的步骤,而是自动地寻找匹配类型的方法。将类型转换的工作通过重载封装在类的内部也能够提高调用方法的安全性,避免外部类型转换不当引起的错误。

  构造方法的重载很常见,在构造方法中可以通过this(params…)的形式调用其它重载的构造方法。构造方法重载使得对象的创建更灵活,同时也能对成员变量赋初值进行有效控制。如下列代码所示:

public class A{    private int a;    private int b;    public A(){        this(5, 10);    }    public A(int a){        this(a, 10);    }    public A(int a, int b){        this.a = a;        this.b = b;    }}

  在类A中,通过重载提供了3种构造方法,方便外部用户根据实际需要选择。不同的构造方法内部通过调用参数最全的构造方法来复用初始化代码,并对外部调用不赋初值的情况给出安全的默认值。

  重写与重载产生的多态是面向对象编程中有趣而又实用的特性,同学们可以在实际编程中好好地感受。不知道大家看过一部电影《黑客帝国》没有,这是一部十几年前的老电影,它的第二部里有一个非常经典的桥段,就是当剧中人用不同的钥匙开锁时,开门进入的场景是不同的。大家可以看一下这个片断:

当时我看到这个桥段的时候心里马上就想,这不就是多态吗?而且好玩的是多态的两种形式:重写是Override,重载是Overload,而《黑客帝国》2的片名是叫《重装上阵》The Matrix Reloaded,你看Override->Overload->Reloaded,是不是有点意思?

多重继承与多继承

  一个类可以有自己的子类,子类又可以有自己的子类(即孙子类),这样的继承关系叫做多重继承,可以一直延续下去,子子孙孙无穷匮也!

我们可以看一下在前面用到的System.out.println方法。System是java.lang包(这是Java语言最基础的包,其中的类不需要import)中的一个类,out是这个类的一个静态成员变量,它的类型是java.io包下声明的PrintStream类。我们可以在JDK的在线说明文档中查看一下这个类的继承关系:

从上图我们可以看出PrintStream是继承自同一包里的FilterOutputStream类,FilterOutputStream类继承自OutputStream类,而OutputStream类则继承自java.lang.Object类。java.lang包是java.lang是Java语言定义的最基础类所在的包,比如String类就属于这个包。而Object类则是所有类的根类,它是所有类的祖先。如果一个类没有声明其所继承的父类,即默认父类就是Object。我们以后用到的JDK与Android SDK中的很多类都有这样的多重继承关系。

  前面说了,继承机制使得子类可以共享父类的代码,这让我们复用代码时非常省事。那可不可以更进一步,让子类继承多个父类(多继承),这样能够共享更多的代码,岂不美哉?

但是Java语言不说话,默默地给你泼了一盆冷水,它不允许这种多继承的做法发生。这是为什么呢?

  这是因为会发生歧义。前面举了一个例子,“动物”是父类,而“猫”、“狗”同为“动物”的子类。如果允许多继承发生,让一个类既继承猫又继承狗。粗看起来没有问题,细看就问题大了。猫有一个方法是叫,它的实现是“喵喵叫”。狗也有一个方法是叫,它的实现是“汪汪叫”。那么你让同时继承它俩的那个类在继承“叫”这个方法是应该继承“喵喵叫”还是“汪汪叫”呢?这显然就会产生混乱。所以多继承就不被允许了。

  但是在实际中需要一个类组合更多功能,就好比瑞士军刀一样。仅从代码复用的角度来考虑这个问题,解决办法很简单,就是声明一个拥有需要功能的类的成员变量,在本类中声明的方法中通过该成员变量来调用对应方法即可。实际执行的是另一个类的代码,但是外面套一张自己类的方法的皮。

  但只是这样明显不解渴。继承机制除了共享代码之外,还有多态的特性。那么

  阻碍子类多继承的根源是方法的具体实现,同样的方法声明在不同父类中的不同实现会造成选择困难。多态的来源是因为方法声明的稳定,而不是方法的实现。顺着这个思路,如果只让一个父类中有方法的实现,其它父类只声明方法,不实现方法。这样既不再需要子类进行选择,又可以通过方法声明保持外部调用的稳定实现多态。

3. 接口

  有没有只声明而没有实现的方法?在我们写的程序中PlayerBase中有一个抽象方法wantMore。抽象方法就是只有方法声明却没有具体实现,但是抽象方法所在的抽象类还是有其它实现了的方法,如果用抽象类进行多继承,还是有可能造成选择困难。就需要有一种类,它的所有方法都是抽象方法。这种类是存在的,但这样的类就不再是类了,而是被称为接口,英文是Interface。当一个子类继承接口时也不叫继承,而是叫实现(Implement)。

  接口的示例如下:

public interface IA {    void doSthA();    int doSthB(int a, int b);}

  接口中的成员方法默认的修饰符是public abstract。接口因为没有任何实现代码,因此它的存在意义就是被子类继承。接口中不能声明构造方法。【想想看为什么?】下面就是实现该接口的类:

public class CA implements IA {    @Override    public void doSthA() {        ...    }
@Override public int doSthB(int a, int b) { ... }}

  子类只能继承一个父类,可以实现多个接口(以逗号分开):

public class CA extends CBase implements IA, IB {    //...}

  子类在实现接口时,除非是抽象类,否则必须全部实现接口的所有方法,而且必须访问修饰符都是public。实例化时,接口与抽象类一样不能用本类的构造方法进行实例化,必须由子类来实例化。接口与实现它的类之间的类型转换规则与抽象类相同(接口就可以看成一个特殊的抽象类,实现它的类也就是它的子类)。

  如下面代码所示:

    IA a1 = new CA();    CA a2 = new CA();    IA a3 = a2;    CA a4 = null;    if ( a3 instanceof CA ){        a4 = (CA)a3;        ...    }

  接口可以继承接口,同样使用extends关键词。如下面代码所示:

public interface IB extends IA{    int doSthC(int a, int b);}

  通过继承,接口IB中就拥有3个方法。

  接口中可以声明成员变量,但是只能声明为公共静态常量,即修饰符只能是public static final,不加修饰符会默认加上,也不能改成其它修饰符。

4. 引用类型

  回顾前面讲的Java的数据类型,在引用类型下有3种类型,分别是类、接口与数组,如下图所示:

  引用类型又叫复合类型,是由基本类型与其它复合类型组合而成的数据类型。数组是将同类型的数据进行组合,而类中可以组合不同类型的数据,并且类还更进一步将与这些数据关系紧密的功能代码也一并进行封装。接口与类是很相似的,可以说接口就是类的一种极端情况。

  我们已经学完了Java的类型体系,我们回顾一下这张图:


第一季 零基础学习Android开发

第一课 第一个Android程序(1)

第一课 第一个Android程序(2)

第二课 Java语言基础1(1)

第二课 Java语言基础1(2)

第三课 Java语言基础2-1

第三课 Java语言基础2-2

第四课 Java语言基础3-1

第四课 Java语言基础3-2

第五课 类与面向对象编程1


文章转载自微信公众号:跟陶叔学编程

类似文章