从零开始学Java之探究继承的特性及sealed+permits阻止继承

作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦

CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在上一篇文章中,壹哥给大家讲解了面向对象三大特征之一的封装,现在我们还有另外的两个特征没有了解。在今天的这篇文章中,壹哥会给大家讲解面向对象的第二大特征--继承!我们之前操作的类一般都是单个的类,还没有怎么同时处理过两个类,而从继承的知识点开始,我们就会处理父子两个类之间的关系了。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【5400】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

一. 继承简介

1. 概述

在日常生活中,“继承”是施方的一种赠与,受方的一种获得,是将一方所拥有的东西给予另一方。

2. 概念

开发中的”继承“,其实和我们日常生活中所熟知的含义类似,代表着子类可以从父类中得到的承接。

在Java中,继承表示子类能够承接父类的特征和行为,使得子类对象(实例)具有父类的成员属性。或者子类可以从父类继承方法,使得子类具有父类相同的行为,所以继承是类与类之间特征(属性)和行为(方法)的一种赠与或获得。继承能让我们创建出带有等级层次的类, 两个类之间的继承会满足“is a”的关系,如下图所示:

Java中的继承是对已存在的类进行扩展,从而产生新的类。已经存在的类称为 父类、基类或超类 ,而新产生的类称为 子类或派生类 。在子类中,不仅包含父类的属性和方法,还可以增加新的属性和方法。

3. 优缺点

继承能够减少代码的冗余,提高代码复用性,具有以下优点:

  1. 实现了代码共享,减少了创建类的工作量,使子类可以拥有父类的方法和属性;
  2. 提高了代码的维护性和可重用性;
  3. 提高了代码的可扩展性,更好的实现父类的方法。

但继承也并非全是优点,毕竟这个世界上没有十全十美的东西,也有如下一些缺点:

  1. 继承具有侵入性。只要继承,就必须拥有父类的非私有属性和方法;
  2. 降低了代码的灵活性。子类拥有父类的属性和方法后就会多了一些约束。
  3. 提高了代码的耦合性(应该高内聚低耦合)。父类的常量、变量和方法被修改时,也要考虑对子类进行修改,有可能会导致大段的代码被重构。

4. 使用特性

继承在使用时,具有如下特性,需要我们牢牢掌握:

  • 子类继承父类,可以继承父类中的属性和方法,即儿子可以继承爹的特征、遗产;
  • 子类可以拥有自己独有的属性和方法,即儿子可以有自己的个性;
  • 只能单继承,java中一个子类只能继承一个父类,但一个父类可以拥有多个子类。即一个儿子只能有一个亲爹,但一个爹可以有多个儿子;
  • 多重继承结构,父类还可以继承另外一个类。Java中最大的父类是Object,如果一个类没有显式地标明继承自哪个父类,默认都是Object的子类。即儿子有爹,爹也有自己的爹......最终有个老祖宗是Object,这是根!

5. 注意事项

但是我们要注意,虽然子类继承父类时,很多属性和方法都能继承过来,但也有一些内容无法继承,主要是以下几点:

  • 构造方法不能被继承,即生成父类对象的方法不能传给儿子,这样就”乱伦“了;
  • 父类的私有属性不能被继承,即爹的私有财产小金库不能继承给儿子;
  • 父类中使用默认修饰符修饰的属性和方法,在不同包的子类中不能被继承;
  • 使用final声明的类是最终类,也不能被继承。

在继承时,需要考虑父类中的访问修饰符问题。关于访问修饰符,壹哥在之前的文章中就给大家讲过,你还能想起来吗?看看下面这个表格回忆一下吧。

修饰符 本类 本包(不同类) 不同包子类 其他
private ✔️
默认的 ✔️ ✔️
protected ✔️ ✔️ ✔️
public ✔️ ✔️ ✔️ ✔️

类的继承不会改变类成员的访问权限。 也就是说,如果父类的成员是公有的、被保护的或默认的,它的子类仍具有相应的这些特性,且子类不能继承父类的构造方法。

了解了关于继承的这些内容之后,接下来我们再通过一些代码案例来实操一下吧。

二. 代码实现

1. 基本语法

首先我们来看看继承的基本语法。

class 父类 {
    ...
}
 
