继承
继承是面向对象语言的一大特性,Java作为面向对象语言的代表,使用类实例化的对象对生活中的事物进行描述,而现实生活中的事物往往是错综复杂的,不同的对象之间可能会有紧密的联系,很多的相同点。我们用在类描述这些具有相同属性对象时,往往会定义重复的成员便变量或成员方法,造成代码冗余。
例如,我们定义猫类和狗类:
package demo1;
public class Cat {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + " 正在跑");
}
public void mew() {
System.out.println(this.name + "正在喵喵叫");
}
}
package demo1;
public class Dog {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + " 正在跑");
}
public void bark() {
System.out.println(this.name + " 正在汪汪叫");
}
}
名字、年龄、颜色成员变量重复了,run
成员方法重复了,针对这种情况,我们需要继承。
面对对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用
继承的概念
继承机制:是面对对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行拓展,增加新功能,这样新产生的类,称为派生类。
继承主要解决的问题是:共性的抽取,实现代码复用。
紧接上面的猫狗例子,我们通俗地介绍一下继承的使用场景:
名字、颜色等描述猫和狗的属性,可以用来描述任何动物,那么我们创建一个动物类,将猫和狗的共性进行抽取,此时猫和狗的特有功能就是上面所讲的拓展,这两个类称为动物类的派生类。
图中给出了父类、子类的概念:
父类:被继承的类
子类:父类的拓展,继承的类
我们什么时候用到继承关系呢?
出现A is B
的情况,我们可以考虑用继承。以上例,猫和狗都是动物,猫和狗类就可以继承动物类。
继承的语法
想在Java代码中体现继承关系,需要用到extends
关键字:
修饰符 class 子类 extends 父类 {
//...
}
我们将猫狗例子更改成继承体系:
//Animal.java
package demo1;
public class Animal {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + " 正在跑");
}
}
//Dog.java
package demo1;
public class Dog extends Animal {
public void bark() {
System.out.println(this.name + " 正在汪汪叫");
}
}
//Cat.java
package demo1;
public class Cat extends Animal {
public void mew() {
System.out.println(this.name + "正在喵喵叫");
}
}
如上,我们建立了继承体系。
【注意】
- 父类中的成员变量和成员方法会继承到子类当中
- 子类继承父类后,要添加自己特有的成员,体现出与父类的不同,否则就没必要继承了
继承体系下访问成员变量
子类继承了父类的成员,那么子类能直接访问父类的成员变量吗?
在上面的继承体系下,有代码:
package demo1;
public class Dog extends Animal {
public void test() {
name = " ";
this.name = "";
age++;
this.age++;
}
public void bark() {
System.out.println(this.name + " 正在汪汪叫");
}
}
子类中能直接访问父类继承下来的成员变量(访问限定修饰符合理)
【子类和父类不存在同名变量】
这种情况下,根据访问的变量,访问父类或子类的成员变量。
【子类和父类变量同名】
这种情况下,访问同名变量,访问的是父类的还是子类的?
我们验证,打印结果是 1 还是 10:
public class Animal {
public String name;
public int age;//Animal中的age默认初始化为0
public String color;
public void run() {
System.out.println(this.name + " 正在跑");
}
}
public class Dog extends Animal {
//Dog中的age就地初始化为10
int age = 10;
public void bark() {
System.out.println(this.name + " 正在汪汪叫");
}
public static void main(String[] args) {
Dog dog = new Dog();
System.out.println(dog.age);
}
}
打印 10 ,证明优先访问的是子类自己的成员变量。
在子类方法中或者通过子类对象访问成员时:(就近原则)
- 如果子类存在要访问的成员变量,则优先访问自己的
- 如果子类中不存在要访问的成员变量,则访问父类继承下来的,如果父类中也没有定义,则会编译报错
- 如果子类和父类中都存在要访问的成员变量(同名),则优先访问自己的
自己有优先自己,自己没有从父类找
继承体系下访问成员方法
与访问成员变量类似:
- 通过子类对象访问父类与子类中不同名方法时,优先在子类中寻找,找到就访问,否则在父类中寻找,找到就访问,否则,编译出错。
- 通过子类对象访问父类与子类同名方法时,如果父类和子类的同名方法构成重载,根据调用方法时传递的参数选择合适的方法访问,如果没有则报错。如果不构成重载,语法上子类必须重写同名方法,此时优先调用的是子类重写过的同名方法。(关于重写,会在多态的一篇中详解)
我们验证一下同名方法构成重载的情况:
//Animal.java
public class Animal {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + " 正在跑");
}
}
//Dog.java
public class Dog extends Animal {
public void run(int a) {
System.out.println("子类的run");
}
public void bark() {
System.out.println(this.name + " 正在汪汪叫");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.run(1);
}
}
我们调用run
传入的参数与子类中的参数列表对应,所以调用子类的成员方法。
super关键字
学习时,可以与this
进行对比理解,后面会讲,我们先接着看:
super
关键字,相当于子类对象从父类继承下来的部分成员的引用,其主要作用:在子类方法中访问父类成员
super
的用法:
super
访问父类的成员变量super
访问父类的成员方法super()
访问父类的构造方法
【super
访问父类的成员变量】
//Animal.java
public class Animal {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + "正在跑");
}
public void test() {
System.out.println("这是父类的成员方法");
}
}
//Dog.java
public class Dog extends Animal {
public int age;
public void bark() {
System.out.println(this.name + "正在汪汪叫");
}
public void func() {
age = 5;//等价于this.age
this.age = 20;//覆盖掉之前的赋值
super.age = 10;//super调用父类的age进行赋值,否则默认访问子类的age
}
public void printInfo() {
System.out.println("子类成员变量age的值是:" + this.age);
System.out.println("父类成员变量age的值是:" + super.age);
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.func();
dog.printInfo();
}
}
【super
访问父类的成员方法】
一般来说,父类和子类成员方法有以下情况:
- 方法名不同。通过方法名访问指定的方法即可,先找子类再找父类
- 方法名相同,构成重载。通过调用时传入的参数即可访问指定参数列表的方法
- 方法名相同,子类方法对父类方法重写。这时候需要
super
关键字调用父类的同名方法(重写在多态时会介绍,我们先给出代码,观察现象即可)
//Animal.java
public class Animal {
public String name;
public int age;
public String color;
public void run() {
System.out.println(this.name + "正在跑");
}
public void test() {
System.out.println("这是父类的成员方法");
}
}
//Cat.java
public class Cat extends Animal{
public void mew() {
System.out.println(this.name + "正在喵喵叫");
}
//重写test方法
public void test() {
System.out.println("重写父类的test方法");
}
public void use() {
//默认调用
System.out.println("这是Java默认调用的test方法:");
test();
System.out.println("利用super调用父类的被重写方法:");
super.test();
}
public static void main(String[] args) {
Cat cat = new Cat();
cat.use();
}
}
【super()
访问父类的构造方法】
这一用法的应用场景在下一模块子类构造方法中介绍:
class A {
public int a;
public A(int a) {
this.a = a;
}
}
class B extends A {
public int b;
//super调用父类的构造方法对父类成员变量初始化
public B(int a) {
super(a);
}
}
【注意】
super
只能在非静态方法中使用(构造代码块也可以),不过,super
可以通过.
调用静态方法
【this
与super
】
相同点:
- 都是Java的关键字,用法类似,都有三种
- 都不能在静态方法中使用,但是可以访问静态方法
- 必须是构造方法的第一条语句(意味着
this()
与super()
不能共存)
不同点:
-
this
代表当前对象的引用,super
相当于子类对象中从父类继承部分的成员的引用。它们指代的内容可以用一张图简单表示: -
构造方法中一定存在
super()
语句,用户没有写编译器默认添加,但是this()
不写则没有 -
构造方法中,
this()
调用当前类的构造方法,super()
调用父类的构造方法 -
在非静态成员方法中,
this
用来访问本类的成员,super
用来访问父类的成员
子类构造方法
Java中,子类对象构造时,需要先调用基类构造方法对基类的成员进行初始化,然后执行子类的构造方法
有父才有子,所以在构造子类对象的时候,要先调用父类的构造方法,将从基类继承下来的成员构造完整,再调用子类自己的构造方法,将子类自己新增的成员初始化完整。这里就要用到super
的第三种用法:super()访问父类的构造方法
我们验证 子类先调用父类构造方法的强制性:
public class Demo {
public int a;
public String s;
//父类的构造方法
public Demo(int a, String s) {
this.a = a;
this.s = s;
}
}
class Child extends Demo {
public int b;
//没有调用父类的构造方法初始化父类的成员变量
}
public class Demo {
public int a;
public String s;
//父类的构造方法
public Demo(int a, String s) {
this.a = a;
this.s = s;
}
}
class Child extends Demo {
public int b;
//子类构造方法中先使用super()调用父类的构造方法对父类的成员变量进行初始化
public Child(int a, String s) {
super(a, s);
}
public static void main(String[] args) {
Child child = new Child(10, "hello");
}
}
完成上面的修改后,程序不再报错。
【注意】
- 如果父类中显性提供了无参的构造方法或者没有显性提供任何构造方法(Java默认提供无参构造方法),子类的构造方法中默认有
super();
语句。 - 如果父类中显性提供了有参的构造方法,那么子类必须手动补充
super()
语句,调用父类的构造方法对父类的成员变量进行初始化。 super()
语句必须是构造方法的第一条语句,所以super()
与this()
不共存。
继承体系下的代码块
此模块主要讨论继承体系下的静态代码块、构造代码块、构造方法的执行顺序。
我们直接看一段代码:
//Animal.java
public class Animal {
public String name;
public int age;
static {
System.out.println("父类Animal的静态代码块执行了......");
}
{
System.out.println("父类Animal的构造代码块执行了......");
}
public Animal(String name, int age) {
this.name = name;
this.age = age;
System.out.println("父类Animal的构造方法被执行了......");
}
}
//Dog.java
public class Dog extends Animal {
static {
System.out.println("子类Dog的静态代码块被执行了......");
}
{
System.out.println("子类Dog的构造代码块被执行了......");
}
public Dog(String name, int age) {
super(name, age);
System.out.println("子类Dog的构造方法被执行了......");
}
}
//Cat.java
public class Cat extends Animal {
static {
System.out.println("子类Cat的静态代码块被执行了......");
}
{
System.out.println("子类Cat的构造代码块被执行了......");
}
public Cat(String name, int age) {
super(name, age);
System.out.println("子类Cat的构造方法被执行了......");
}
}
//Test.java
//这是测试类
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("mimi", 2);
}
}
当我们执行上面的代码,打印结果是多少?
根据打印结果,我们可以得到:
继承体系下的执行顺序:
父类的静态代码块 > 子类的静态代码块 > 父类的构造代码块 > 父类的构造方法 > 子类的构造代码块 > 子类的构造方法
了解了上面的知识后,我们修改一下Test
测试类,判断修改后的打印结果:
public class Test {
public static void main(String[] args) {
Cat cat1 = new Cat("mimi", 2);
System.out.println("=========");//分割线
Cat cat2 = new Cat("lala", 3);
}
}
-
在类和对象篇中已经介绍了,静态代码块在类被加载时执行且只执行一次,所以静态代码块只执行一次
-
每次实例化对象时,构造代码块和构造方法都会执行一次
protected关键字
protected
关键字属于类和对象篇的遗留问题了,学习了继承后,我们对子类有了认识,现在我们详细介绍一下:
被protected
修饰的成员,同包底下任何一个类都是可以访问的,不同的包下只有子类才能访问
注意: 被protected
关键字修饰的成员可以在不同包的子类中访问。
对于注意事项,我们验证:
package Demo1;//Demo1中
public class Base {
protected int a;//protected修饰
}
package Demo2;//Demo2中
//验证不同包下的子类的可访问性
import Demo1.Base;
public class Test1 extends Base {
public int b;
public void printInfo() {
System.out.println(a);
}
public static void main(String[] args) {
Test1 test1 = new Test1();
test1.printInfo();
}
}
package Demo2;//Demo2中
//验证不同包的非子类的不可访问性
public class Test2 {
public int c;
public void printInfo() {
System.out.println(a);
}
public static void main(String[] args) {
Test2 test2 = new Test2();
test2.printInfo();
}
}
【补充】
被private
修饰的父类成员不能在子类中直接访问,但实际上它们被继承下来了。
class A {
private int a;
public int b;
}
class B extends A {
public void test() {
super.a = 10;//编译报错,被private修饰的父类成员不能在子类中直接访问
super.b = 10;
}
}
继承方式
继承方式有很多种,但Java中只支持以下几种继承方式:
-
Java不支持多继承,即一个子类只能有一个父类
-
我们的代码尽量不要出现超过三层的继承关系,如果出现过多层的继承,就要考虑对代码重构了
-
Java中如果想实现多继承,需要用到接口的知识,后面会讲
final关键字
final
关键字可以用来修饰变量、成员方法或类。
-
修饰变量或字段,表示常量(即不能修改)
public static void main(String[] args) { final int a = 10; a = 20; }
-
修饰类:表示此类不能被继承
被
final
修饰的类叫做密封类final class A { } class B extends A { }
-
修饰方法:表示该方法不能被重写
被
final
修饰的方法叫做密封方法class A { public final void test() { System.out.println("被final修饰,不能重写"); } } class B extends A { public void test() { System.out.println("失败"); } }
继承与组合
组合也是一种表示类之间关系的方式,能够实现代码复用。
实现组合,只需要将一个类的实例作为另一个类的成员变量。
前面提到:继承是A is B
的关系,而组合是A has B
的关系,如,学校有老师、学生。
我们结合一段代码了解:
//Student.java
public class Student {
public String name;
public int age;
//......
}
//School.java
public class School {
//组合
public Student[] students = new Student[100];
}
在实际应用中,组合的使用多余继承,我们在写代码时还是能用组合不用继承。
上面就是继承的所有内容了。
预告:多态