《Java8学习笔记》读书笔记(6)

第6章 继承与多态

学习目标
  • 了解继承的目的
  • 了解继承与多态的关系
  • 知道如何重写方法
  • 认识java.lang.Object
  • 简介垃圾回收机制
6.1 何谓继承

面向对象中,子类继承父类,就拥有了父类的所有非私有属性和方法,这是为了避免重复的写相同的代码。这在当时可以说是一件创举,因为它大大提高了代码的可维护和可扩展的能力,但是站在今天的角度,它也带来了内存的无谓浪费与性能的下降等诸多的问题。如何正确判断使用继承的时机,以及继承之后如何活用多态,才是学习继承的重点。

6.1.1 继承共同的行为

要说明继承,最好是举个例子来说明,其中RPG游戏是最容易来说明问题的。
我们现在需要设定一个战士类和一个魔法师类:
先写个战士类:

public class Fighter{
    private String name;//名称
    private int level;//等级
    private int hp;//血量
    private int mp;//魔法值
     //战斗方法
    public void fight(){
        System.out.println("战士拨出了宝剑!");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getHp() {
        return hp;
    }

    public void setHp(int hp) {
        this.hp = hp;
    }

    public int getMp() {
        return mp;
    }

    public void setMp(int mp) {
        this.mp = mp;
    }

}

再来一个魔法师类:

public class Fighter{
    private String name;//名称
    private int level;//等级
    private int hp;//血量
    private int mp;//魔法值
    //战斗方法
    public void fight(){
        System.out.println("魔法师挥动了他的魔杖!");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getHp() {
        return hp;
    }

    public void setHp(int hp) {
        this.hp = hp;
    }

    public int getMp() {
        return mp;
    }

    public void setMp(int mp) {
        this.mp = mp;
    }

}

等会儿,我又有不好的感觉了,这两类的成员变量都是一样的,代码又是重复的!
我们可以仔细想想,其实战士或者魔法师,它们都是游戏中的一个”角色”,所以我们可以写一个父类Role(角色),放所有相同的部分都放到里面,然后再用子类Fighter和Magic继承Role,子类里不用写一句代码,就继承了父类里的成员变量和方法了。象这样:

/**
*父类:角色
*/
public class Role{
    private String name;//名称
    private int level;//等级
    private int hp;//血量
    private int mp;//魔法值

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    public int getHp() {
        return hp;
    }

    public void setHp(int hp) {
        this.hp = hp;
    }

    public int getMp() {
        return mp;
    }

