初识Java【5】——继承、抽象类、接口
前言
在面试的时候,面试官经常会问:Java的三大特性是什么?其答案就是:继承、封装、多态。然而笔者并不打算完全按这个顺序讲下去,本文将会从继承开始介绍,再一步一步拓展下去。在我们日常工作中,最常用到的其实是接口,而接口的逻辑源自继承开始,再到抽象类,最后才是接口本身。我认为这样会更好理解一下,如果你想了解就继续阅读下文吧!
一、继承
1.初识继承
首先,让我们一起来编写一段关于动物的代码。要求:需要写出至少三个的动物,每一个动物都要有自己的name
,且每一个动物都需要实现 eat() say()
这两个方法。看到这个要求的时候,基础好的读者便会不屑一顾,实现这代码太简单了吧?只不过烦琐了一些。
class Dog {
public String name;
public Dog(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "在吃东西");
}
public void say() {
System.out.println(name + "在说啥呢?");
}
}
class Cat {
public String name;
public Cat(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "在吃东西");
}
public void say() {
System.out.println( name + "在说啥呢?");
}
}
class Bird {
public String name;
public Bird(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + "在吃东西");
}
public void say() {
System.out.println(name + "在说啥呢?");
}
}
的确,要实现上方的要求并不困难,然而我们在编写的过程中,却隐隐发现了一个很大的问题:代码冗余,拉低开发效率。我们可以看到name eat() say()
这两个成员方法,在上方的三个类中都出现了。如果只是写三个动物类,我们或许还能接受,但如果我们需要整合所有的动物呢?这三个相同的成员变量,我们却要写成千上万次。这是在浪费生命,属于是间接谋杀呀!!!
那么我们能不能提高一下代码的复用性?用什么办法来解决呢?这就要介绍到我们本文的主角之一:继承!
继承的作用就是:提高代码的复用性。
为了解决上方的问题,我们就可以使用继承的方法。修改代码如下所示。
class Animal {
public String name;
public void eat(String name) {
System.out.println(name + "在吃东西!");
}
public void say(String name) {
System.out.println(name + "在说啥呢?");
}
}
class Dog extends Animal {
}
class Cat extends Animal {
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "旺财";
dog.eat(dog.name);
dog.say(dog.name);
System.out.println("===========================");
Cat cat = new Cat();
cat.name = "蛋面";
cat.eat(cat.name);
cat.say(cat.name);
}
}
此时运行的效果如何呢?让我们一起来看看吧~
以上代码不理解没有关系,而我们能够直观地感受到:代码变得简洁了。看来这个继承确实有大用处呀!那么接下来,笔者正式向大家介绍继承。
2.何为继承
继承(inheritance)机制:是一种在面向对象程序设计中可以提高代码复用性的手段。
(1)继承的语法
继承的语法非常简单,只需要用到extends
关键字即可,详细格式如下:
class 父类类名 {
// 子类共有的成员变量 与 成员方法
}
class 子类类名 extends 父类类名 {
// 子类特有的成员变量 与 成员方法
}
在继承中,父类也叫:超类、基类,对应上方示例就是Animal
;而子类也叫:派生类,对应上方示例就是Dog Cat
。
继承虽好,却也不能滥用,主要有以下两条:
(1)最好不要超过三层。因此我们会在第三层的子类前加
final
关键字;
(2)一次只能继承一个父类,如果需要“多继承”,需要用到接口;
在了解完何为继承之后,我们就要来探讨如何使用继承的问题了。
(2)继承的使用
最为基本的使用我们已经了解的了,在减少代码的冗余上,继承确实非常强大。但是,我们也不要忘记一个点:Dog
在继承之后,仍然有属于自己的特性! 这就引来了两个问题:1.子类应该如何添加属于自己的成员 ; 2.子类如何写出共性中的个性?
要解决第一个问题非常简单,直接在子类中写就可以了,代码示例如下:
class Animal {
public String name;
public void eat(String name) {
System.out.println(name + "在吃东西!");
}
public void say(String name) {
System.out.println(name + "在说啥呢?");
}
}
class Dog extends Animal {
public int age;
public void run(String name) {
System.out.println(name + "在飞奔着!");
}
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
// 继承自父类的成员
dog.name = "旺财";
dog.eat(dog.name);
dog.say(dog.name);
// 子类特有的成员
dog.age = 1;
dog.run(dog.name);
}
}
在看完上方代码之后,笔者还想提醒一点:子类要有自己的属性为好,否则就跟父类没什么区别了,就失去了创建子类的意义。
至于第二个问题,我们需要用到:方法重写,这个功能来解决。方法重写,也是面试中会出现的题目,一般跟方法重载一起出现,其实两者没啥关系,就是名字长得像而已,不了解方法重载的读者可以转跳下面这篇文章。
初识Java【3】——方法与数组(含方法重载的介绍)
我们先来用上方法重写这个功能吧,实现代码如下:
class Animal {
public String name;
public void eat(String name) {
System.out.println(name + "在吃东西!");
}
public void say(String name) {
System.out.println(name + "在说啥呢?");
}
}
class Dog extends Animal {
public int age;
@Override
public void eat(String name) {
System.out.println(name + ",对就是我狗爷,正在吃狗粮!");
}
@Override
public void say(String name) {
System.out.println((name + ",对就是我狗爷,正在狗叫!"));
}
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "旺财";
dog.eat(dog.name);
dog.say(dog.name);
}
}
我们能够发现:在添加了 @Override
这个标签之后,运行的结果的确发生了变化,这就是所谓的共性中的个性了。
方法重写
这个方法重写这么神奇,同时也是这么重要,那关于方法重写我们需要了解什么呢?请读者继续阅读下面的内容吧!
我们直接上一些错误示例,看看能不能找出一些规律。
第一个方法中,我们可以看到String name
这个参数被省去了;第二个方法中,我们将eat
的方法名修改成了eating
之后报的错;而第三个方法中,我们在参数列表中新增了int age
的形参。
通过这三个例子,我们可以感受到,其实要正确实现一个方法的重写,我们:只能改变方法体中的内容,而 返回类型、方法名、形参个数、形参顺序、形参的类型
都不能没修改!
3.继承中的访问
正常的访问,直接在main
方法中实例化子类对象,再用.
的方式进行访问即可,笔者就不重复演示了,详情参考上方的诸多示例。关于方法重写 ,我们其实也可以理解为:成员方法同名时访问的选择,很明显:在方法重名时,发生了方法重写,会访问子类的同名方法。那么,在父子类成员变量同名是呢?我们应该怎么办呢?
(1)父子类成员变量同名
在继承中,我们极有可能会遇到父子类成员同名的情况,这时候访问的是谁呢?
父子类成员同名时,优先访问子类成员
class Animal {
public int age = 1;
public String name = "Animal";
}
class Dog extends Animal {
public int age = 11;
public String name = "Dog";
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.age);
System.out.println(dog.name);
}
}
通过运行结果我们可以清楚知道:成员变量访问遵循就近原则,子类中有优先访问子类的,如果没有再向父类中找(都没有就报错)。
上面这些都简单,一句话:就近原则。但是应该会有不少倔强的读者就会想:我非要初始化子类后,直接去访问父类中的父子类同名成员不行吗?好问题!当然可以!这就要引出下面的super
关键字。
(2)super关键字
super
关键字就是用来通过子类中访问父类的成员。
既然能够访问父类的成员,哪怕的父子类成员同名也是允许的。super
关键字的用法跟this
的用法相似,有以下三点:
A.
super.data
访问父类成员变量
B.super.func( )
访问父类成员方法
C.super( )
调用父类构造方法
super
与this
虽然用法相似,但如果说:super
是父类的引用,这是错误的。接下来,让我们通过一些代码示例来理解一下super
关键字吧。
A.super.data
访问父类成员变量
class Animal {
public String name;
}
class Dog extends Animal {
public String name = "旺财";
public void testSuper() {
super.name = "来福"; // A.访问父类成员变量
System.out.println("父类中的name: " + super.name);
}
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
// 访问子类成员变量
dog.name = "旺财";
System.out.println("子类中的name:" + dog.name);
// 通过子类,访问父类成员变量
dog.testSuper();
}
}
B.super.func( )
访问父类成员方法
class Animal {
public void eat() {
System.out.println("父类中的eat()");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("子类中的eat()");
}
public void testSuper() {
super.eat();
}
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog();
// 访问子类成员方法
dog.eat();
// 通过子类,访问父类成员方法
dog.testSuper();
}
}
C.super( )
调用父类构造方法
class Animal {
public Animal(String name) {
System.out.println("父类构造方法");
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
System.out.println("子类构造方法");
}
}
public class TestForBlog {
public static void main(String[] args) {
Dog dog = new Dog("旺财");
}
}
在访问父类的构造方法中,笔者并没有再将之单独放在一个子类成员方法中,并不是笔者不想放,而是:要想初始化父类,必须先在子类帮助父类完成构造,即必须要写super( )
。一般来说,调用了构造方法其实就实例化了一个对象,但这里比较特殊的是:父类并没有创建一个对象,只是子类初始化了从父类继承过来的属性。
那么这就引申出一个问题:我们能不能单独构造子类的构造方法呢 ?答案是:不能!
最后关于super
关键字还有一个值得注意的点:super
关键字只能在非静态方法中使用。这也相当于:super
关键字不能用在main
方法中使用。
(3)final关键字
上面提及到,在继承的时候,不应该超过三层,否则代码的可读性就会变差,因此我们需要在第三层的类前加上final
关键字,此处我们详细介绍final
关键字,这也是我们在面试中会出现的基础考题。
我们可以从下面三个角度去描述final
这个关键字
A.基本数据类型被
final
修饰后会变成一个常量
B.引用类型被
final
修饰后其指向的地址不可修改,但地址上的内容可以修改
C.
final
修饰的类的情况
(I)被修饰类的成员变量必须被赋值为常量
(II)被修饰类的成员方法不可被重写,但是可以被子类访问(前提是方法不能被private
修饰)
(III)类本身被修饰后不可被继承
非常值得一提的点是:如果被final
修饰的变量值已经确切知道后,那么这个变量会在编译时期就被初始化,否则就是在运行时期才完成初始化。
如果你想了解更多,以及验证上方的几点,可以点击下方这个超链接:程序员真的理解final关键字吗?
二、抽象类
1.抽象类的意义
在简单学习继承之后,我们接着看看抽象类。抽象类本身是不能被实例化的。这就令人费解了,一个类难道不就是创建出来实例化的吗?是的,但是抽象类不同,普通类本身就是对事物的一种抽象,但是抽象来要更加抽象,抽象类的作用就是原来被继承。
为什么要设计这样的抽象类呢?笔者可以简单举一个例子:
现在开发要求:设计一个动物类,而这个类至少有一百个方法,其子类必须继承所有的方法并重写为子类的实现方式,而且具体方法只能由子类实现。
在这种情况下,难道我们程序员在写子类的时候,就 一定能够保证我们自己可以正确写出所有的父类方法吗 ?一般来讲,我们是不要创建父类实例的,都是通过子类去实现具体的方法,而我们 能够保证自己一定不创建父类实例吗 ?
我想答案大家都很清楚,而在开发中利用编译器校验是非常有意义的,这能提高我们的开发效率,不至于一个小问题找很久。
2.抽象类的语法
那么抽象类的语法是怎么样的呢?请看下方的代码:
public abstract class 抽象类名字 {
abstrct public 方法返回类型 抽象方法名 (形参列表) ;
}
class 普通类类名 extends 抽象类名字 {
@Override
public abstrct 方法返回类型 抽象方法名 (形参列表) {
}
}
public abstract class 子类抽象类类名 extends 父类抽象类类名 {
// 可以不重写抽象方法,也可以重写
}
3.抽象类的使用
在抽象类的使用中,有若干点需要注意,首当其冲的就是开始时提到的:抽象类本身是不能被实例化的。
抽象类使用细节汇总
(1)抽象类不能被实例化
(2)如果一个类有抽象方法,这个类必须时抽象类,而抽象方法不需要有具体的实现
(3)普通类在继承抽象类之后,必须重写抽象类中的抽象方法,否则自己就要成为一个抽象类
(4)抽象类也是类,可以有自己的构造方法,普通的成员变量与成员方法
(5)abstract
不能 与private、static、final
关键字共同修饰一个类或一个方法
(6)abstract
不能修饰属性和构造器,只能修饰类、方法和接口。
上方的汇总中,除了第(3)点 与 第(4)点值得解释一下之外,其他的细节都比较好理解。
关于第(3)点,如果我们写了下方这样的代码,也就是故意让普通子类不去重写抽象类中的抽象方法 。
编译器就会报出这样的错误,而解决方法也很简单,在普通子类中重写抽象方法。
public abstract class Animal {
abstract public void eat();
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("这是动物中的狗在吃饭!");
}
}
那么后面那句否则自己就要成为一个抽象类,又是什么意思呢?大家直接看下方的代码示例就会明白了。
public abstract class Animal {
abstract public void eat();
}
abstract class Dog extends Animal {
abstract public void eat();
}
class DogOne extends Dog {
@Override
public void eat() {
System.out.println("这是中华田园犬在吃饭!");
}
}
class DogTwo extends Dog {
@Override
public void eat() {
System.out.println("这是藏獒在吃饭!");
}
}
运行结果如下所示:
我们可以看到Dog
这个类在上方那段代码中就又充当了一个抽象类的角色。当然,如果不重写最开始Animal
中的eat()
也是可行的。
关于第(4)点,其实可以总结成一句话:使普通子类无法抽象抽象方法。
对于private
,这个关键字修饰了抽象方法之后,抽象方法就只能在抽线类中被调用了,而我们创建的普通子类抽象方法的类外,故而无法访问。
对于final
,被final
修饰的方法是不能被重写的,那么这就跟完全与要求的普通子类必须重写抽象方法的规则相违背。
对于static
,我们曾经说过,static
是修饰类的,是为类服务的,当static
修饰抽象方法之后,此时该抽象方法只属于类。所以普通子类在调用抽象方法的时候,只能用抽象类去调用(Animal.eat()
或者Dog.eat()
的方式去调用),但是我们要求普通子类必须重写抽象父类的抽线方法,这两者就矛盾了。故而,static
也不可以修饰抽象方法。
三、接口
1.接口的意义
我们在学习完抽象类之后,已经初步感受到抽象类的强大了,但是我们在编码的过程中为什么又需要接口呢?这就是要讲到Java的尿性了,Java中的继承特性要求:一个类一次只能继承一个父类。
这个要求就会给我们带来很大的困难呀!比如说,我们要实现一个这样的要求:
(1)将奔跑这个动作写成一个抽象类,不同的动物奔跑的方式在子类中重写;
(2)再将吃东西这个动作写成一个抽象类,使得不同的动物吃饭的方式也具有个性化;
(3)最后要求写一个Dog
类,需要实现run() eat()
这两个方法。
当我们按照要求编写完成代码之后,编译器就给我们报了如上错误,翻译过来就是:咱们要么将 Dog
类变成一个抽象类,要么实现 Eating
中的抽象方法 。但是我们又只能继承一个,如果我们选择将Dog
类变成一个抽象类,那么我们就无法实现Dog
类的个性化方法。
那我们有没有办法解决这个问题呢?这就要介绍本文最后一个主角了——接口。为啥说接口可以解决问题呢?因为接口是可以实现多接口的。
这时候我们就会好奇,如果接口可以实现多接口的话那很好呀,但是接口是不是也可以实现抽象类的功能呢?那么接下来我们就先从接口的语法开始介绍起吧!
2.接口的语法
接口的语法非常简单,相当于只需要把class
换成interments
// 新建一个接口
public interface 接口名 {
......
}
// 继承一个接口
public class 类名 implements 接口名称 {
......
}
接下来笔者就用上新工具,和大家一起完成上面的那个要求吧!
首先我们需要创建一个接口类型,大家在命名接口名的时候,最好在名字前面写上I
这个字母,所命名的名字也最好用动词。这是阿里巴巴《Java开发手册-嵩山版》中建议的命名规范,如果公司有自己的命名规范,按照公司的命名规范命名即可。
最终新建的类与接口如下
// IRunning中的代码
public interface IRunning {
public abstract void run();
}
// IEating中的代码
public interface IEating {
void eat();
}
// Dog中的代码
public class Dog implements IRunning,IEating{
@Override
public void eat() {
System.out.println("小狗在吃东西!");
}
@Override
public void run() {
System.out.println("小狗在跑步!");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
dog.run();
}
}
代码运行结果如下所示。
到此为止,上方的要求总算是解决了,我们也认识到了接口这个功能的强大,那么接下来就让我们一起来认识一下接口吧,了解一下接口具体有什么特性。
3.接口的使用
接口的特性其实也是蛮多的,根据笔者的总结如下:
1.接口不能被实例化。
2.接口也是可以有变量的,但是只能用
public static final
修饰
3.每一个接口方法都被public abstract
修饰,其他都不可以。(Java8 后default
也可以)
4.接口中的方法不能由接口实现,都是抽象方法,必须由实现接口的类重写实现
5.接口可以实现多接口的功能,方式形如:
class 类名 implements 接口名,接口名
6.如果是实现接口的类不能重写接口的所有抽象方法,那此类需要变成一个抽象类
7.接口中不能有静态代码块和构造方法
未了解封装的读者可能不太理解第5点,可以先去了解一下default
的修饰范围后,再结合着接口的使用特性进行理解。
接口的多继承
我们之前谈到,一个类只能继承一个父类,但是在接口这里可不太相同,接口是可以实现多继承的。
具体的语法形式如下,其使用方法跟上方演示的代码一致,只是需要重写跟多的抽象方法,此处就不再重复更多的示例了。
interface 接口名 extends 接口名1,接口名2
抽象类与接口的区别
结语
写到这里,我们总算是理顺了从继承一路下来的抽象类、接口了。这些知识其实不算难,主要是一些语法规则我们需要去遵守,但是讲到最后还是熟能生巧,代码都是敲出来的,大家一定要多敲代码,结合着去理解。
最后,如果你觉得本文对你有帮助的话,就请点个赞支持一下博主吧!如果文中有任何不对或者疑惑的地方,希望不吝赐教。