1、继承简介
- 继承体现的是一种类与类之间的关系;
- 我们可以使用已存在的类(也叫父类、基类)的定义作为基础来建立新类(也叫子类或者派生类);
- 新类的定义可以增加新的数据或新的功能,也可以调用父类的功能,但不能选择性地继承父类(必须承接父类所有开放的特征,是没有选择余地的);
- 满足“A is a B”的关系就可以形成继承关系。比如说 Dog is a animal,Student is a person等等。
- 在Java当中的继承只能是单继承,即只能继承一个父类,也就是说一个子类只能有一个唯一的父类。
2、继承的实现
在Java中我们是通过extends关键字来实现继承的。下面我们进行代码演示。
- 1.新建一个名为
AnimalProj
的Java项目,然后在项目中新建两个包,分别命名为:com.cxs,animal
、com.cxs.test
,然后在animal
的包下新建一个Animal
类作为父类。
/**
* 动物类,作为父类
* @author chaixingsi
*/
public class Animal {
private String name;
private int month;
private String species;
public Animal() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
// 吃东西
public void eat() {
System.out.println(this.getName() + "在吃东西!");
}
}
- 2.依旧在
animal
包下新建一个Cat
类。
/**
* 猫类,作为子类
* @author chaixingsi
*/
public class Cat extends Animal {
private double weight;
public Cat() {
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
// 猫还有自己特有方法:跑动
public void run() {
System.out.println(this.getName() + "是一只" + this.getSpecies() + ",它在快乐地奔跑!");
}
}
- 3.同理,在
animal
包下新建一个Dog
子类。这里我们在新建类是就可同时完成继承的编写,如下图所示。
/**
* 狗类,作为子类
* @author chaixingsi
*/
public class Dog extends Animal {
private String sex;
public Dog() {
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
// 独有的方法:睡觉
public void sleep() {
System.out.println(this.getName() + "现在" + this.getMonth() + "个月大,它正在睡觉!");
}
}
- 4.接下来在
test
包下新建一个名为Test
类用于测试。
/**
* 测试类
* @author chaixingsi
*/
public class Test {
public static void main(String[] args) {
Cat oneCat = new Cat();
oneCat.setName("花花");
oneCat.setSpecies("中华田园猫");
oneCat.eat();
oneCat.run();
System.out.println("=====================");
Dog oneDog = new Dog();
oneDog.setName("旺旺");
oneDog.setMonth(1);
oneDog.eat();
oneDog.sleep();
}
}
- 5.运行代码,结果如下所示。这边说明,子类继承父类之后,就可以访问父类的非私有成员。
- 6.总结:从上面可以看出,子类只能继承父类当中的非私有成员,但是父类无法访问子类的特有成员(作为补充,大家可以实例化一个animal对象尝试一下)。
3、方法的重写
在上面的代码中,我们实例化出来的猫和狗都调用了eat()方法(该方法是在父类当中定义的),但是比如我们想让狗有自己的独立的吃东西的方法(也就是我也会有吃东西的能力,但是我吃东西的表现形式和父类是不一样的),那应该怎么做呢?答案是方法重写。
- 提到方法重写,不由地想起方法重载,那方法重载需要满足什么条件呢?
- 要在同一个类当中。
- 方法名相同,但是参数列表不同(参数顺序、个数、类型任一一种不同,参数名不同除外)。
- 方法返回值、访问修饰符不做限制。
- 方法重写的语法规则:子类方法的返回值类型(返回值类型可以为子类,但是不能使Object(Animal的父类));方法名;参数类型、顺序、个数都要与父类继承的方法相同;此外,子类在重写父类的方法时,方法的访问修饰符是允许(有前提条件,后面访问修饰符会说)有变化的,并且与方法的参数名无关。
- 补充:上面提到子类在重写父类的方法时,方法的访问修饰符是允许有变化的,但是访问范围需要大于等于父类的访问范围。
- 子类在重写父类的方法后,子类对象调用的是重写后的方法。
- 我们可以在子类当中定义与父类重名的属性。
- 我们可以借助注解快速重写父类的方法并且可以检验其正确性。如下图所示,我们在
Dog
类中重写eat
方法。在windows中我们按Alt+/
快捷键,此时会提示我们可以重写父类的哪些方法。
- 或者我们直接写方法名编辑器会给出提示(前提,需要设置一下Eclipse以增强其代码提示功能,具体戳此文章Eclipse自动补全/自动提示的设置),然后选择要重写放方法即可。
- 在重写的方法中添加代码
System.out.println(this.getName()+"最近没有食欲~~");
,运行代码。
- 删掉重写方法中的
super.eat();
,再次运行代码,结果如下,发现父类的方法eat这次并没有运行,那么那这句代码有什么含义,讲到super的时候便会知晓,这里先说注解。
- 注解:
- 是从JDK1.5版本引入的一个特性,可以声明在包、类、属性、方法、局部变量、方法参数等的前面,用来对这些元素进行说明、注释。
- 可以把注解当作是一种标记,当在程序中加入了注解,就相当于给程序打上了标记,被打上标记的这些程序,我们的编辑器等开发工具及其他程序就可以借由这些标记来了解程序。
- 按照运行机制来分注解可分为:
- 源码注解:像
@Override
就是源码注解,就是只在源码中存在,编译成.class
文件就不存在了。 - 编译时注解:在编译成
.class
文件后注解仍然存在 - 运行时注解:在程序运行阶段还起作用,甚至会影响运行逻辑的注解。
- 源码注解:像
- 按照来源划分:
- 来自JDK的注解
- 来自第三方的注解
- 我们自定义的注解
- 此外,还有一种注解是元注解,它是用来对注解进行注释的。
4、访问修饰符
访问修饰符 | 本类 | 同包 | 子类 | 其他 |
---|---|---|---|---|
private | √ | |||
默认 | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
解释:√表示可以访问
5、super关键字
5.1、super关键字
- 通过前面所学可以知道,子类即可以继承父类的方法,也可以重写自己的方法,但是如何判断
在子类当中调用的是继承自父类的方法还是自己重写放方法呢?这就要涉及到super关键字的使用了。 - 比如我们要调用父类吃东西的方法,则通过
super.eat();
就可以了。super:父类对象的引用。 - 通过super关键字,我们还可以访问父类允许子类派生的任意成员(该成员需要用protected及public访问修饰符修饰,这样子类便可访问)。
- 父类的构造方法是不允许被继承,也不允许被重写的。
5.2、继承的初始化顺序
下面我们以Animal和Cat这两个类为例,但是需要修改补充一下代码。
- 1.我们在Animal父类当中添加两个静态成员,并添加静态代码块和构造代码块,并先对属性进行赋值(之所以个属性赋值,是为了方便看到在构造对象的时候成员的执行情况)。
public class Animal {
private String name = "哈哈";
private int month = 2;
private String species = "动物";
public int temp = 15;
private static int st1 = 22;
public static int st2 = 23;
static {
System.out.println("我是父类的静态代码块");
}
{
System.out.println("我是父类的构造代码块");
}
public Animal() {
System.out.println("我是父类的无参构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public String getSpecies() {
return species;
}
public void setSpecies(String species) {
this.species = species;
}
// 吃东西
public void eat() {
System.out.println(this.getName() + "在吃东西!");
}
}
- 2.修改Cat类代码如下。
public class Cat extends Animal {
private double weight=3;
public static int st3 = 24;
static {
System.out.println("我是子类的静态代码块");
}
{
System.out.println("我是子类的构造代码块");
}
public Cat() {
System.out.println("我是子类的无参构造方法");
}
public double getWeight() {
return weight;
}
public void setWeight(double weight) {
this.weight = weight;
}
// 猫还有自己特有方法:跑动
public void run() {
System.out.println(this.getName() + "是一只" + this.getSpecies() + ",它在快乐地奔跑!");
}
}
- 3.在test包下新建一个名为TestTwo的测试类。
public class TestTwo {
public static void main(String[] args) {
Cat one = new Cat();
System.out.println(one.temp);
}
}
- 4.运行代码结果如下。
- 5.我们再实例化cat对象那行加一个断点,然后通过调试来看一下代码执行顺序(这里我用IntelliJ IDEA来进行调试)。
关于调试的讲解请参考下面两篇博客:
- 6.从上图调试过程我们来分析一下:
- 首先在实例化对象语句打上断点,然后进入调试模式;
- 这里首先点击ForceStepInto,然后就进入了ClassLoader的类,这个类是用来实现类信息的加载,即在程序执行的时候将该程序相关的类加载到虚拟机当中;
- 然后点击StepOver跳出继续往下走,首先进入的是父类Animal,然后先执行父类的静态成员的赋值操作,然后顺序执行; 加载完静态成员后,执行静态代码块;
- 同样地,子类Cat也是先对静态成员进行赋值然后执行静态代码块;然后回到main方法,继续进行对象实例化的构造。
- 于是首先来到子类Cat的无参构造方法,但此时并不会执行构造方法,而是首先会进入父类Animal的无参构造,然后点击ForceStepInto进入Object类(顶级父类,它是Animal的父类)当中,然后继续单步执行,然后回来父类Animal当中。此时,并没有执行无参构造,而是首先完成成员属性的赋值操作。
- 在完成父类Animal的成员属性的赋值操作后,紧接着会执行父类的构造代码块,然后才会执行父类的无参构造方法;
- 然后继续调试,此时会走到子类Cat中,首先对子类Cat的成员属性进行赋值操作(若没赋值,则会沿用加载时属性默认值),然后执行子类的构造代码块,接着是子类的无参构造方法。
- 此时回到Main方法当中,最后打印输出成员属性temp的值。
总结:
类信息的加载→父类的静态成员的赋值→执行父类的静态代码块→子类的静态成员的赋值→执行子类静态代码块→来到子类无参构造(并不执行无参构造)→来到父类无参构造→来到顶级父类Object当中→回到父类Animal中对普通成员属性赋值→执行父类构造代码块→执行父类的无参构造方法→对子类普通成员属性赋值→执行子类构造代码块→执行子类无参构造方法。直到此时便完成了对象的实例化操作。
注意:上面的静态代码块和静态成员赋值的执行顺序和书写顺序有关,若先写的静态代码块则先执行静态代码块。
- 7.继续修改代码,首先在子类Cat中增加一个带参构造。
public Animal(String name, int month) {
System.out.println("我是父类的带参构造方法");
}
- 8.同样地,在Animal中也增加一个带参构造。
public Cat(String name,int month) {
System.out.println("我是子类的带参构造方法");
}
- 9.然后在TestTwo类当中用带参构造去实例化对象。
Cat one = new Cat("旺旺", 6);
- 10.运行代码,结果如下,发现用带参构造实例化对象时,调用了子类的带参构造方法,但仍然调用了父类的无参构造(调试略)。
- 11.倘若我们就要调用父类的双参构造呢?这就要用到super关键字,修改Cat的带参构造。
public Cat(String name,int month) {
super(name, month);//super代表父类的引用,必须放于子类构造方法有效代码的第一行(见上图)
System.out.println("我是子类的带参构造方法");
}
- 12.运行结果如下。
5.3、super VS this
- this:当前类对象的引用
- 访问当前类的成员方法
- 访问当前类的成员属性
- 访问当前类的构造方法
- 不能在静态方法中使用
- super:父类对象的引用
- 访问父类的成员方法
- 访问父类的成员属性
- 访问父类的构造方法
- 不能在静态方法中使用
并且,在构造方法中调用时,super和this不能同时出现。