    public void setMp(int mp) {
        this.mp = mp;
    }
}

记住,父类Role仅仅把子类中共同的部分移了进来。
然后是子类Fighter,它要继承Role:

public class Fighter extends Role{
    public void fight(){
        System.out.println("战士拨出了宝剑!");
    }
}

这里有一个关键字:extends,这表示Fighter会扩展Role的代码,意思就是首先Fighter获得了Role的非私有代码,同时Fighter还可以添加新的代码(比如fight())。
魔法师类也是一样的:

public class Magic extends Role{
    public void fight(){
        System.out.println("魔法师挥动着他的魔杖!");
    }
}

Magic同样继承了Role的代码,同时添加了新的fight()方法。

说明:在图6.1中,为UML的类图,每个框都有三格,最上格为类名;中间格为属性名,前面的减号代表private。冒号(:)后面是数据类型的名称;最下格为方法名称,加号代表public。空心的三角代表继承关系,三角指向的是父类。


图6.1 类图
我们写段代码测试一下:

/**
 *      角色游戏测试
 * @author mouyong
 */
public class RoleTest {
    public static void main(String[] args){
        /*****************战士测试**********************/
        Fighter f1=new Fighter();
        f1.setName("战士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        System.out.println("战士测试输出:");
        System.out.println("姓名:"+f1.getName());
        System.out.println("等级:"+f1.getLevel());
        System.out.println("血量:"+f1.getHp());
        System.out.println("魔法值:"+f1.getMp());
        /******************魔法师测试********************/
        Magic m1=new Magic();
        m1.setName("大魔法师默然");
        m1.setLevel(120);
        m1.setHp(100);
        m1.setMp(100000);
        System.out.println("\n\n\n魔法师测试输出:");
        System.out.println("姓名:"+m1.getName());
        System.out.println("等级:"+m1.getLevel());
        System.out.println("血量:"+m1.getHp());
        System.out.println("魔法值:"+m1.getMp());
    }
}

我们可以看到,在Fighter和Magic类并没有定义姓名,等级,血量和魔法值这些属性,也没有定义它们的读取和设置方法,可是我们仍然可以使用f1和m1两个对象使用这些属性和方法,并得出正确的结果。这就是继承的力量,不需要重复写同样的代码,就可以使用它们。
输出结果:

图6.2 角色游戏测试输出界面

6.1.2 多态与”是一个”

继承可以让我们避免类间重复的代码定义,同时,它还带来很更多的”智能”。
在3.1.4我们讲过类型转换。我们说,当类型之间兼容的时候,我们可以进行两种类型转换,一种是自动类型转换,由编译自动帮我们完成,一种是强制类型转换,由我们强制声明完成。当一个类继承了另一个类的时候,我们说父子类之间就是兼容的,这个时候我们就可以进行类型转换。
下面的代码我相信大家能看懂,并且知道是可以编译通过的:

Fighter f1=new Fighter();
Magic m1=new Magic();

上面的代码并没有进行类型转换。那么我们接着看下面的代码:

Role role1=new Fighter();
Role role2=new Magic();

如果你把上面的代码进行编译,你会发现,它们能通过编译!这是因为当一个类的实例对象赋值给自己的父类变量时,编译器会自动进行类型转换,将子类当做父类看待,也就是编译器认为”战士是一个角色”,”魔法师也是一个角色”。我们知道这两句话是正确的,这就是我说的,父类和子类如果用我们人类的话来表达就是”子类(战士)是一个父类(角色)”的关系。当有”是一个”的关系时,编译器就会进行自动类型转换(默然说话:换个说法:子转父,自动转!)。
看完自动转换,我们再来看第三种情况:

Fight f1=new Role();
Magic m1=new Role();

在这个情况下,我们看到了,我们把一个父类的对象赋值给了一个子类的变量,这会发生什么呢?编译报错!因为编译器认为”角色是一个战士”和”角色是一个魔法师”并不是正确的,所以它报错了。(默然说话:你可以这样理解,每个人总想装爹,但是爹却是不愿意装儿子的。也许这样可以帮助你记住这个规则)。
再来看一个在现实代码中会存在的情况:

Role r1=new Fighter();
Fighter f1=r1;

第一句代码我们已经解释过了,它是可以成功通过编译的(子转父,自动转),但是第二句代码呢?它是不能通过编译的。这时你肯定会觉得奇怪,这个角色对象就是一个战士呀,为何不行呢?因为编译器是不会结合上一句代码来看第二句代码的,所以在编译器看来,第二句是有可能出错的(父转子,不愿意),所以它不会自动转换,那么如果我们一定要完成这个转换呢?我们可以改成这样:

Role r1=new Fighter();
Fighter f1=(Fighter)r1;

大家已经看到了,第二句的语法就是我们在第三章提到的”强制类型转换”的语法,相当于我们告诉编译器:”伙计,你放心,出了问题我负责!”。于是编译器就会去尝试完成类型转换。(默然说话:你可以记下这句口诀:子转父,不安全,需强制
强制转换在任意的父类转子类时均可以进行,编译都能通过,但是并不保证能成功执行,比如下面的代码:

Role r1=new Magic();
Fighter f1=(Fighter)r1;

这两句代码均是可以通过编译的,虽然我们能发现第二句代码是有问题的,因为r1其实不是战士,是一个魔法师。但是因为我们声明了强制类型转换,于是编译器本着”你说的,你负责!”的态度闭上了错误提醒的嘴,于是我们需要承担的后果就是,执行报错。

图6.3 类型转换失败:不能将Magic转为Fighter
在执行的时候我们会看到红色的报错信息(默然说话:额,那个绿色的字是我PS上去的,不要误以为你的报错信息里会有那行绿色的字哦{尴尬脸})。
总结一下:父转子,自动转,子转父,不安全,需强制。如果强制转换时类型真的不对,会出现ClassCastException(类转换异常)的运行时异常抛出。
前面花了很多的篇幅来讲”是一个”的原理,讲自动转换与强制转换的原则并非只是在玩语法的游戏,而是为”多态”的实现铺平理论的道路。只有在了解了自动转换与强制转换的原则之后,我们才有可能写出更灵活的代码。
例如,有这样一道题目是做为游戏一定要做的,就是显示角色的血量,魔法值。我们很可能会这样来完成:

public void showBlood(Fighter f){
    System.out.println("姓名:"+f.getName+"血量:"+f.getHp()+"魔法值:"+f.getMp());
}
public void showBlood(Magic m){
    System.out.println("姓名:"+m.getName+"血量:"+m.getHp()+"魔法值:"+m.getMp());
}

不错不错,现学现用呀。前面才讲过方法重载,我们这里就用上了,真的很棒哦!不过,别高兴得太早,我们来设想一个很实际的问题:我们这里只有两个角色,而一个实际的游戏中很可能有几百个角色,按我们现在的思路,我们就得重载几百个方法来显示不同角色的血量?写几百个方法倒也罢了,因为毕竟最终我们的程序里肯定会有上千个方法,问题是这几百个方法内的代码非常相似,几乎是重复的,这完全违背我们”任何代码只写一遍”的原则呀。
我们来想想”战士是一个角色”和”魔法师是一个角色”这两句话,还有”父转子,自动转”。可以只写下面的一个方法:

public void showBlood(Role r){
    System.out.println("姓名:"+r.getName+"血量:"+r.getHp()+"魔法值:"+r.getMp());
}

因为Role是所有角色的父类,所以,我们可以把任何子类作为参数传递给这个方法,而这个方法就可以输出任何角色的姓名,血量和魔法值,这包括目前还不存在的几百个角色,唯一的要求,就是它们需要继承自Role。这就是”多态”的写法。下面是具体实现的完整代码:

/**
 * 测试多态方法showBlood,通过设置传入的参数为父类,可以方便的适应多变的角色。
 * @author mouyong
 */
public class Game {
    public void showBlood(Role r){
        System.out.println("姓名:"+r.getName()+"血量:"+r.getHp()+"魔法值:"+r.getMp());
    }

    public static void main(String[] args){
        Game test=new Game();
        Fighter f1=new Fighter();
         f1.setName("战士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        Magic m1=new Magic();
        m1.setName("大魔法师默然");
        m1.setLevel(120);
        m1.setHp(100);
        m1.setMp(100000);
        //显示战士小中的血量
        test.showBlood(f1);
        //显示魔法师的血量
        test.showBlood(m1);


    }
}

下面是运行结果:

图6.4 多态方法运行结果
多态的意思,就是”一个方法,多种实现”。按字面意思,前面学过的方法重载也是多态实现的一种方式,这里讲到的利用父类参数的例子,也是多态实现的典型例子。后面我们还会接着讲方法重写,它是多态实现的第三种方式。

6.1.3 方法重写

我们接下来完成游戏中的另一个功能,完成游戏中任意角色的攻击调用。根据刚刚才学习过的思路,我想我们可以写这样一个方法:

public void attack(Role r){
    r.fight();
}

然后我们得到了一个编译器的报错信息:Role中找不到fight()方法!是的,fight()方法被定义在战士和魔法师两个子类中,Role中并没有这个方法,所以我们不可能使用Role来调用的。但我们可以观察到另一个特点,无论是战士,还是魔法师,fight()方法的声明都是这样的:

public void fight()

也就是说,方法声明是一样的,只是方法的操作代码不一样。所以,其实我们这可以把这个方法提升到父类方法中,象这样。

public class Role {
    //省略前面成员变量的声明
    //声明战斗方法,让子类重写,方便多态使用
    public void fight(){
        //此处无代码
    }
    //省略setter和getter方法的定义
}

由于所有的攻击都是子类才会知道的,所以我们让父类的这个方法为空方法,然后在子类中重新定义它的执行代码,
在继承父类之后,在子类中将父类的方法重新进行定义,我们称为方法重写(Override)。
由于Role定义了fight()方法(虽然方法体一行代码也没有),编译器就不会找不到fight()方法了,此时就可以继承利用我们前面所学的多态了。

/**
 * 测试多态方法showFight,通过设置传入的参数为父类,可以方便的适应多变的角色。
 * @author mouyong
 */
public class Game {
    //省略前面显示血量的方法定义….

    //显示战斗的方法定义,使用父类参数实现多态
    public void showFight(Role r){
        r.fight();
    }

    public static void main(String[] args){
        Game test=new Game();
        Fighter f1=new Fighter();
        f1.setName("战士小中");
        f1.setLevel(1);
        f1.setHp(100);
        f1.setMp(0);
        Magic m1=new Magic();
        m1.setName("大魔法师默然");
        m1.setLevel(120);
        m1.setHp(100);
        m1.setMp(100000);


        //显示战士小中的战斗
        test.showFight(f1);
        //显示魔法师的战斗
        test.showFight(m1);
    }
}

程序执行结果也表明Java非常的智能,你传给它 Fighter,它就调用Fighter的fight()方法,你传给它Magic,它就调用Magic的方法,结果如下:

图6.5 方法重写测试,智能完成子类重写方法的调用
子类重写父类一个方法时,必须要注意到方法的方法名,参数和返回值必须一模一样。这是一个锁碎的工作,特别是针对我们这边非英语国家的学生来说,真是一个地狱般的考验与修炼(默然说话:耶!我来自地狱,我居然活着出来了!),这种锁碎的工作,我们程序员一定要养成习惯,重复锁碎的活计,交给机器去办。自从JDK5加入了注解(Annotation)之后,这个检查是不是做了正确的方法重写的任务,总算可以给机器去完成了。

/**
 * 战士,加入了方法重写的注解@Override
 * @author mouyong
 */
public class Fighter extends Role {
    //战士战斗的方法
    //@Override注解表示让编译器检查此方法是否为方法重写
    @Override
    public void fight(){
        System.out.println("战士拨出了宝剑!");
    }
}

@Override这个注解表示让编译器检查这个方法是不是一个父类方法的重写,如果不是,则给出报错信息提示。(默然说话:这报错信息明显不是一个中国人翻译的,完全不明显它要表达什么,我怀疑这也是由机器来翻译的!正确的翻译应该是”此方法没有重写或者父类的方法”

图6.6 错误重写引发的报错信息(天坑,这是哪国人的翻译?!)
如果要重写父类的某个方法,加上@Override注解,写错方法名,机器就会告诉你了。关于注解,我们在第18章详细说明。

6.1.4 抽象方法、抽象类

一方面,Role类中的fight方法就象这样空着不写,不免让人觉得奇怪。(默然说话:在实际当中,其实有很多的时候都会有空着不写的方法存在的,这是一个避免不了的事实。)另一方面,由于没有提示,我们真的很难保证一次就写对这个方法的定义。(默然说话:不要说我们这些非英语国家的人民,就算是英语国的人民们也深受折磨。名字稍复杂,免不了进行反复核对,即使你使用了@Override,它也仅只能告诉你有没有错,却不能告诉你错在哪里)为了解决这一问题,Java引入了抽象方法的概念。
如果某个方法的确不知道应该写什么,Java允许你不写一对大括号({}),直接分号结束它就好了,唯一的代价是,你需要在返回值前面加上关键字abstract(抽象),以声明它是一个抽象方法。
还要付出的一个代价是,你的类也要在关键字class前面加上abstract关键字。以声明它是一个抽象类。
//含有抽象方法的类必须声明为抽象类,不能被实例化(new)

public abstract class Role {
    //省略成员变量的声明
    //省略setter与getter方法定义

    //声明抽象战斗方法,没有方法体,直接分号结束。
//抽象方法必须让子类重写,否则报错
    public abstract void fight();
}

类中如果有方法被声明为抽象方法,则说明这个方法没有可执行的代码,是不完整的,带有不完整方法的类也不应该进行实例化(new),这也就是当一个类声明了抽象方法后,这个类本身也必须声明为抽象的原因。如果你硬要实例化(new)一个对象,那等待你的自然就是编译器的报错信息。

图6.7 实例化一个抽象类的结果:报错信息
如果一个子类继承了一个抽象类,那这个子类就必须要实现这个抽象类声明的所有抽象方法(默然说话:是的,必须实现所有的抽象方法,一个都不能少!),这个时候你有两个选择,一个是继续声明方法为抽象方法(默然说话:额,我不觉得这个可以选,因为同时你就要把你的类也弄成抽象的,而抽象类又不能new,你写一个抽象类继承另一个抽象类搞毛线?),另一个是就是重写这个抽象方法。如果你没有,比如你只重写了部分抽象方法,并没有全部都实现,那你也会收到一个编译器的报错信息。

图6.8 未重写(图中叫”未覆盖”)抽象方法的报错信息
默然说话:耶!我看到了方法的名字,现在我知道哪个方法没有重写了!另外,我还发现,现在的IDE工具都可以帮助我进行重写,这样我就不用再浪费时间去核对这该死的方法名了!

图6.9 现在的IDE都提供了帮助我们改正错误的办法,只要轻轻一点!

6.2 继承语法细节

前面简单介绍了继承的语法,下面来具体对一些细节做一些说明。

6.2.1 protected成员

前面我们写了显示血量的方法,这个方法其实蛮麻烦的,因为我个人觉得,血量等等信息应该是由对象自身来告诉我们,而不是应该在另外的方法中去依次获得的。所以,我们可以为战士和魔法师两个类分别添加toString()方法,如下。

public class Magic extends Role {

    //省略其他代码
    //toString方法专为输出信息而设置
     public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.getName(),this.getHp(),this.getMp());
    }
}
public class Fighter extends Role {
    //省略战士战斗的方法

    //toString方法专为输出信息而设置
    public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.getName(),this.getHp(),this.getMp());
    }
}

这样修改之后,我们的测试类就可以很简捷的写成这样:

public void showBlood(Role r){
    System.out.println(r);
}

但是每次都要写getName()这样来获得成员变量的值真的好麻烦呀,能不能直接使用成员变量的名字呢?目前不行,因为这些成员变量都被设为private,如果改为public又不是我们想要的,我们只是想在子类里可以直接访问这些成员变量,并不想让所有的类都可以轻易访问它们。Java为我们提供了第三个关键字:protected,它可以限制其他的类不能访问,但是子类可以直接访问父类的protected成员。(默然说话:对的,和private与public一样,protected不仅可以修饰成员变量,同样也可以修饰成员方法。)象这样。

package cn.speakermore.ch06;

/**
 * 用于讲解类的继承
 * 父类:角色
 * @author mouyong
 */
public abstract class Role {
    protected String name;//名称
    protected int level;//等级
    protected int hp;//血量
    protected int mp;//魔法值
   //略。。。。
}

加了protected的类成员,同一个包中的类可以访问,不同包下的子类也可以访问。现在我们可以这样来写Fighter类了。

package cn.speakermore.ch06;

/**
 * 战士
 * @author mouyong
 */
public class Fighter extends Role {
    //……
    public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.name,this.hp,this.mp);
    }
}

