6.Java-类的继承
继承与多态
1.继承
- 用来表示对象间的分类关系。父类是对子类的分类,子类是父类作为基类的派生类,子类和父类是is-a的关系,例如Dog is an Animal。某一个子类也可以作为其他子类的父类。
- 每个类有且只有一个父类(单继承),没有声明父类的,其父类为Object,子类继承了父类非private的属性和方法,可以增加自己的属性和方法,以及重写@Override父类的方法实现。
- new过程中,父类先进行初始化,可通过super调用父类相应的构造方法,没有使用super的情况下,调用父类的默认构造方法。
- 子类变量和方法与父类重名的情况下,可通过super强制访问父类的变量和方法。
- 子类继承父类,自动拥有父类的属性和行为,并可扩展属性和行为,同时,可重写父类的方法以修改行为
2.多态
- Shape图形符类,有Circle圆形、Line线形两个子类,ArrowLine带箭头的线形是Line线性的子类。定义变量Shape shape,可以引用Shap类所有的子类对象,子类对象可以赋值给父类引用变量,这叫多态。
- Shape类为变量shape静态类型,Circle、Line和ArrowLine子类为变量shape的动态类型。子类重写了父类的成员方法draw(),shap变量引用子类对象时,实际执行时会调用子类重写的成员方法,这叫做动态绑定。
继承的注意事项
1.构造函数
- 父类没有默认构造方法,只有带参构造方法时,其子类的构造方法中,需要通过super调用父类的带参数构造方法,否则,Java会提示编译错误
public class Base {
private String member;
public Base(String member){ // 父类base只有一个带参数的构造方法
this.member = member;
}
}
public class Child extends Base {
public Child(String member) {
super(member); // Base类的子类需要通过super调用Base类的构造方法
}
}
- 在父类构造方法中调用可被子类重写的方法,会引起混淆,构造方法中只调用private方法
public class Base {
public Base(){
test(); // 构造方法中有非private的成员方法test()
}
public void test(){ // test方法是可以被子类重写
}
}
public class Child extends Base {
private int a = 123;
public Child(){
}
public void test(){ // 子类重写了父类的test方法,在调用的时候
System.out.println(a);
}
// 调用子类的方法预期输出123,却输出了0、123
public static void main(String[] args){
Child c = new Child(); // 原因是new的过程会先初始化父类,父类构造方法调用test方法,发现test被重写,动态绑定机制会调用子类test方法,此时子类变量a的赋值还未执行,输出了默认的0值。
c.test();
}
}
2.静态绑定
- 动态绑定是子类可以重写父类非private的方法,父类对象引用子类对象后调用重写的方法时,会动态绑定,执行子类的方法。
- 静态绑定是父类的实例变量、静态变量、静态方法、private方法,当子类有重名的变量或方法时,实际上有两个变量或方法,子类隐藏了父类的变量和方法。
- 静态类型是父类,则访问父类的变量和方法;静态类型是子类,则访问的是子类的变量和方法。Base b = new Child(),其中Base为变量b的静态类型,Child为变量的动态类型。Child b = new Child(),Child则为变量b的静态类型。
- 静态绑定在程序编译阶段即可决定,而动态绑定则要等到程序运行时。
public class Base {
public static String s = "static_base"; // 静态变量
public String m = "base"; // 成员变量
public static void staticTest(){
System.out.println("base static: "+s); // 静态方法,只能访问静态变量
}
}
// 子类重名的静态变量、成员变量,静态方法
public class Child extends Base {
public static String s = "child_base";
public String m = "child";
public static void staticTest(){
System.out.println("child static: "+s);
}
}
/** 以下代码输出
static_base
base
base static: static_base
child_base
child
child static: child_base
**/
public static void main(String[] args) {
Child c = new Child(); // 静态类型是子类
Base b = c; // 静态类型是父类
System.out.println(b.s); // 访问父类静态变量
System.out.println(b.m); // 访问父类成员变量
b.staticTest(); // 访问父类静态方法
System.out.println(c.s); // 访问子类静态变量
System.out.println(c.m); // 访问子类成员变量
c.staticTest(); // 访问子类静态方法
}
3.其他
(1) 函数匹配:当有多个重名函数的时候,在决定要调用哪个函数的过程中,首先是按照参数类型进行匹配的,换句话说,寻找在所有重载版本中最匹配的,然后才看变量的动态类型,进行动态绑定。如sum(2,3),当子类方法是sum(long, long),父类方法是sum(int, int),根据类型匹配会访问父类的方法。
(2) 向下转型: 父类赋值给子类叫做向下转型。能否转换成功,取决于这个父类变量的动态类型(即引用的对象类型)是不是这个子类或这个子类的子类。如成功向下转型:Base b = new Child(); Child c = (Child)b; 向下转型抛异常 Base b = new Base(); Child c = (Child)b,通过instanceof判断父类变量是否引用子类对象。
(3) protected:一个常用的场景是模板模式,父类定义了实现的模板,为方法执行的步骤,而用protected修饰的方法具体实现由子类提供。
public class Base {
protected int currentStep;
protected void step1(){
}
protected void step2(){
}
public void action(){ // 模板方法
this.currentStep = 1;
step1(); // 模板-步骤1
this.currentStep = 2;
step2(); // 模板-步骤2
}
public class Child extends Base {
protected void step1(){ // 模板-步骤1的具体实现
System.out.println("child step " + this.currentStep);
}
protected void step2(){ // 模板-步骤2的具体实现
System.out.println("child step " + this.currentStep);
}
}
(4) 可见性重写:子类重写方法时,可以升级父类方法的可见性但不能降低。可见范围是public > default> protected > private,从做左到右为降低。即父类是public,子类只能是public,父类是protected,子类可以是protected,也可以是public。
(5) 防止继承:使用final关键字。
- final修饰变量,变量只能赋值一次,且不可修改
- fianl修饰protected方法和public方法,方法不能被重写
- fianl修饰Java类,该类不能被继承了
3.继承基本原理
一个类完整的信息包括
- 类变量 (静态变量)
- 类初始化代码 (静态代码块)
- 类方法 (静态方法)
- 实例变量
- 实例初始化代码
- 实例方法
- 父类信息引用
类初始化的过程
- 静态变量的赋值语句
- 静态代码块
实例初始化的过程
- 实例变量时的赋值语句
- 实例初始化代码块
- 实例初始化代码块
类加载的过程
- 分配内存保存类的信息
- 给类变量赋默认值
- 加载父类
- 设置父子关系
- 执行类初始化代码 (先执行父类再执行子类,父类执行时子类变量为默认值)
以上初始化过程通过下面打印的结果来理解
/**打印结果:
---- new Child()
基类静态代码块, s: 1
子类静态代码块, s: 10
基类实例代码块, a: 1
基类构造方法, a: 2
子类实例代码块, a: 10
子类构造方法, a: 20
---- c.action()
start
child s: 20, a: 30
end
---- b.action()
start
child s: 20, a: 30
end
---- b.s: 2
---- c.s: 20
---- b.a: 3
---- c.a: 30
**/
public class Base {
public static int s = 1; // step-1
public int a = 1; // step-1
static {
System.out.println("基类静态代码块, s: "+s); // step-2 由于先执行赋值语句,所以打印是结果为1
s = 2;
}
{
System.out.println("基类实例代码块, a: "+a); // step-3
a = 2;
}
public Base(){
System.out.println("基类构造方法, a: "+a); // step-4 先执行实例的代码块,在执行构造方法
a = 3;
}
protected void step(){
System.out.println("base s: " + s +", a: "+a);
}
public void action(){
System.out.println("start");
step();
System.out.println("end");
}
}
public class Child extends Base {
public static int s = 10;
public int a = 10;
static {
System.out.println("子类静态代码块, s: "+s);
s = 20;
}
{
System.out.println("子类实例代码块, a: "+a);
a = 20;
}
public Child(){
System.out.println("子类构造方法, a: "+a);
a = 30;
}
protected void step(){
System.out.println("child s: " + s +", a: "+a);
}
}
public static void main(String[] args) {
System.out.println("---- new Child()");
Child c = new Child(); // 对象初始化前先初始化类信息(静态方法),子类初始化前先初始化父类
System.out.println("\n---- c.action()");
c.action();
Base b = c;
System.out.println("\n---- b.action()");
b.action(); // 动态绑定
System.out.println("\n---- b.s: " + b.s);
System.out.println("\n---- c.s: " + c.s);
System.out.println("\n---- b.a: " + b.a);
System.out.println("\n---- c.a: " + c.a);
}
内存分为栈和堆,栈存放函数的局部变量,而堆存放动态分配的对象,还有一个内存区,存放类的信息,这个区在Java中称为方法区
书籍:18年出版/机械工业出版社/马俊昌/《Java编程的逻辑》第二部分第三章
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)