在 Java 编程世界里,继承是一个核心概念,它赋予了 Java 强大的代码复用性和可扩展性。今天,我们就来深入探讨一下 Java 继承。
一、什么是继承
继承,简单来说,就是一个类可以获取另一个类的属性和方法。被继承的类称为父类(超类、基类),继承的类称为子类(派生类)。通过继承,子类不仅拥有了父类的非私有属性和方法,还可以添加自己特有的属性和方法,或者重写父类的方法以满足特定需求。
继承是面向对象的三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法。直接看概念还是比较难理解,下面跟随我继续向下看,会有更深刻的理解。
二、继承的特点
Java只能单继承,不能多继承、但可以多层继承。这句从字面意思上比较难理解,我们来一点点来分析。
Java只能单继承,不能多继承,也就相当与一个儿子只能有一个父亲,如果子类继承了多个父类那么如果两个父类都有同一个方法名,那么子类到底该调取哪一个呢,所以Java为了结果这个问题就规定只能单继承。
但可以多层继承,这句话的通俗一点理解也就是儿子-爸爸-爷爷之间的关系,儿子和爸爸之间是继承关系,爸爸和爷爷之间也是继承关系,儿子和爷爷之间的关系是间接继承的关系,如下图所示。 看到这里可能就会有同学问了,那按照这样的规律来说是不是还有太爷爷,曾爷爷的关系啊?其实Java中每一个类都直接或者间接的继承于Object类,这个Object类也就是所有的祖宗, 这个Object类是每一个类默认继承的一个类,里面有很多已经写好的方法。
在这里我们随便创建一个对象然后调用他的方法时,你就会发现明明我没给这个对象写什么方法啊,但是我还可以直接调用这么多方法。其实在这里就是我们默认继承的Object类中的方法。
三、继承的书写方式
在这里我们学完了继承,但是该怎么写呢?实际上十分的简单,我们只需要在子类名后面加上一个extends + 父类名即可。
像这样就可以表示Dog类继承了Animal类,也就是说Dog类是子类,Animal类是父类。
四、继承的练习
看到这里想必你对继承已经大概理解了,那么就跟随我来做一个简单的练习。
首先我们遇见这种的继承关系首先要做的就是分析,为了我们以后自己开发或者是企业开发需要分析继承关系时更好的去理解,可以采用画图法的原则来进行分析。说白了也就说画个树状图,但是一定要从下往上画!
像这样我们画好了图像就可以进行代码的编写了,在这里一定要切记代码的编写一定是从上往下进行编写的。
Animal类
public class Animal {
public void eat() {
System.out.println("吃东西");
}
public void drink() {
System.out.println("睡觉");
}
}
Cat类
public class Cat extends Animal {
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
Dog类
public class Dog extends Animal {
public void watchHouse() {
System.out.println("狗看家");
}
}
Ragdoll类
public class Ragdoll extends Cat{
}
LiHua类
public class LiHua extends Cat{
}
Teddy类
public class Teddy extends Dog {
public void touch() {
System.out.println("泰迪在*****");
}
}
Husky类
public class Husky extends Dog{
public void braekhome() {
System.out.println("哈士奇在柴家");
}
}
好了,在此我们所有类都创建完成,我下一步就需要测试一下了
Test类
public class Test {
public static void main(String[] args) {
LiHua lh = new LiHua();
lh.eat();
lh.drink();
lh.catchMouse();
System.out.println("----------------------------------");
Husky hs = new Husky();
hs.eat();
hs.drink();
hs.braekhome();
}
}
运行结果如下:
在这里发现运行结果正常,但是在这里up主还是要和大家强调一点,子类只能继承父类的非私有成员,什么是非私有成员?在上述父类中写的方法都是用public修饰的,如果public改成private那么就代表是私有的,子类就不能进行调用了。如果理解不了那就更通俗一点说,爸爸的私房钱能给儿子花吗?是不是私房钱都自己偷偷留着用的,不能给儿子花。
五、子类到底能继承父类中哪些内容?
子类能继承父类哪些东西首先要看的就说父类到底有什么,那么咱们一起从三个层面来看。
(1)构造方法
不管是父类的构造方法是私有还是非私有的,子类都不能继承。我们来实践试一下。
public class Test {
public static void main(String[] args) {
//利用无参构造创建子类对象
Zi z1 = new Zi();
//利用带参构造创建子类对象
Zi z2 = new Zi("zhangsan", 23);
}
}
class Fu {
String name;
int age;
public Fu() {}
public Fu(String name, int age) {
this.name = name;
this.age = age;
}
}
class Zi extends Fu {
}
我们在idea上写如上代码,就会发现有报错,为什么会报错呢,实际上就是子类不能继承父类的构造方法。
在这里就会有细心的同学去问下面带参数的构造方法报错,上面无参的构造方法为什么不报错呢,明明他也是构造方法啊?如果你有这样想法说明思考的还不错,实际上在Java中,如果一个类中没有构造方法,虚拟机会默认的给你添加一个空参构造,也就是说上面的空参构造是子类自己默认有的,而不是继承于父类的。
(2)成员变量
对于成员变量来说,不管是私有还是非私有的都是可以直接继承的,但是但是但是但是,私有的不能直接用。需要用this或其他方法才能够进行使用。
用一个简单的例子来看,父亲有一个私有的成员变量,儿子虽然继承到了,但是不能直接使用,需要特定的钥匙解锁才能够使用,这个钥匙是什么呢其实就是我们常用的get和set方法。
(3)成员方法
在这里我们设想一种情况,我想在a对象中调取方法c,该怎么办?这里会有人想是A先找到B看看有没有,然后再看看C有没有。
那你如果是这么想的我再给你换一种情况,在A对象去找方法p呢,是不是看起来超级麻烦,如果你觉得不麻烦,那JVM虚拟机都会说:“你**想累死我啊!”。
于是就出现一种方式,就是在最开始的父类种创建一种虚方法表,那什么是虚方法表呢?就是非private、非static、非final的方法放在一张表里,换句话来说就是常用的方法表,然后再传给自己的儿子,儿子再把自己的虚方法表加如入其中再传给儿子的儿子,不断的向下传。
有了这个概念就子类再想调用什么方法就直接可以查找虚方法表了。
六、继承中的访问特点
(1)成员变量
访问成员变量主要就遵循一个就近原则:谁离我近,我就找谁,如果还没有就去父类中找。如果出现重名的我们可以采用this关键词,那问题就出现了,咋找父类里面的重命名的成员变量啊
在这里我给大家介绍一个关键字的用法,super:在 Java 里,super关键字用于在子类中访问父类的成员,它有以下重要用
访问父类成员变量:当子类中定义了与父类同名的变量时,子类会隐藏父类的该变量。此时若想访问父类的同名变量,就需要借助super关键字。例如:
class Parent {
int value = 10;
}
class Child extends Parent {
int value = 20;
public void printValues() {
System.out.println("子类中的value: " + value);
System.out.println("父类中的value: " + super.value);
}
}
在Child类的printValue方法中,value访问的是子类自身的变量,而super.value访问的是父类Parent中的变量。
综上所述,继承中成员变量访问原则就是遵循:就近原则:先在局部位置找,本类成员位置找,父类成员位置找,逐级向上找。如果出现重名怎么办,采用this和super关键字。
System.out.println(name);
System.out.println(this.name);
System.out.println(super.name);
(2)成员方法
访问成员方法和访问成员变量其实一样,遵循就近原则:谁离我近,我就找谁,如果还没有就去父类中找。
class Person {
public void eat() {
System.out.println("吃米饭,吃菜");
}
public void drink() {
System.out.println("喝开水");
}
}
class Student extends Person {
public void lunch() {
//先在本类中查看eat和drink方法,就会调用子类的,如果没有,就会调用从父类中继承下来的eat和drink方法
this.eat();
this.drink();
super.eat();
super.drink();
}
}
成员变量重名我们可以去解决,成员方法重名同样也可以解决。但是两个方法名一样其实也就是方法的重写。在Java中,当父类的方法不能满足子类的需求时,就需要方法的重写,当子类里出现了和父类一模一样的方法声明时,我们就称这个子类的方法是重写的。
有一个特别注意的点,重写的方法上面建议加一个注解@Override放在重写方法的上面,这样JVM虚拟机就可以直接知道这个方法是重写的了,并且它还可以校验重写方法是否正确,如果错误的话注解上就会出现红色波浪线,表示语法有错误。建议大家在重写方法前都要加一个@Override这样不仅安全,还显得代码十分高雅,权威!
在这里还有强调一下,重写方法的名字和形参列表一定要和父类中的一致,只有加到虚方法表里面的方法才能够被重写,也就说像那种私有的private或者是static这种方法都不能被继承更谈不到重写了。
(3)构造方法
父类中的构造方法不会被子类继承。
子类中所有的构造方法默认先访问父类中的无参构造,再执行自己。看到这想必很多读者会有点迷惑,Java为啥要这样设计?其实先访问父类中的无参构造是为了初始化,然后就可以继承到父类中的变量和方法了,如果没有初始化,子类就无法继承到父类中的内容了。那我们该怎么调用父类的构造方法呢?其实子类构造方法第一行语句默认都是super(),不写的话也是存在的,因为JVM虚拟机默认给我生成。但是如果想调用父类的有参构造方法,就必须手动的书写super进行调用。
最后结果显示为
七、this和super关键字
- this:代表本类对象的引用(this关键字指向调用该方法的对象一般我们是在当前类中使用this关键字所以我们常说this代表本类对象的引用)
- super:代表父类存储空间的标识(可以理解为父类对象引用)
二者的异同点
- 相同点:在构造方法中,
super(参数列表)
和this(参数列表)
都必须是构造方法的第一条语句。 - 不同点:
this
指向当前对象本身,super
指向当前对象的父类对象。this
用于访问本类的属性和方法、调用本类其他构造方法;super
用于访问父类的属性和方法、调用父类构造方法。this
和super
不能同时出现在同一个构造函数中,因为this
调用其他构造函数时,被调用的构造函数也会有super
语句,同时出现会导致逻辑混乱,编译器也不允许。this()
和super()
都不可以在static
环境(如静态方法、静态代码块)中使用,因为它们都与对象实例相关。