class 子类 extends 父类 {
    ...
}
复制代码

extends关键字直接跟在子类名称之后,后面是子类要继承的父类名称。

2. extends关键字

Java中的继承主要是通过extends关键字来实现。extends的英文意思是扩展,而不是继承。 extends很好地体现了子类和父类的关系,即子类是对父类的扩展,子类是一种特殊的父类。从这个角度看,使用“继承”这个词来描述子类和父类的关系是错误的,所以用“扩展”更恰当。

Java类的继承是单一继承,即一个子类只能拥有一个父类。 如果一个类没有明确地继承某个别的类,编译器会自动加上extends Object,即默认继承Object(在java.lang包中,不需要手动import导包 ) 祖先类。所以,除了Object之外的任何类,都会继承某个类,只有Object没有父类。

另外我们也可以利用implements关键字实现接口,这其实也是一种变相的继承,壹哥后面会再给大家单独讲解接口的实现。所以很多地方在介绍Java单继承时,会说Java类只能有一个父类,其实严格地说,这种说法是不准确的!应该是一个类只能有一个直接父类,但它可以有多个间接的父类。 比如儿子只能有一个亲爹,但是爷爷、老爷爷、老老爷爷等也是儿子的“父类”,“父辈”,这属于间接父类。

3. 需求分析

接下来壹哥通过一个案例,让大家来看看该如何进行继承的实现。这里我对动物的共性做了一些抽象,如下图所示:

从上图中,我们可以看到这些不同的动物有一些共同的属性,比如“品种、年龄、性别”;也有一些共同的方法,比如吃、睡等。但不同的动物也有会一些个性化的行为或特征,比如鱼可以游泳,鸟会飞,狗会跑,蛇会爬。那么如果让我们来设计一个程序对动物进行描述,就需要对他们的特征和行为进行抽象归纳。

所以根据上图,我们可以总结出一些基本的规律:如果我们想实现继承,需要把使用到的多个具体类,进行共性的抽取,进而定义父类。在一组相同或类似的类中,抽取出共性的特征和行为定义在父类中,实现代码的重用。

4. 代码实现

那么具体该怎么进行代码实现呢?我们来参考下面这些案例吧。

4.1 Animal父类

我们先来定义一个Animal父类,在这里定义一些共同的属性和方法。

/**
 * @author 一一哥Sun
 * 定义父类
 */
public class Animal {
    //定义公共属性
    String name;
    int age;
    String type;

    //定义公共方法
    public void sleep() {
        System.out.println("睡觉...");
    }

    public void eat() {
        System.out.println("吃饭...");
    }
}
复制代码

在OOP面向对象的术语中,我们可以 Animal 称为父类(parent class)、超类(super class)或者基类(base class);把 Cat/Dog等 称为子类(subclass)、扩展类(extended class)。

4.2 定义子类Cat

我们再来定义一个子类Cat。子类会从父类中继承共同的属性和方法,但不能继承父类的构造方法和私有属性,子类中可以定义自己特有的属性和方法。

/**
 * @author 一一哥Sun
 * 定义子类
 */
public class Cat extends Animal{
    //从父类中继承共同的属性和方法,但不能继承父类的构造方法和私有属性!
    //定义独有属性
    String color;

    //定义独有方法
    public void catchMouse() {
        System.out.println("抓老鼠...");
    }
}
复制代码

子类继承父类之后,就具有了父类中的属性和方法,子类不用再重复地编写这些代码。我们只需要为子类编写新增的功能即可,这样代码的可维护性和复用性也就提高了,代码也更加简洁了。

4.3 定义子类Dog

我们再来定义第二个子类Dog。

/**
 * @author 一一哥Sun
 * 定义子类
 */
public class Dog extends Animal{
    //定义子类独有属性
    String color;

    //定义子类独有方法
    public void lookHome() {
        System.out.println("看家...");
    }
}
复制代码

4.4 效果测试

接下来我们在main()方法中对上面的继承关系进行测试。

/**
 * @author 一一哥Sun
 * 测试继承
 */
