目录
什么是继承
继承的语法
父类成员的访问
访问父类的成员变量
访问父类的成员方法
super 关键字
访问父类成员变量
访问父类成员方法
访问父类构造方法
super 和 this
初始化
访问限定符
继承方式
final 关键字
继承和组合
什么是继承
在 Java 中,使用类来对现实中的实体进行描述,例如
定义一个 Cat 类来描述猫:
public class Cat { String name; int age; public void eat() { System.out.println("吃饭"); }}
定义一个 Dog 类来描述狗:
public class Dog { String name; int age; public void eat() { System.out.println("吃饭"); }}
而上述代码中的 name、age 以及 eat 方法,都是重复的
此时,就可以对这些共性进行抽取
而在面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码复用
继承(inhertance):是面向对象编程的特征,它允许在保持原有类的基础上进行扩展,增加新的功能,这样产生的新类,称之为派生类。通过继承,能够实现共性的抽取,从而实现代码的复用
我们将上述 Cat 和 Dog 的共性进行抽取,使用继承的思想来达到复用效果:
如上图所示,Dog 和 Cat 都继承了 Animal 类,其中,Animal 称为 父类(或 超类、基类),Dog 和 Cat 称之为 Animal 的 子类(或 派生类),继承之后,子类可以复用父类中的成员,子类在实现时只需关心自己新增的成员即可
继承的语法
在 Java 中若要表示类之间的继承关系,需要使用 extends 关键字:
修饰符 class 子类 extends 父类{
...
}
public class Animal { String name; int age; public void eat() { System.out.println("吃饭"); }}
Cat 类继承 Animal,还可以新增需要的成员变量或方法
public class Cat extends Animal{ public void miaow() { System.out.println("喵喵叫"); }}
子类会将父类中的成员变量或成员方法继承到子类中
子类在继承父类之后,可以添加自己特有的成员
父类成员的访问
子类将父类中的方法和变量继承之后,那么,子类中能否直接访问父类中继承下来的成员呢?
访问父类的成员变量
当子类和父类不存在同名对象时
public class A { int a = 10;}
public class B extends A { int b = 20; public static void main(String[] args) { B b = new B(); System.out.println(b.a); System.out.println(b.b); // System.out.println(b.c); // 编译失败,子类和父类中都不存在 c b.method(); }}
运行结果:
子类自己有就访问自己的成员变量,若没有,就访问父类的成员变量
当子类和父类存在同名对象时
public class A { int a = 10; int b = 20;}
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void method() { System.out.println("a: " + a + " b: " + b); } public static void main(String[] args) { B b = new B(); b.method(); }}
运行结果:
存在同名变量时,优先访问子类的成员变量
从上述两个示例中,可以看出:
若访问的成员变量子类中有,则访问子类自己的成员变量
若访问的成员变量子类中没有,则访问父类中继承下来的,若父类中也没有,则会编译报错
若访问的成员变量,父类子类中都有,则优先访问自己的成员变量
即 自己有就优先自己的,若没有再向父类中找
访问父类的成员方法
成员方法名不同
public class A { int a = 10; int b = 20; public void methodA() { System.out.println("methodA..."); }}
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { System.out.println("methodB..."); } public static void main(String[] args) { B b = new B(); b.methodA(); b.methodB();// b.methodC(); // 编译失败,在继承体系中没有发现 methodC() }}
运行结果:
当成员方法没有同名时,在子类方法中或通过子类对象访问方法时,若自己有,访问自己的,若自己没有,则在父类中找,若父类中也没有,则报错
成员方法名相同
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodA(int a) { System.out.println("methodA..." + a); } public static void main(String[] args) { B b = new B(); b.methodA(); b.methodA(10); }}
运行结果:
当子类方法名和父类方法名相同,但其参数列表不同时,就构成了重载,根据调用方法时传递的参数选择合适的方法访问,若不存在该方法,则报错
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 @Override public void methodA() { System.out.println("B methodA..."); } public static void main(String[] args) { B b = new B(); b.methodA(); }}
当子类方法名和父类方法名相同,且其参数列表也相同时,就构成了重写,此时,会访问子类的方法
那么,当子类中存在和父类相同的成员时,如何在子类中访问父类同名成员呢?
super 关键字
当子类和父类中存在相同名称的成员时,若要在子类方法中访问父类同名成员时,是不能直接访问的。Java 提供了 super 关键字,super 的主要作用:在子类方法中访问父类成员
访问父类成员变量
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { System.out.println(super.a); System.out.println(super.b); } public static void main(String[] args) { B b = new B(); b.methodB(); }}
访问父类成员方法
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 public void methodB() { super.methodA(); System.out.println("methodB..."); } public static void main(String[] args) { B b = new B(); b.methodB(); }}
需要注意的是:只能在非静态方法中使用 super
访问父类构造方法
在子类对象构造时,会先调用父类构造方法,然后再执行子类构造方法
为 A 添加带一个参数的构造方法
public class A { int a = 10; int b = 20; public A(int a) { System.out.println("a"); } public void methodA() { System.out.println("methodA..."); }}
编译报错:A 中没有可用的默认构造函数
为什么会报错呢?
这是因为在 A 中没有定义的构造方法时,编译器提供了默认的无参构造方法
子类 B 中也没有定义构造方法,编译器也提供了默认的无参构造方法,且在 B 的构造方法中第一行默认有隐含的 super() 调用父类的无参构造方法
当添加了带有一个参数的构造方法后,编译器也就不再提供无参的构造方法,此时也就会报错
我们可以在 A 中添加无参构造方法
或是在 B 的无参构造方法中调用 A 带有一个参数的构造方法
public B(int a) { super(a); }
当父类中有多个构造方法时,就需要在子类构造方法中选择一个合适的父类构造方法调用,否则编译失败
为什么要先调用父类的构造方法呢?
子类对象中的成员是由两部分组成的:父类继承下来的成员 和 子类新增的成员
因此,在构造子类对象时,就需要先调用父类的构造方法,将从父类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增的成员初始化完整
在子类构造方法中,通过 super(...) 调用父类构造,且 super(...) 必须是子类构造函数中的第一条语句
因此,super(...) 只能在子类构造方法中出现一次,且必须在第一行
而若需要在构造方法中使用 this 关键字调用类中的其他构造方法,也必须在第一行使用,也就是说,super(...) 和 this(...) 不能同时出现
super 和 this
相同点
(1)都是 Java 中的关键字
(2)都可以在成员方法中用来访问成员变量和调用其他成员方法,都可以作为构造方法的第一条语句
(3)只能在类的非静态方法中使用,用来访问非静态成员方法和变量
(4)在构造方法中调用时,必须是构造方法的第一条语句,并且不能同时存在
不同点
(1)this 表示当前对象(实例方法的对象)的引用,而 super 表示子类对象重父类继承下来部分成员的引用
(2)在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性
(3)在构造方法中,this(...) 用于调用本类构造方法,super(...) 用于调用父类构造方法,两种调用不能同时在构造方法中出现
(4)构造方法中一定会存在 super(...) 的调用,但 this(...) 若没有写则没有
初始化
在前面的文章 Java 代码块-CSDN博客 中,我们学习了代码块,重点学习了 实例代码块 和 静态代码块,以及它们的执行顺序,接下来,我们就来看存在继承关系时,它们的执行顺序
public class A { int a; int b; { a = 10; b = 20; System.out.println("A 构造代码块执行..."); } static { System.out.println("A 静态代码块执行..."); } public A(int a) { System.out.println("A 构造方法执行..."); } public void methodA() { System.out.println("methodA..."); }}
public class B extends A { int a = 100; // 与父类中的成员同名,且类型相同 char b = 'b'; // 与父类中的成员同名,但类型不同 static { System.out.println("B 静态代码块执行..."); } { System.out.println("B 构造代码块执行..."); } public B(int a) { super(a); System.out.println("B 构造方法执行..."); } public void methodA() { System.out.println("methodB..."); } public static void main(String[] args) { B b1 = new B(10); System.out.println("---------------------"); B b2 = new B(20); }}
运行结果:
通过运行结果,可以看到:
静态代码块先执行,并且只执行一次,在类加载阶段执行
父类静态代码块优先于子类静态代码块执行,且最早执行
当有对象创建时,才会执行实例代码块
父类实例代码块和父类构造方法先执行,然后再执行子类的实例代码块和子类的构造方法
访问限定符
为了实现封装性,Java 引入了访问限定符,主要限定:类或类中成员能否在类外或其他包中被访问
范围 | private | default | protected | public |
---|---|---|---|---|
同一个包中的同一个类 | √ | √ | √ | √ |
同一个包中的不同类 | √ | √ | √ | |
不同包中的子类 | √ | √ | ||
不同包中的非子类 | √ |
public:在任何类都可以访问
protected:在同一个包中的类或不同包中的子类可以访问
default:同一个包中的类可以访问
private:只有该类内部可以访问
当 A 和 B 位于同一个包中时:
public class A { public int a; int b; protected int c; private int d;}
public class B extends A { public void method() { super.a = 10; super.b = 20; super.c = 30; super.d = 40; }}
由于变量 d 是 A 私有的,即只能在 A 类中访问,因此,子类中不能直接访问
但是,父类中的 private 成员变量虽然不能在子类中直接访问,但是也继承到子类中了
子类可以通过父类提供的方法来进行修改
public class A { public int a; int b; protected int c; private int d; public int getD() { return d; } public void setD(int d) { this.d = d; }}
public class B extends A { public void method() { super.a = 10; super.b = 20; super.c = 30; super.setD(40); }}
当 A 和 B 位于不同包中:
public class B extends A { public void method() { super.a = 10; // 父类中 public 修饰的成员在不同包子类中可以直接访问 super.b = 20; // 父类中 protected 修饰的成员在不同包子类中可以直接访问 // super.c = 30; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问 // super.d = 40; // 编译报错,父类中默认访问权限修饰的成员在不同包子类中不能直接访问 }}
继承方式
在 Java 中,支持的继承方式有:
(1)单继承
(2)多层继承
(3)不同类继承同一个类
(4)多继承(不支持)
Java 中不支持多继承
类是对现实事物的抽象,而当情况比较复杂时,涉及到的类也会比较多,类之间的关系也会比较复杂
但即使如此,我们并不希望类之间的继承层次太复杂,一般不希望出现超过三层的继承关系
此时,若想从语法上限制继承,就可以使用 final 关键字
final 关键字
final 可以用来修饰变量、成员方法以及类
当修饰变量时,表示该变量为常量(不可变)
当修饰方法时,表示该方法不能被重写
当修饰类时,表示该类不能被继承
继承和组合
与继承类似,组合也是一种表达类之间关系的方式,也能够达到代码重用的效果。
但组合并没有涉及到特殊的语法,仅仅是将一个类的实例作为另一个类的字段
继承表示的对象之间是 is a 的关系,如:猫 是 动物
组合表示的对象之间是 has a 的关系,如:电脑 有 键盘
组合:
public class Person { private Student[] students;}
此时 Student 属于 Person 的一部分, 在 Person 中,可以复用 Student 中的属性和方法
继承:
public class Student extends Person{}
Student 是 Person 的子类,继承了父类的成员,能够复用父类中的属性和方法
组合和继承都可以实现代码复用,是使用组合还是继承,需要根据具体的情况来进行选择