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

本文介绍了Java中的继承、抽象类和接口的概念、语法及使用场景。通过实例展示了如何使用继承减少代码冗余,解释了super关键字的用途,讨论了final关键字在继承中的作用。接着,探讨了抽象类的意义,强调了抽象类不能实例化,以及抽象方法的使用规则。最后,讲解了接口的重要性,包括接口的多继承特性,并对比了抽象类与接口的区别。
摘要由CSDN通过智能技术生成

前言

在面试的时候,面试官经常会问: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( )调用父类构造方法

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)抽象类也是类,可以有自己的构造方法,普通的成员变量与成员方法
(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,这个关键字修饰了抽象方法之后,抽象方法就只能在抽线类中被调用了,而我们创建的普通子类抽象方法的类外,故而无法访问。

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

对于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 

抽象类与接口的区别

在这里插入图片描述

结语

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

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

  • 12
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值