public class ExtendTest {
    public static void main(String[] args) {
	Dog dog = new Dog();
        //使用父类继承下来的属性
        dog.name = "旺财";
        dog.type = "泰迪";
        dog.age = 3;
        //使用子类独有属性
        dog.color = "黄色";

        System.out.println("姓名为:"+dog.name+",品种为:"+dog.type+",毛色为:"+dog.color);
        //使用父类继承下来的方法
        dog.eat();
        dog.sleep();
        //使用子类独有方法
        dog.lookHome();
    }
}
复制代码

三. 几种不能继承的情况

1. 构造方法不能被继承

子类不能继承父类的构造方法,只能隐式或显式地调用。 如果父类的构造方法带有参数,继承的子类可以在自己的构造方法中,显式地利用 super关键字调用父类的构造方法,并配以适当的参数列表。

如果父类的构造方法没有参数,则子类的构造方法中可以不用 super关键字调用父类的构造方法,系统会自动调用父类的无参构造方法。

接下来我们再通过一个案例来进行说明。

1.1 定义父类

这里定义了一个带有2个构造方法的父类,如下所示:

/**
 * @author 一一哥Sun 
 * 父类
 */
public class Father {
    //私有属性不能被继承
    private String name;
    private int age;
    private String secret;
	
    //公开的属性--姓氏。公开属性可以被继承
    public String familyname;

    //如果在父类中存在有参的构造方法,但没有重载无参的构造方法,那么子类中必须存在有参的构造方法。否则会产生如下异常:
    //Implicit super constructor Father() is undefined. Must explicitly invoke another constructor
    public Father() {
	System.out.println("Father父类的无参构造方法");
    }

    public Father(String name, int age) {
	this.name = name;
	this.age = age;
	System.out.println("Father父类的有参构造方法");
    }
}
复制代码

如果在父类中存在有参的构造方法,但没有重载无参的构造方法, 那么子类中必须显式地调用父类有参的构造方法。 否则会产生如下异常:

Implicit super constructor Father() is undefined. Must explicitly invoke another constructor。

这是因为子类中默认会去调用父类中无参的构造方法,而在父类中如果没有无参的构造方法就会出错。

1.2 定义子类

接着我们定义一个带有2个构造方法的子类,如下所示:

/**
 * @author 一一哥Sun
 * 定义子类
 */
public class Son extends Father{

    //子类自己的私有属性
    private String hobby;
    private int height;
    private String job;

    //如果在父类中存在有参的构造方法,但没有重载无参的构造方法,那么子类中必须显式地调用父类有参的构造方法。否则会产生如下异常:
    //Implicit super constructor Father() is undefined. Must explicitly invoke another constructor
    public Son() {
        //不用显式调用super();方法
        //super();
        //父类中存在有参构造方法,但没有重载无参构造方法,需要显式调用如下方法。
	//super("",11);
	//这里会隐式地调用父类的无参数构造方法
	System.out.println("Son子类的无参构造方法");
    }

    public Son(String name,int age,String job) {
	//super();
	//子类显式地调用父类中带有参数的构造方法
	super(name, age);
	this.job = job;
	System.out.println("Son子类的有参构造方法"+job);
    }
}
复制代码

从这些案例中我们可以知道,子类不会继承父类任何的构造方法,子类默认的构造方法是Java自动生成的,不是继承来的!

1.3 测试类

这里定义一个测试类,测试上面的继承关系,如下所示:

/**
 * @author 一一哥Sun
 */
public class FatherTest {
    public static void main(String[] args) {
	//创建第一个子类对象
	Son son1 = new Son();
		
	//创建第二个子类对象
	Son son2 = new Son("小棒",38,"盗窃");
    }
}
复制代码

执行结果如下图所示:

2. 私有属性不能被继承

父类中的私有属性不能被子类继承,公开的属性是可以的,如下图所示:

但private私有的修饰符,有可能会使得继承的作用被削弱。所以有时候为了让子类可以访问父类的某些字段,我们可以把private改为protected关键词,用protected修饰的字段可以被子类访问。protected 关键字可以把字段和方法的访问权限控制在继承树的内部,一个 protected 字段和方法可以被其子类,以及子类的子类所访问。

另外父类中使用默认修饰符修饰的属性和方法,在不同包的子类中也不能被继承。

3. final类不能被继承

假如我们把上面的父类进行调整,用final关键字修饰Father类,如下图所示:

此时子类就会出现如下图所示的提示信息:

“The type Son cannot subclass the final class Father”,即子类不能继承final类

四. 新特性(拓展)--sealed+permits阻止继承

1. 概述

一般情况下,只要一个类没有被 final 修饰,那么任何类都可以继承该类。 但从JDK 15开始,允许使用sealed(密封)关键字来修饰class,并利用permits(许可)关键字明确写出能够从该类继承的子类名称。

2. 示范案例

2.1 定义Shape类

/**
 * @author 一一哥Sun 
 * 定义一个父类的“形状类”
 *         
 * Permitted class Triangle does not declare demo14.Shape as direct super class
 */
public sealed class Shape permits Rect, Circle, Triangle {
    ...
}
复制代码

Shape类是一个被sealed修饰的类,它指定了3个类Rect/Circle/Triangle可以继承它,我们是利用permits关键字实现允许继承。sealed 类主要用于一些框架中,防止继承被滥用!

2.2 定义Rect类

/**
 * @author 一一哥Sun
 * 定义一个子类的“矩形类”
 * 
 * The class Rect with a sealed direct superclass or a sealed direct superinterface Shape 
 * should be declared either final, sealed, or non-sealed
 */
public final class Rect extends Shape{
}
复制代码

Rect类可以继承Shape类,因为 Rect类 Shape permits允许 列表中的一个,属于白名单中的类。但如果是别的不在permits列表中的类就会报错。

2.3 定义Circle类

/**
 * @author 一一哥Sun
 * 定义一个子类的“矩形类”
 * The class Rect with a sealed direct superclass or a sealed direct superinterface Shape 
 * should be declared either final, sealed, or non-sealed
 */
public final class Circle extends Shape{
    ...
}
复制代码

2.4 定义Triangle类

/**
 * @author 一一哥Sun
 * 定义一个子类的“矩形类”
 * 
 * The class Rect with a sealed direct superclass or a sealed direct superinterface Shape 
 * should be declared either final, sealed, or non-sealed
 */
public final class Triangle extends Shape{
    ...
}
复制代码

2.5 定义Ellipse类

/**
 * @author 一一哥Sun
 * 定义一个子类的“三角形类”
 * 
 * The type Ellipse extending a sealed class Shape should be a permitted subtype of Shape
 */
public final class Ellipse extends Shape{
    //Compile error: class is not allowed to extend sealed class: Shape
    ...
}
复制代码

Ellipse类没有出现在Shape的permits列表中,就不能继承Shape类,否则就会报错:The type Ellipse extending a sealed class Shape should be a permitted subtype of Shape。Compile error: class is not allowed to extend sealed class: Shape

------------------------------正片已结束,来根事后烟----------------------------

五. 结语

至此,壹哥就把Java里的继承给大家讲解完毕了,现在你知道继承有什么特点了吗?关于继承,有如下几个要点:

  • 继承是面向对象编程的一种强大的代码复用方式;
  • Java只允许单继承,所有类最终的根类都是 Object, C++可以有多重继承(即一个子类有多个直接父类)
  • 父类中的 private 成员在子类中是不可见的,子类中不能直接使用它们;
  • protected 允许子类访问父类的字段和方法;
  • 在子类的构造方法中可以通过 super() 调用父类的构造方法;
  • 子类一般比父类包含更多的属性和方法;
  • 子类和父类的关系是is a,has关系不能用继承,但也并不是所有符合“is-a”关系的都应该用继承。 如正方形是一个矩形,但不能让正方形类来继承矩形类,因为正方形不能从矩形扩展得到任何东西。正确的继承关系是正方形类继承图形类。

关于继承的内容其实还有很多,比如super关键字的详情、父子类之间的转型问题等,更多关于继承的内容,壹哥会在后面的文章中专门进行讲解。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

六. 配套视频

如果你不习惯阅读技术文章,或是对文中的技术概念不能很好地理解,可以来看看壹哥帮你筛选出的视频教程。与本文配套的Java学习视频,链接如下:

player.bilibili.com/player.html…

七. 今日作业

1. 第一题

设计一个Person类和Teacher类,理顺两者之间的关系,说说子类对象的实例化过程。

本文正在参加「金石计划」

猜你喜欢

转载自juejin.im/post/7214880895092719671