当然,Magic也可以同样进行修改了,这里就不列出代码了。

提示:基于程序可读性,以及充分利用IDE的提示功能,强烈建议使用this.成员的形式书写代码

Java的三个访问修饰符均登场了,它们是publicprotectedprivate。如果你一个都没有写,那类的成员就拥有包访问权限,这个权限我们称为默认权限。同一个包内的类均可以访问默认权限的类成员。表6.1列出了他们的权限范围:
表6.1 访问修饰符与访问权限

关键字类内部相同包不同包
public可访问可访问可访问
protected可访问可访问子类可访问
不写关键字(默认)可访问可访问不可访问
private可访问不可访问不可访问

提示:此张表看上去很复杂,也不是很好背,可以比较简单地记住它们的使用规则,大部分情况下会使用public,它可以无限制访问,不愿意给访问的就写private,通常成员变量都是private的,只想给子类访问的就写protected。

6.2.2 方法重写的细节

在前面,我们在Fighter和Magic重写了toString()方法(默然说话:等会儿!toString()方法在Role里可没有!这种说法不对!),我们注意到,它们的代码又是一样的,那只要是一样的,是不是可以直接写在父类里呢?我们来试试。Role里添加toString(),象这样:

package cn.speakermore.ch06;

public abstract class Role {
    /**
     * 在Role中重写toString()
     * 此方法添加在Role类的最后,前面的代码省略
     * @return 
     */
    @Override
    public String toString(){
        return String.format("姓名:%s 血量:%d 魔法值:%d", this.name,this.hp,this.mp);
    }
}

