上篇文章我们介绍了继承,子类继承父类的时候,所有的属性和方法都会被继承,相当于在子类中做了一份拷贝。对于构造方法则按照父类的构造方法是否含有参数而分为有两种情况:如果没有参数,实例化子类对象时默认先调用父类的构造方法,如果子类里边有自己明确写出来的构造方法会再调用子类自己的;如果有参数,则子类对象需用super关键字先显式调用父类构造方法,再调用自己的。再复习一遍上次写的这个爸爸和儿子小程序:
父类的构造方法有参数,所以子类对象s需要先super显式调用父类的构造方法后才能执行自己的。而且由于hobby()这个方法被继承了,相当于拷贝了一份,所以儿子这个对象调用hobby()的时候也能执行通过,一切看起来都没问题。这时候儿子说了,谁说我喜欢干农活?干农活多没意思,我喜欢泡妞。这怎么办呢?java里面有一个概念,叫做方法的重写:
之前介绍成员方法时说过一个叫重载的概念,就是允许同名方法接受不同类型不同数量的参数,参数必须独一无二。重写也是关于方法的概念,顾名思义,就是把继承过来的方法内容重新写一遍。父类的hobby方法被子类继承过来,子类可以再把这个方法的内容替换掉重新写,写完了就把继承过来的直接覆盖了:
现在我让儿子去泡妞:
子类对象s.hobby()输出的是新的内容了。方法重写就是你如果不满意继承过来的方法,不满意执行内容或是输出内容,你就可以手动重新写一遍把原来继承的给覆盖掉。注意我这里说的,不满意执行内容或输出内容,那有人说了我不满意方法名或是返回类型,也不满意参数,我想都给它改了。行么?不行。你要是都改了谁知道是你自己的方法还是继承来的?所以在方法重写时有一条规则:方法的返回类型,方法名和参数内容必须完全一致。换句话说,就是方法的声明部分必须和继承来的一模一样才行:
不信你把父类的hobby方法加个返回值:
肯定报错,不兼容,子类的hobby方法不能覆盖父类的。重载和重写不要搞混,再说一遍,重载和继承没关系,它只是允许某一个类中同名方法存在,并且这些同名方法的参数独一无二。重写则是把从父类继承过来的方法重新编写里面的内容,方法的返回类型,方法名和参数内容必须完全一致。
有了方法重写的概念之后我们就可以继续讨论第三个,也是最后一个特征了。假如现在这个爸爸除了有个儿子,还有个可爱的小女儿。那又多了个子类:
打印出来的结果是:
现在我把程序稍微做一点改变,把Son s = new Son()和Daughter d = new Daughter()分别替换成f = new Son()和f = new Daughter(),然后用f调用儿子类和女儿类继承过来的变量和方法。看看会发生什么:
我们惊奇地发现,程序通过了,我们用父类的对象f成功初始化了子对象。怎么还能这么玩?其实我们仔细想想也对,这个f是Father类型的,又因为Son继承了Father,所以Son也就间接属于Father类型。等号左右都是一个类型,怎么不行呢?当然可行。告诉大家一个小方法,如果以后遇到要判断一个类是不是间接属于另外一个类时可以用一个叫instanceof的关键字:
a是一个对象,b是一个类名,翻译过来就是a的类型是不是间接属于b。它返回的是一个布尔类型结果,true代表属于,false代表不属于。比如你现在写s instanceof Father,看看结果:
因为Son间接属于Father类型,所以if内的语句块肯定被执行。父类对象初始化子对象一定是可行的。有人问了,这样做的好处是什么?这就是面向对象的第三个特征:多态,就是一个对象的多种状态。一个父类对象引用可以指向自己的对象,也可以指向继承它的子类的对象。指向自己对象时因为存的是自己对象的地址,当前状态是自己,所以调用自己的属性或方法;指向某个继承它的子类的对象时因为存的是子类对象的地址,状态就变成了子类,也就会调用子类的属性和方法。内存图可以这样画:
你看,这样我们就不用再声明一个子类的引用了,直接用父类的就可以。所以说,多态是在继承上的一个延伸。我们上节课说继承的时候也说了,在实现继承时我们最关键的一步其实就是找到父类,而找到父类的过程就是提取相同元素的过程。你看咱们这个例子,儿子和女儿都有父亲的共同属性和方法。然后多态就是用父类的引用指向子对象,然后调用子类里重写的方法。根据这个思想,再举个例子,比如我现在想测试下打开某一个网页时IE,chrome和Firefox的响应时间,实际输出是这样的:
学了继承和多态以后写这个小程序就容易了。第一步,先提取相同元素找父类:三个浏览器都有名称、制造商、期望响应时间以及实际响应时间。这些相同的元素就可以组成一个父类。所以,我在Eclipse新建一个叫BrowserInheritance的项目 -> 一个叫com.browsers的包 -> Browser.java:
这段代码理解起来应该不费劲,对成员变量赋值用了第三种方法 - 通过属性封装用set和get方法对属性赋值和取值。此外还有一个getActualResponseTime方法,用来接收期望响应时间并经过计算后返回一个实际相应时间。我想着反正这个方法也要被子类重写,我就不嘚瑟了,简简单单返回一个值就完事了。我本来还想空着啥都不写呢,但没办法必须要返回一个String类型的结果。我很懒,但我对大家不懒,尽力而为,能解释多清楚就解释多清楚。
第二步,写子类。子类就是那三个浏览器,继承后需要重写的也就是getActualResponseTime方法,需要把计算过程写进去。我把它们都分开放到IE.java,Chrome.java和FireFox.java三个文件中:
Chrome.java:
Firefox.java:
IE.java:
最后一步,写主类。我循环三次,用父类对象依次初始化每个子类,并用set和get赋值取值,最后打印结果:
父类Browser虽然有个对象引用b,但b里存的都是子类对象的地址。b存谁的地址就指向谁,也就访问谁的变量调用谁的方法。这就是多态的一个典型应用。通过它我们就不用再为每一个子类写个对象引用了。
回到刚才说我懒得那个问题,父类方法getActualResponseTime要不是因为需要字符串返回值我连return都不写,就让它空着,反正父类方法被继承后很大可能性被重写,而且指向子类对象后,就只能调用子类方法。空着就空着呗,我倒觉得没什么,可java设计者一看,觉得不美观,就介绍了另一种方法来避免空着的方法,叫作抽象方法。格式是:
abstract是抽象的意思,就是没有实体。你会发现,抽象方法是没有大括号也没有内容的,只需要在最后加一个分号。在这个例子里我们可以把父类的getActualResponseTime方法写成一个抽象方法,原因刚才说了,因为我们不需要实体:
可是我们发现报错了,因为类出了问题,它说抽象方法需要声明在抽象类中:
那什么是抽象类呢?一句话,包含抽象方法的类就叫做抽象类。只因为类中有抽象方法,所以类必须被定义成抽象类。我们把类前面加上abstract,发现没错误了:
所以,只要类中有抽象方法,则类必须被定义成抽象类。反过来呢?不用。抽象类中可以包含一般方法,甚至可以没有任何抽象方法。这是第一点。这点跟之前咱们讲的什么比较像?是不是静态变量静态方法之间的关系?如果忘记的可以参考之前讨论的关于静态变量和静态方法相关的知识点。
第二点,抽象类也可以被继承。但由于抽象方法没有方法体,所以在子类中必须重写。这点也好理解,抽象方法就好比是一个半成品,你在父类中写个半成品当然要在子类中把它写全呀,否则你定义它干嘛?
最后一点,抽象类的对象不能被实例化,会报错。因为抽象类可能包含抽象方法,尽管有实体,但毕竟是抽象的,无法被实例化。鉴于这一点,java中的对象也分为可实例化对象和不可实例化对象。很明显,咱们之前说的都叫可实例化的对象,它们又被称为类实例。所以有时候你看一些书上和教材上管对象又叫做类实例,特指的就是可实例化的对象;而抽象类的对象不能被实例化,你不能管它们叫类实例,这点一定要记清。
这篇又说了不少东西,讨论了面向对象的最后一个特点 - 多态,又捎带手讲了抽象类和抽象方法。有人就说了,好烦呐,明明抽象类,你还可以包含一般方法甚至不包含抽象方法,这不是挂羊头卖狗肉?就不能只包含抽象方法?于是一个比抽象类更抽象的东西产生了,也是我们在面向对象这个大范围内介绍的最后一个概念 – 接口。
这篇文章的源代码是BrowserInheritance项目。
本篇知识点及注意事项:
1. 重载和重写区别:重载和继承没关系,它只是允许某一个类中同名方法存在,并且这些同名方法的参数独一无二。重写则是把从父类继承过来的方法重新编写里面的内容,方法的返回类型,方法名和参数内容必须完全一致。
2. 多态就是一个对象的多种状态。一个父类对象引用可以指向自己的对象,也可以指向继承它的子类的对象。指向自己对象时因为存的是自己对象的地址,当前状态是自己,所以调用自己的属性或方法;指向某个继承它的子类的对象时因为存的是子类对象的地址,状态就变成了子类,也就会调用子类的属性和方法。
3. 只要类中有抽象方法,则类必须被定义成抽象类,反过来不用。抽象类既可以包含抽象方法,也可以包含一般方法。
4. 抽象类也可以被继承。但由于抽象方法没有方法体,所以在子类中必须重写。
5. 抽象类的对象不能被实例化。