初识Java【5】——继承、抽象类、接口

初识Java【5】——继承、抽象类、接口

  • 前言
  • 一、继承
    • 1.初识继承
    • 2.何为继承
      • (1)继承的语法
      • (2)继承的使用
        • 方法重写
    • 3.继承中的访问
      • (1)父子类成员变量同名
      • (2)super关键字
      • (3)final关键字
  • 二、抽象类
    • 1.抽象类的意义
    • 2.抽象类的语法
    • 3.抽象类的使用
  • 三、接口
    • 1.接口的意义
    • 2.接口的语法
    • 3.接口的使用
      • 接口的多继承
      • 抽象类与接口的区别
  • 结语

前言

在面试的时候,面试官经常会问: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);
    }
}

此时运行的效果如何呢?让我们一起来看看吧~
初识Java【5】——继承、抽象类、接口_第1张图片

以上代码不理解没有关系,而我们能够直观地感受到:代码变得简洁了。看来这个继承确实有大用处呀!那么接下来,笔者正式向大家介绍继承。

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这个标签之后,运行的结果的确发生了变化,这就是所谓的共性中的个性了。

在这里插入图片描述

方法重写

这个方法重写这么神奇,同时也是这么重要,那关于方法重写我们需要了解什么呢?请读者继续阅读下面的内容吧!

我们直接上一些错误示例,看看能不能找出一些规律。

初识Java【5】——继承、抽象类、接口_第2张图片
第一个方法中,我们可以看到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( )调用父类构造方法

superthis虽然用法相似,但如果说: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)抽象方法不能被private static final关键字修饰
(5)抽象类也是类,可以有自己的构造方法,普通的成员变量与成员方法

上方的汇总中,除了第(3)点 与 第(4)点值得解释一下之外,其他的细节都比较好理解。

关于第(3)点,如果我们写了下方这样的代码,也就是故意让普通子类不去重写抽象类中的抽象方法

初识Java【5】——继承、抽象类、接口_第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,这个关键字修饰了抽象方法之后,抽象方法就只能在抽线类中被调用了,而我们创建的普通子类抽象方法的类外,故而无法访问。

对于finalfinal修饰的方法是不能被重写的,那么这就跟完全与要求的普通子类必须重写抽象方法的规则相违背

对于static,我们曾经说过,static是修饰类的,是为类服务的,当static修饰抽象方法之后,此时该抽象方法只属于类。所以普通子类在调用抽象方法的时候,只能用抽象类去调用(Animal.eat()或者Dog.eat()的方式去调用),但是我们要求普通子类必须重写抽象父类的抽线方法,这两者就矛盾了。故而,static也不可以修饰抽象方法。

三、接口

1.接口的意义

我们在学习完抽象类之后,已经初步感受到抽象类的强大了,但是我们在编码的过程中为什么又需要接口呢?这就是要讲到Java的尿性了,Java中的继承特性要求:一个类一次只能继承一个父类

这个要求就会给我们带来很大的困难呀!比如说,我们要实现一个这样的要求:

(1)将奔跑这个动作写成一个抽象类,不同的动物奔跑的方式在子类中重写;
(2)再将吃东西这个动作写成一个抽象类,使得不同的动物吃饭的方式也具有个性化;
(3)最后要求写一个Dog类,需要实现run() eat()这两个方法。

初识Java【5】——继承、抽象类、接口_第4张图片

当我们按照要求编写完成代码之后,编译器就给我们报了如上错误,翻译过来就是:咱们要么将 Dog类变成一个抽象类,要么实现 Eating中的抽象方法 。但是我们又只能继承一个,如果我们选择将Dog类变成一个抽象类,那么我们就无法实现Dog类的个性化方法。

那我们有没有办法解决这个问题呢?这就要介绍本文最后一个主角了——接口。为啥说接口可以解决问题呢?因为接口是可以实现多接口的

这时候我们就会好奇,如果接口可以实现多接口的话那很好呀,但是接口是不是也可以实现抽象类的功能呢?那么接下来我们就先从接口的语法开始介绍起吧!

2.接口的语法

接口的语法非常简单,相当于只需要把class换成interments

// 新建一个接口
public interface 接口名 {
	......
}

// 继承一个接口
public class 类名 implements 接口名称 {
	......
}

接下来笔者就用上新工具,和大家一起完成上面的那个要求吧!

首先我们需要创建一个接口类型,大家在命名接口名的时候,最好在名字前面写上I这个字母,所命名的名字也最好用动词。这是阿里巴巴《Java开发手册-嵩山版》中建议的命名规范,如果公司有自己的命名规范,按照公司的命名规范命名即可。
初识Java【5】——继承、抽象类、接口_第5张图片

最终新建的类与接口如下
初识Java【5】——继承、抽象类、接口_第6张图片

// 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.接口中的方法不能由接口实现,都是抽象方法,必须由实现接口的类重写实现
4.每一个接口方法都被public abstract修饰,其他都不可以。
5.重写接口中的方法时,不能使用default访问权限修饰符修饰

6.接口可以实现多接口的功能,方式形如:class 类名 implements 接口名,接口名
7.如果是实现接口的类不能重写接口的所有抽象方法,那此类需要变成一个抽象类
8.接口中不能有静态代码块和构造方法

未了解封装的读者可能不太理解第5点,可以先去了解一下default的修饰范围后,再结合着接口的使用特性进行理解。

接口的多继承

我们之前谈到,一个类只能继承一个父类,但是在接口这里可不太相同,接口是可以实现多继承的

具体的语法形式如下,其使用方法跟上方演示的代码一致,只是需要重写跟多的抽象方法,此处就不再重复更多的示例了。

interface 接口名 extends 接口名1,接口名2 

抽象类与接口的区别

其实我们能感受到两者功能上有相类似的地方,但是它们最大的区别在于结构组成抽象类是可以包含普通变量和普通方法的,这个普通变量和普通方法可以被子类直接使用,不必重写;而接口是不包含普通方法的,子类必须重写所有抽象方法

具体的,我们还可以从访问权限、子类使用、子类限制、关系这几个角度去述说。

结语

写到这里,我们总算是理顺了从继承一路下来的抽象类、接口了。这些知识其实不算难,主要是一些语法规则我们需要去遵守,但是讲到最后还是熟能生巧,代码都是敲出来的,大家一定要多敲代码,结合着去理解。

最后,如果你觉得本文对你有帮助的话,就请点个赞支持一下博主吧!如果文中有任何不对或者疑惑的地方,希望不吝赐教。

你可能感兴趣的:(java,开发语言)