默然说话:天呀,它居然加了@Override注解!居然没有错!
然后删掉Fighter与Magic里的toString方法定义,运行测试类,看看是什么结果?

图6.10 运行结果与前面一样,没有变化
默然说话:Java真的好智能,这都能对!)我们发现运行的结果和前面是一样的!又一次把重复的代码变得只写一遍,感觉真的很好。
不过,我总觉得应该再做点什么。在这个角色信息输出中,似乎应该要显示出角色的类型,不然人家取角色名的时候没有加角色的类型,我们就不知道他是一个什么样的角色了。对!就这样办。
看来我们还是得重新为Fighter重写toString()方法。不过,这次重写与前面不一样,因为Role中的toString()已经写好了角色基本信息了,所以我们只要在子类的toString()里获得父类的toString()方法返回字符串,再连接上角色类型信息就可以了。问题来了,如何在子类里指定调用父类的方法呢?我们可以使用super关键字,象这样:

package cn.speakermore.ch06;

public class Fighter extends Role {
    //省略前面的代码
    @Override
    public String toString(){
        //super表示父类对象
        return "战士:["+super.toString()+"]";
    }
}
战士写完,魔法师也一样:
package cn.speakermore.ch06;

public class Magic extends Role {


    @Override
    public String toString(){
        return "魔法师:["+super.toString()+"]";
    }
}

来看看输出结果:

图6.11 修改toString()后的的执行结果
耶!成功的在父类的字符串前加上了角色的名称!
super的意思就是”我爹”。指当前对象的父类对象(默然说话:对的,是一个对象,不是父类。所以super关键字拥有所有对象的特点,比如,只能调用非private修饰的成员变量或方法。
方法重写要注意一个问题,就是方法重写的访问修饰符只能扩大,不能缩小。所以,如果声明为public,就只能写为public了。

图6.12 重写不能缩小访问修饰权限
关与重写,有个小细节必须提及。就是关于前面提到的,关于”方法重写要求方法的返回值,方法名称,参数列表完全一致”,在JDK5之后,你可以声明返回值为原来返回值的子类。例如,我们有两个类,Animal是父类,Cat是子类。我们在使用它们做返回值时,有一个方法定义如下:

public Animal getSome(){}。

在JDK5之前,如果我重写这个方法如下:

public Cat getSome(){}

是会报错的,但是JDK5之后却不报错了。

提示:static方法不存在重写,因为static方法均为类方法,是公有成员,所以如果子类中定义了相同返回值、方法名、参数列表的方法时,也仅只属于子类,并非方法重写。

6.2.3 再看构造方法

如果类有继承关系,则在实例化子类对象的时候,会先实例化父类对象。也就是说,会先执行父类的初始化过程,然后再执行子类的初始化过程。
由于构造方法是可以重载的,所以子类也可以指定调用父类的某个重载的构造方法,如果子类没有指定,则默认调用无参构造方法(默然说话:这个时候,如果你的父类没有无参构造方法,那就麻烦了,子类无法实例化了。所以如果你进行了构造方法的重载,请务必写上无参的构造方法,即使打一对空的大括号也行,这可以防止很多Java的高级特性(如反射机制)无法进行的问题)。
如果想要在子类中指定调用父类的构造方法,可以使用super()的语法。要注意的是,super()只能写在子类构造方法中,而且必须是构造方法中的第一行。你可以在super()中添加入参数,这样Java就会智能的识别对应的父类重载的构造方法进行调用了。来看例子:
首先,我们编写了一个父类Father,它有两个构造方法,默认的,和带一个整型参数的:

package cn.speakermore.ch06;

/**
 * 构造方法调用顺序的教学类,
 * 父类,拥有两个构造方法
 * @author mouyong
 */
public class Father {
    public Father(){
        System.out.println("这是Father无参构造方法");
    }
    public Father(int a){
        System.out.println("这是Father有参构造方法,它传入了"+a);
    }

}

然后我们再编写两个子类,Son和Son2。其中Son用来测试默认情况下的调用顺序:

package cn.speakermore.ch06;

/**
 * 用于测试默认构造方法调用的测试类
 * 这是一个子类
 * @author mouyong
 */
public class Son extends Father {
    public Son(){
        //这里没有使用super(),但是编译器会默认添加调用父类的无参构造方法
        //super();
        System.out.println("这是Son的无参构造函数");
    }
}

而Son2,是用来测试指定调用父类一个参的构造方法的(使用super(3)这条语句来指定):

package cn.speakermore.ch06;

/**
 * 用于测试使用super()调用指定的父类构造方法的子类,
 * 另一个子类
 * @author mouyong
 */
public class Son2 extends Father {
    public Son2(){
        //通过传递一个整型数,指定调用父类中带一个整形参数的构造方法
        super(3);
        System.out.println("这是Son2的无参构造方法");
    }
}

最后,使用一个测试类,对它进行测试:

package cn.speakermore.ch06;

/**
 * 父子类构造方法调用的测试
 * @author mouyong
 */
public class FatherAndSonTest {
    public static void main(String[] args){
        //测试默认情况下,构造方法的调用顺序
        new Son();
        System.out.println("============漂亮的分割线================");
        //测试在子类中指定调用父类某个构造方法的调用顺序
        new Son2();
    }
}

执行的结果如下图:

图6.13 继承下的初始化代码执行顺序及指定父类的构造方法
我们可以看到,第一个new Son()调用了Father的无参构造方法,而第二个new Son2(),由于使用了super(3),指定调用了Father的有参构造方法,并收到了参数3。

注意:由于this()和super()都要求写在构造方法的第一行,所以一个构造方法中,写了this()就不可能再写super(),同样,写了super()就不可能再写this()。

6.2.4 再看final关键字

第三章告诉我们,可以在方法变量前添加final,让变量的值不能再被修改,第五章又告诉我们,还可以在类的成员变量前添加final,让成员变量也不能再次被修改。这里,我们要知道,在class的前面,也可以添加final,让这个类成为太监。(默然说话:理论上,太监都不会再有后代了。
Java里最有名的”太监”类,就是我们经常使用的String。

图6.14 Java APIs中String的文档描述
如果打算继承final类,则会发生编译错误,如图:

图6.15 无法从最终(final)String进行继承
除了可以用于类的前面,final还可以用于方法的前面,用来表示方法不能被子类重写。Java中最著名的Object类里就有这样的方法。

图6.16 无法重写的wait()方法

提示:Java SE API中会声明为final的类或方法,通常都与JVM对象或操作系统资源管理有密切关系。所以都不希望用户重写这些方法,以免出现不可预料的情况,甚至破坏JVM的安全性。比如这里例举的wait()方法,还有notify()方法等等。


图6.17 错误:不能重写(图中叫”覆盖”)final方法

6.2.5 java.lang.Object

在Java中,子类只能继承一个父类,如果定义类时没有用到extends关键字来指定任何父类,则会自动继承java.lang.Object(默然说话:现在知道我前面为什么说Object是”著名的”了吧?Object是一切Java类的父类,有时候也被称为Java类的根类,因为所有Java类的最顶层父类一定是Object。不过它也是最可怜的,Object没有父类。)。
再根据我们前面说过的类对象的类型转换规律,所以我们可以得出:任何一个类都可以赋值给Object类型的变量(子转父,自动转):

Object o1="默然说话";
Object o2=new Date();

这样做的好处是明显的,坏处也是明显的。好处就是,当我们在编码的时候,如果我们要处理的数据,它的类型要求是多种类型,这时我们就可以声明一个Object[]类型来收集它们,并做统一处理。Java的集合就是利用了这一点,很轻松解决了不同数据类型的数据放在一起的难题。它的源代码看起来大概是这样的:

package cn.speakermore.ch06;

import java.util.Arrays;

/**
 * 一个模仿Java的ArrayList功能的类
 * @author mouyong
 */
public class ArrayList {
    //因为允许集合可以任意混装各种类型的对象,所以使用Object数组
    private Object[] list;
    //目前list数组的下标,这个下标还没有装东西,可以赋值。相当于集合的长度
    private int next;

    /**
     * 指定集合的初始长度的构造方法
     * @param capacity 一个数字,指定集合的初始长度
     */
    public ArrayList(int capacity){
        list=new Object[capacity];
    }

    /**
     * 默认构造方法,指定了数组初始长度为16
     */
    public ArrayList(){
        this(16);
    }

    /**
     * 添加对象到集合里
     * @param o 被添加到集合里的对象,可以是任意对象,所以定义为Object类型
     */
    public void add(Object o){
        if(next==list.length){
            //如果next刚好是集合的长度,说明集合已经满了,自动扩容到原来的2倍
            list=Arrays.copyOf(list, next*2);
        }
        //把对象添加到数组中
        list[next]=o;
        //下标移动到下一个位置,准备接收下一个元素
        next++;
    }

    /**
     * 获得指定位置的对象
     * @param index 整数,指定的集合下标,从0开始,不应该超过集合的最大长度。
     * @return 对象,因为不知道集合中所装对象的具体类型,所以也被定义为Object
     */
    public Object get(int index){
        return list[index];
    }

    /**
     * 获得集合的长度
     * @return 整数,集合的长度
     */
    public int size(){
        return next;
    }
}

自定义的ArrayList类,它使用了一个Object[]数组装对象。如果在创建对象时没有指定长度,则默认使用16。
可以通过add()方法来装入任意对象。如果原长度不够,则自动扩容到原来的2倍。如果要取出对象,则使用get()方法,传入下标来获取。如果想要知道有多少个对象装在里面,则可调用 size()方法。下面是一个使用的例子。

package cn.speakermore.ch06;

import java.util.Scanner;

/**
 * 测试自定义ArrayList的测试类
 * @author mouyong
 */
public class ArrayListTest {
    public static void main(String[] args){
        //实例化自定义集合对象
        ArrayList infos=new ArrayList();
        //准备键盘输入
        Scanner input=new Scanner(System.in);
        //设置循环终止变量
        String isQuit="";
        do{
           System.out.println("请输入姓名:");
           String name=input.nextLine();
           infos.add(name);//将字符串放入集合中
           System.out.println("请输入年龄:");
           int age=input.nextInt();
           infos.add(age);//将整数放入集合中
           System.out.println("是否继续?(y/n)");
           isQuit=input.next();
           //为解决字符输入的bug而多写的接受语句(想知道bug是什么样,可以删除此句)
           input.nextLine();
       }while("y".equalsIgnoreCase(isQuit));
        //循环输出集合中所有的数据
       for(int i=0;i<infos.size();){
           System.out.println("姓名:"+infos.get(i++));
           System.out.println("年龄:"+infos.get(i++));
       }
    }
}

下面是具体执行的结果:

run:
请输入姓名:
默然说话
请输入年龄:
44
是否继续?(y/n)
y
请输入姓名:
狂狮中中
请输入年龄:
10
是否继续?(y/n)
n
姓名:默然说话
年龄:44
姓名:狂狮中中
年龄:10
成功构建 (总时间: 31 秒)

java.lang.Object是所有类的顶层父类,所以任意子类均可重写其定义的非final方法,在现实中,我们也是这样做的。有一些方法是经常会被重写的。
1. 重写toString()
在前面的例子中,我们已经重写过toString()方法了,它是Object经常被重写的一个方法,主要的作用就是用来方便我们显示一些字符串内容(默然说话:前面的游戏已经大量应用喽。
在Object中toString()的方法声明是这样的:

public String toString(){
    return getClass().getName()+"@"+Integer.toHexString(hashCode());
}

现在还不好解释以上代码的具体含义,它输出了一个类名,后跟”@”,接着是十六进制的数字(默然说话:我常告诉学生,这一串十六进制数字与内存有关,并不是内存地址,但的确是根据内存地址换算出来的。)。如果你没有重写过toString(),那么用下面这句代码,就会得到这样的一个输出。

ArrayList infos=new ArrayList();
System.out.println(infos);


图6.18 System.out.println(infos)的输出结果

注意:如果你尝试在你的电脑上运行,那么后面的十六进制数字会和我的不一样。

2.重写equals()
在第四章谈过,如果想要比较两个对象内容相等,不能使用==,而是要通过equals()方法。而equals()也是属于Object类的一个方法,其源代码是这样的:

public boolean equals(Object obj){
    return this==obj;
}

如果你能看懂,其实应该看出来了,Object的equals()方法定义也是用的==,所以,如果你不重写equals()方法,你想要的比较两个对象内容相等的奇迹也是不会出现的。如何定义eqauls()方法呢?这还真没有统一的写法,不过有一个模式可以借鉴,如下面的代码:

package cn.speakermore.ch06;

import java.util.Objects;

/**
 * 示范equals方法重写
 * @author mouyong
 */
public class Student {
    private Integer id;

    @Override
    public boolean equals(Object obj){
        //首先,比较"我是不是我"
        if(this==obj){
            return true;
        }
        //其次,证明类型是不是匹配
        if(!(obj instanceof Student)){
            return false;
        }
        //排除前面两种情况,进入自定义部分
        Student stu=(Student)obj;
       //下面这句代码的意思,我们定义了一个规则:只要id相同,我们就认为是同一个学生
        return Objects.equals(stu.getId(), this.getId());
    }

    /**
     * @return the id
     */
    public Integer getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(Integer id) {
        this.id = id;
    }
}

上面代码的注释就在说明这个模式,第一步首先验证对象的内存地址相不相同,之后再验证对象是不是同一种类型,最后是自定义规则,这部分就是要由你来决定如何写的。也就是说,在具体的类中,你们是如何规定”对象的内容相同”。在这个例子里,我们规定”如果对象的id是相同的,我们就认为两个对象是相同的”。
此外,为了完成类型比较,我们使用了instanceof关键字,它是一个比较运算符,在左边要写一个对象变量名,在右边要写类的名称,instanceof完成比较左边的对象与右边类是否兼容。如果不兼容,直接报语法错。
另外要注意的是,instanceof关键字为true的情况并非类名称与对象名完全一致,类为父类也是会返回true的。
最后,通常我们重写了eqauls()方法之后,同时也会重写hashCode()。等到第9章时我们再来讨论。

6.2.6 关于垃圾收集

创建对象就会占据内存,这是一个常识。如果程序执行流程中出现了无法使用的对象,这个对象就只是”占着茅坑不拉屎”的垃圾,它占用了内存,却无法使用,浪费了这些内存。
放在以前,程序员是要自己来做这件很”脏”却经常很难搞定的事情(默然说话:哦,”偷鸡不成蚀把米”就是指这类”脏”活了吧。垃圾没清干净,倒留下一堆bug可真是老前辈们的”家常便饭”。其实,我们经常听老前辈们传说C语言如何如何难学,特别是指针。其实指针一点都不难学,难的是如何确定一块内存已经是垃圾了,何时释放内存才是正确的。这个过程中经常写出bug,把程序搞崩溃。这才是C语言真正的地狱模式。),于是Java提供了垃圾回收机制(Garbage Collection, 简称GC),专门用来处理这些垃圾。只要是程序里没有任何一个变量引用到的对象,就会被GC认定为垃圾对象。在CPU有空的时候,或者是内存已经占满的时候,GC就会自动开始工作(这就是多线程运作的方式,我们在第11章说明)。
实际要说明垃圾回收的原理是很困难的,因为它的算法就很复杂,不同的需求还会导致有不同的算法。所以作为我们来说,只要知道“JVM会帮助我们进行内存管理,它的名称叫垃圾回收,简称GC,耶,太棒了!”,就足够了。细节让JVM工程师帮我们搞定吧。
那到底哪些是垃圾呢?下面的例子将说明这个问题,先来看代码:

Object o1=new Object();
Object o2=new Object();
o1=o2;

我们需要弄清楚的是,在第一行的代码中进了三步操作,第一步声明了o1变量内存,第二步创建了一块内存放Object对象,第三步是赋值操作,把Object对象的内存地址放到了o1变量中。第二行代码也是一样:o2变量得到了第二次new出来的Object对象的地址。这时,两个new出的对象都分别由o1和o2引用,所以它们目前都不是垃圾。

图6.19 两个对象不是垃圾
接下来是第三行代码,把o2的值(第二个Object对象的地址)赋值给了o1。此时o1原来的值就会被覆盖,而o1和o2两个变量都在引用第二个对象了。第一个Object对象就没有任何变量在引用它,它就成为了垃圾,GC就会自动找到这样的垃圾并予以回收。

图6.20 第一个对象成为垃圾

6.2.7 再看抽象类

写程序常有些看似不合理但又非得完成的需求。举个例子,现在老板叫你开发一个猜数字的游戏,随机产生一个1000-9999的四位数,用户输入的数字与随机产生的数字相比,如果相同就显示”猜对了”,如果不同主继续让用户输入数字,一共猜12次。
这个程序有什么难的?相信现在的你可以写出来:

package cn.speakermore.ch06;

import java.util.Random;
import java.util.Scanner;

/**
 * 猜数游戏:计算机产生一个四位数(1000-9999),由用户来猜。<br />
 * <br />
 * 如果没猜中,给出"大了"或"小了"的提示,同时还给出"猜中了x个数"的提示<br />
 * 最多可以猜12次。<br />
 * 如果猜中了,给出猜中的提示
 * @author mouyong
 */
public class Guess {
    public static void main(String[] args){
        Scanner input=new Scanner(System.in);
        Random random=new Random();
        Integer guess=random.nextInt(9000)+1000;

        //用来存放电脑想出来的四位数中的每一个位置上的数字
        int[] numberComputer=new int[4];
        int clientInput=0;


        Integer tempComputer=guess,tempClient=clientInput;
        int i=0;
        while(tempComputer!=0){
            //将电脑想出来的四位数分为四个数字放到数组里
            numberComputer[i]=tempComputer%10;
            tempComputer=tempComputer/10;
            i++;
        }
        System.out.println("我现在想好了一个1000-9999之间的数,你可以猜12次");

        for(i=0;i<12;i++){
            System.out.println("第"+(i+1)+"次请输入一个数:");
            clientInput=input.nextInt();
            //如果猜对了,则结束游戏
            if(clientInput==guess){
                System.out.println("恭喜!你猜对了!");
                System.exit(0);
            }
            //告诉用户猜的数是大是小
            if(clientInput>guess){
                System.out.println("大了");
            }else{
                System.out.println("小了");
            }
            //告诉用户猜中了几个数
            int count=0;
            for(int j=0;j<numberComputer.length;j++){
                if(numberComputer[j]==clientInput%10){
                    count++;
                }
                clientInput=clientInput/10;
            }
            if(count!=0){
                System.out.println("你猜的数有"+count+"个");
            }else{
                System.out.println("你一个都没有猜中!");
            }

        }

        System.out.println("很遗憾,没有猜中!");


    }
}

我们可以做了一个挺复杂,富有挑战且真的很有趣的猜数游戏哦!(默然说话:哦,这个例子来自于一次与儿子去于密室逃脱时的一个迷题。)你兴冲冲的把程序交给老板,准备迎来一如既往的表扬时,老板却皱着眉头说:”这个,我们似乎不应该在文本的状态下执行这个游戏呀。”,你一楞,随即机智的问道:”那会怎么来执行这个程序呢?”,老板一脸看到未来的迷茫样子:”这是个好问题,不过我们还没有完全决定,可能用窗口程序,其实网页或者app也不错,难说我们需要造一台专用的游戏机,通过九宫按钮直接输入数字?下周开会讨论一下吧。”,于是你舒了口气,说:”好吧,那我下周讨论完了再写吧。”,老板用不容置疑的口气说:”不行!”。你只好无奈的点点头,退出老板的门时,你有没有感觉到一万只草泥马欢快地在你的心脏里跳舞呢?
这可不是一个段子(默然说话:嗯,当然,我似乎把它写成了段子)。在团队合作、多部门开发程序时,有许多时候,有一定顺序完成的工作必须要同时开工,因为老板是不可能闲养你3个月等上一个工序完成之后,再你完成你的工作。(默然说话:对的,如果要等3个月,那直接不请你,让人家直接全做完就好了。)虽然需求没有决定,但你却要把你的程序完成的例子太多了。
有些不合理的需求,本身确实不合理,但有些看似不合理的需求,其实可以通过设计来解决。比如上面的例子,虽然用户输入,显示结果的环境未定,但你负责的部分(猜数游戏的逻辑)还是可以先操作的。我们可以这样完成:

public abstract class GuessNumber {
    public void go(){
        Random random=new Random();
        Integer guess=random.nextInt(9000)+1000;

        //用来存放电脑想出来的四位数中的每一个位置上的数字
        int[] numberComputer=new int[4];
        int clientInput=0;


        Integer tempComputer=guess,tempClient=clientInput;
        int i=0;
        while(tempComputer!=0){
            //将电脑想出来的四位数分为四个数字放到数组里
            numberComputer[i]=tempComputer%10;
            tempComputer=tempComputer/10;
            i++;
        }
        //所有输出消息均替换为抽象方法,以便在将来不用修改这部分代码
        print("我现在想好了一个1000-9999之间的数,你可以猜12次");

        for(i=0;i<12;i++){
            print("第"+(i+1)+"次请输入一个数:");
            //用户输入替换为抽象方法,以便在将来保证不用修改这部分代码
            clientInput=clientInput();
            //如果猜对了,则结束游戏
            if(clientInput==guess){
                print("恭喜!你猜对了!");
                System.exit(0);
            }
            //告诉用户猜的数是大是小
            if(clientInput>guess){
                print("大了");
            }else{
                print("小了");
            }
            //告诉用户猜中了几个数
            int count=0;
            for(int j=0;j<numberComputer.length;j++){
                if(numberComputer[j]==clientInput%10){
                    count++;
                }
                clientInput=clientInput/10;
            }
            if(count!=0){
                print("你猜的数有"+count+"个");
            }else{
                print("你一个都没有猜中!");
            }

        }

        print("很遗憾,没有猜中!");
    }

    public abstract void print(String msg);
    public abstract Integer clientInput();
}

你可以看出,我们把不确定的部分(用户的输入与消息的输出)替换为抽象方法,这样既解决了老板没决定,不知道如何输入和输出的问题,又解决了我们写的代码将来也许会面临的大量修改的问题。
等到下周开会决定了,你只需要再写个子类,继承GuessNumber,重写两个抽象方法即可。实际上你应该已经发现了,由于这两个抽象方法,咱们的猜数代码可以利用继承反复重用了。下个月开会研究,由于猜数游戏大受欢迎,我们需要进行”全平台”商业化,此时你只需要再写几个子类,继承GuessNumber,对两个方法做不同的重写就够了,省下的时间,为你和公司带来了丰厚的回报。你可以买更大的房子,更漂亮的车,生更多的孩子了!这就是设计的力量!

提示:设计上的经验,我们称为设计模式,上面的例子我们使用了”模板方法”模式。如果对其他设计模式感兴趣,可以上网查找相关”设计模式”的资料

默然说话:在去查找之前……先擦擦你的口水,纸弄湿了不要紧,键盘要是湿了,说不准会电你的

6.3 重点复习
  • 面向对象中,子类继承父类,充分进行代码重用是对的,但是不要为了代码重用就滥用继承。如何正确使用继承,如果更好的活用多态,才是学习继承时的重点(默然说话:这似乎能讲的东西太多了,所以这里只说明一个你在学习过程重点需要关注的地方,后面我们还会具体的举很多很多的例子来说明的,总之二十多年的编程经验告诉我,优秀程序的的样子都是长一样的,那就大家常说的六个字:”易维护,易扩展”,如果你觉得这太高大上,不接地气,我换六个平易近人且吸引眼球的另外六个字告诉你:”少干活,多拿钱”!要知道,在写这些文字之前,一般人我都不告诉他们的。
  • 如果出现代码的反复书写,就应引警觉,此时可考虑的改进之一,就是把相同的程序代码提升为父类(默然说话:其实这是第三步,第一步应该考虑把重复代码写到一个独立的方法中,第二步应该考虑使用方法重载,第三步才考虑父类。)。
  • 在Java中,继承使用extends关键字,所有非私有属性均会被继承。但是私有属性如果父类提供了公有方法,也可以使用。
  • Java为了保持程序不出现”伦理道德”的争议,只允许单继承,即一个类有且只有一个父类(默然说话:Object例外,它没有父类
  • 还记得父子类的类型转换口诀么?”子转父,自动转;父转子,强制转”。
    abstract表示抽象方法,它使用在类和方法定义的前面。抽象方法不能写方法体(默然说话:方法体就是那对大括号,还记得么?),子类必须重写父类的抽象方法,否则报错;抽象类不能实例化(默然说话:实例化就是new,new就是实例化,记得了么?),只能由子类来实际执行它的功能代码。另外,有抽象方法的类必须声明为抽象类,否则也报错。
  • 被声明为portected的成员,相同包中的类可以直接存取,不同包中的类可以在继承后的子类直接存取。
  • Java中有publicprotectedprivate三个权限关键字,但却有四种权限,因为默认权限就是不写关键字的时候。
  • 如果想在子类中指定调用父类的某个方法,可以使用super关键字。
    重写方法时要注意,在JDK5之后,方法重写时可以返回被重写方法返回类型的子类。
  • final可以用于类、方法和属性的前面,用于类的前面表示类不能被继承(默然说话:太监类,记得吗?),用于方法前,表示方法不能被重写,用于属性前,则意味着属性不能被第二次赋值(默然说话:就是我们常说的常量了呢)。
  • 如果定义类时没有指定任何父类,并不意味着它没有父类,因为JVM会自动让这个类继承Object。
  • 对于在程序中没有被变量引用的对象,JVM会进行垃圾收集(GC),这是非常重要的,因为这能提高我们对内存的使用,不至于浪费内存。
6.4 课后练习
6.4.1 选择题

1.如果有以下的程序片段:

class Father{
void service(){
    System.out.println("父类的服务");
}
}
class Children extends Father{
@Override
void service(){
    System.out.println("子类的服务");
}
}
public class Main{
public static void main(String[] args){
    Children child=new Children();
    child.service();
}
}

以下描述正确的是()

A. 编译失败
B.显示”父类的服务”
C.显示”子类的服务”
D.先显示”父类的服务”,后显示”子类的服务”

2.接上题,如果main()中改为:

Father father=new Father();
father.service();

以下描述正确的是()
A. 编译失败
B.显示”父类的服务”
C.显示”子类的服务”
D.先显示”父类的服务”,后显示”子类的服务”

3.如果有以下的程序片段:

class Test{
String ToString (){
    return "某个类"
}
}
public class Main{
public static void main(String[] args){
    Test test=new Test();
System.out.println(test);
}
}

以下描述正确的是()。

A. 编译失败
B.显示”某个类”
C.显示”Test@XXXX”,XXXX为十六进制数
D.发生ClassCastException

4.如果有以下的程序片段:

class Test{
int hashCode (){
    return 99
}
}
public class Main{
public static void main(String[] args){
    Test test=new Test();
System.out.println(test.hashCode());
}
}

以下描述正确的是()。

A. 编译失败
B.显示”99”
C.显示”0”
D.发生ClassCastException

5.如果有以下的程序片段:

class Test{
    @Override
String ToString (){
    return "某个类"
}
}
public class Main{
public static void main(String[] args){
    Test test=new Test();
System.out.println(test);
}
}

以下描述正确的是()。

A. 编译失败
B.显示”某个类”
C.显示”Test@XXXX”,XXXX为十六进制数
D.发生ClassCastException

6.如果有以下的程序片段:

class Father{
abstract void service();
}
class Children extends Father{
@Override
void service(){
    System.out.println("子类的服务");
}
}
public class Main{
public static void main(String[] args){
    Father father=new Children();
    child.service();
}
}

以下描述正确的是()

A. 编译失败
B.显示”子类的服务”
C.执行时发生ClassCastException
D.移除@Override可编译成功

7.如果有以下的程序片段:

class Father{
protected int x;
Father(int x){
    this.x=x;
}
}
class Children extends Father{
Children(){
    this.x=x;
}
}

以下描述正确的是()

A. new Children(10)后,对象成员x值为10
B.new Children(10)后,对象成员x值为0
C.Children中无法存取x,编译失败
D.Children中无法调用父类构造方法,编译失败

8.如果有以下的程序片段:

public class StringChild extends String{
public StringChild(String str){
    super(str);
}
}

以下描述正确的是()

A. String s=new StringChild(“测试”)可通过编译
B.StringChild s=new StringChild(“测试”)可通过编译
C.因无法调用super(),编译失败
D.因无法继承String,编译失败

9.如果有以下的程序片段:

class Father{
Father(){
    this(10);
    System.out.println("Father()");
}
Father(int x){
    System.out.println("Father(x)");
}
}
class Children extends Father{
Children(){
    super(10);
    System.out.println("Children()");
}
Children(int y){
    System.out.println("Children(y)");
}
}

以下描述正确的是()

A. new Children()显示”Father(x)”、”Children()”
B.new Children(10)显示”Children(y)”
C.new Father()显示”Father(x)”、”Father()”
D.编译失败

10.如果有以下的程序片段:

class Father{
Father(){
    System.out.println("Father()");
    this(10);
}
Father(int x){
    System.out.println("Father(x)");
}
}
class Children extends Father{
Children(){
    super(10);
    System.out.println("Children()");
}
Children(int y){
    System.out.println("Children(y)");
}
}

以下描述正确的是()

A. new Children()显示”Father(x)”、”Children()”
B.new Children(10)显示”Father()”、”Father(x)”、Children(y)
C.new Father()显示”Father(x)”、”Father()”
D.编译失败

6.4.2 操作题
  1. 如果使用6.2.5设计的ArrayList类收集对象,想显示所收集对象的字符串描述时,会显示非常麻烦。尝试重写toString()方法,让客户端可以方便的显示所收集对象的字符串描述。
  2. 接上题,请重写ArrayList类的equals()方法,先比较收集的数量是否相等,然后对应位置比较各对象的内容是否相等(使用各对象的equals()),只有数量相等且对应位置的各个对象的内容相等,才判断两个ArrayList对象是相等的。
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看rEADME.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看rEADME.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默然说话

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值