继承——Java中的“家族传承”
继承
继承是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和多态),它允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码复用和扩展。
类和类之间的关系有很多中,继承就是其中一种关系,除此之外还有依赖、组合、聚合等。
生活中有很多例子:
继承是一种 "is-a" 的关系
- 父类(超类 / 基类):被继承的类,包含通用的属性和方法。
- 子类(派生类):继承父类的类,可拥有父类的成员,还能添加自己的特有成员或重写父类方法。
继承实现/继承的语法
使用 extends 关键字声明继承关系:
[public] class 子类名 extends 父类名 {
子类新增内容;
}
// 父类
class 父类名 {// 父类的属性和方法
}// 子类继承父类
class 子类名 extends 父类名 {// 子类自己的属性和方法
}
案例展示:
//定义Person类,作为父类
class Person {private String name;public void setName(String name) {this.name = name;}public void sayHello() {System.out.println("hello~ I am " + name);}
}
//定义Student类,作为子类继承Person
class Student extends Person {// 新增内容可以为空
}
public class Test{public static void main(String[] args) {// 实例化一个子类对象Student stu = new Student();// setName方法从父类中继承而来stu.setName("tom");// sayHello方法从父类中继承而来stu.sayHello();}
}
继承特点
1.代码复用
子类无需重复定义父类已有的属性和方法,直接继承使用。
例如:Student 类继承了 Person 类的 name属性和各种方法。
2.单继承
Java 只支持 单继承(一个子类只能有一个直接父类)
咳咳,我们举个其他的例子(;¬_¬)
//编译报错,一个类只能有且只有一个父类,不能同时继承俩个父类
public class 博士生 extends 老师,学生 {}
3.支持多层继承
子类 A 继承父类 B ,父类B 继承爷爷类 C
class A {}
class B extends A {} // 正确:B继承A
class C extends B {} // 正确:多层继承(C间接继承A)
class D extends A, B {} // 错误:不支持多继承
举个栗子🌰:
class Grandpa {public void farming() {System.out.println("爷爷喜欢种地");}
}
class Father extends Grandpa {// Father类 继承父类中 farming方法// 子类新增方法public void working() {System.out.println("爸爸以劳动为荣");}
}
class Son extends Father {// Son类 继承父类中 farming方法 和 working方法// 子类新增方法public void studying() {System.out.println("孙子爱学习");}
}
// 多层继承测试
public class Test {public static void main(String[] args) {Son s = new Son();s.farming();s.working();s.studying();}
}
4.访问权限控制
子类只能继承父类中非私有(不是private) 的成员(属性和方法)。
private 成员:子类无法直接访问(需通过父类的 public/protected 方法间接访问)。
public/protected 成员:子类可直接访问。
默认权限(无修饰符):同包内的子类可访问,不同包的子类不可访问。
class Parent {private int num1 = 1; // 私有成员,子类不可访问public int num2 = 2; // 公共成员,子类可访问protected int num3 = 3; // 受保护成员,子类可访问int num4 = 4; // 默认权限,同包子类可访问
}class Child extends Parent {void show() {// System.out.println(num1); // 错误:无法访问private成员System.out.println(num2); // 正确System.out.println(num3); // 正确System.out.println(num4); // 若同包则正确,不同包则错误}
}
Java官方文档描述:从继承的概念来说,private修饰的成员不被继承。
官网描述: https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html
我们写不同类可以分开写,这个时候要带上前面的修饰符,比如:
父类:
//提供父类
public class Animal {//成员一个私有,一个公有private String color;public int age;//提供get|set方法public String getColor() {return color;}public void setColor(String color) {this.color = color;}public int getAge() {System.out.println("in Animal getAge() ...");return age;}public void setAge(int age) {this.age = age;}
}
子类:
//定义Dog子类 继承 Animal
public class Dog extends Animal {// 父类中的private成员,子类中不可以直接操作(无直接访问权限)// Animal: private String color;// 父类中的非private成员,子类中可以直接操作// Animal: public int age;// 子类新增属性private String name;// 子类新增get|set方法public void setName(String name) {this.name = name;}public String getName() {return name;}// 子类新增方法:输出子类对象所有属性(含新增属性、从父类继承)public void disp() {// 子类方法中 不能直接操作 父类private成员// System.out.println("继承 color: " + color); error// 子类方法中 可以间接操作(借助public的get|set) 父类private成员System.out.println("继承 color: " + getColor()); // 子类方法中 可以直接操作 父类非private成员System.out.println("继承 age: " + age); System.out.print("新增:" + name);}
}
测试案例:
public class Test{public static void main(String[] args) {Animal a = new Animal();a.setColor("black");a.setAge(3);a.show();System.out.println("------------");Dog d = new Dog();//子类对象调用从父类继承的public方法d.setColor("yellow");d.setAge(2);//子类对象调用新增的方法d.setName("虎子");d.disp();}
子类对象内存构成:
注意事项:
从内存角度来分析,父类中的private成员,子类实际上也完全继承了下来
#从 “语法定义” 角度,Java 官方明确 private 成员不被纳入 “继承范围”(子类无法直接访问,也不能通过 super 直接引用);从 “内存结构” 角度,子类对象实例化时,会包含父类所有成员(包括 private)的内存空间(因父类构造方法执行时需初始化这些成员),但子类只能通过父类提供的 public/protected 方法间接操作这部分内存中的数据。
非专业描述:完全继承、有限访问,新皇登基,全部继承(含private后宫),但不能直接访问,间接方式操作
父类private成员为父类私有,在父类成员方法中可以直接访问,在其他类中(含子类),不能直接操作
子类中不能直接操作父类private成员,但是可以通过间接方式(比如借助父类public的get、set方法)操作
Java中的类,如果类定义时没有指定父类,那么这个类会默认继承Object类。
Java中的每个类都直接或间接继承Object类,Object类是Java继承体系中的最顶层父类(不是余胜军(◎_◎;)
继承优缺点
优点:
提高代码的复用性
提高了代码的维护性(父类修改后,子类全部生效)
让类与类之间产生了 is-a 的关系,是多态的前提
弊端:
继承是侵入性的
继承让类与类之间产生了关系,类的耦合性增强了
代码与代码之间存在关联都可以将其称之为"耦合"
降低了代码的灵活性,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性
super
从父类继承的非private成员,和子类新增的成员重名,如何在子类成员方法中区分两个成员⌯oᴗo⌯?
这个时候我们可以借助super关键字可以实现区分
super表示子类对象中从父类继承的那部分(可看成一个父类对象)引用
super内存结构图如下:
在子类方法中访问一个变量,会根据就近原则:
1. 先在子类局部范围中找局部变量
2. 再在子类新增的范围中查找
3. 最后从父类继承的成员范围中查找
如果一定要使用从父类继承的成员,可以通过super关键字,进行区分。
super 总结:super 关键字的用法和 this 关键字的用法相似
this:代表本类对象的引用
super:代表父类存储空间的标识(可以理解为父类对象引用)
构造方法
思考:如何给子类对象进行初始化?
子类对象的数据成员包含两部分:继承部分,新增部分
对新增部分数据成员初始化,子类构造方法中直接 this.新增数据成员 = 值; 即可
对继承部分数据成员初始化
如果:子类构造方法中直接 this.继承数据成员 = 值;
Bug:父类中数据成员如果private修饰,则子类中没有权限直接访问!
如果:子类构造方法中采用 this.setXxx(值); 对继承部分数据成员进行初始化
Bug:过于繁琐,如果从父类继承的成员很多(几十个),我们要写几十个set方法
So(︶.̮︶✽)子类对象的初始化(构造方法中),要借助父类的构造方法,对父类部分成员进行初始化
子类构造方法通过super关键字调用父类构造方法: super(实际参数列表);
子类构造方法前,会优先找到父类构造方法调用,对父类继承部分成员进行初始化
父类部分初始化完成后,再执行子类构造方法代码
如果子类构造方法中没有显式调用 super(实参列表) ,则系统默认调用 super()
若父类仅定义了有参构造(未显式定义无参构造),子类构造方法必须显式通过 super(实参) 调用父类的有参构造,否则编译器会报错(因系统无法生成默认的无参构造供子类隐式调用)
案例展示:
//定义父类
class Fu {//1个私有成员private int f;//2个构造方法public Fu() {System.out.println("in Fu() ...");}public Fu(int f) {System.out.println("in Fu(int) ...");this.f = f;}//对应的get|set方法public int getF() {return f;}public void setF(int f) {this.f = f;}
}
//定义子类
class Zi extends Fu {//新增1个数据成员private int z;//子类构造方法public Zi() {//如果不显式调用super,则默认调用父类无参构造器//super();System.out.println("in Zi() ...");}public Zi(int z) {super(10);System.out.println("in Zi(int) ...");this.z = z;} public Zi(int f, int z) {//下面这行注释的代码,放开则编译报错//System.out.println("in Zi(int,int) ...");//super调用,必须为子类构造方法的第一行有效代码super(f);System.out.println("in Zi(int,int) ...");this.z = z;}// 新增方法public void disp() {//借助super可以直接访问父类继承部分的成员System.out.println("super.f: " + super.getF());//借助this,会先去找子类新增getF(),如果找不到,再去父类继承部分查找System.out.println("this.f: " + this.getF());System.out.println("Zi.z: " + z);}
}
public class Test{public static void main(String[] args) {Zi z1 = new Zi();z1.disp();System.out.println("---------------");Zi z2 = new Zi(20);z2.disp();System.out.println("---------------");Zi z3 = new Zi(100,200);z3.disp();}
}
注意1:子类构造方法中如果显式调用super(实参列表),则该代码必须为第一行有效代码!(和this那个第一行冲突,只有一个才对哦🤓)
注意2:子类构造方法中显式调用的super(实参列表),父类中必须提供,否则编译报错!
让我帮你仔细理一下(๑•̀ㅁ•́๑)✧:
子类对象初始化的完整顺序(从父到子,从静态到非静态):
- 初始化父类的静态属性和静态代码块(按定义顺序执行);
- 初始化子类的静态属性和静态代码块(按定义顺序执行);
- 初始化父类的非静态属性和非静态代码块(按定义顺序执行);
- 执行父类的构造方法;
- 初始化子类的非静态属性和非静态代码块(按定义顺序执行);
- 执行子类的构造方法。
class Parent {// 父类静态属性static int parentStaticNum = 1;// 父类静态代码块static { System.out.println("父类静态代码块:" + parentStaticNum); }// 父类非静态属性int parentNum = 2;// 父类非静态代码块{ System.out.println("父类非静态代码块:" + parentNum); }// 父类构造方法public Parent() { System.out.println("父类构造方法执行"); }
}
class Child extends Parent {// 子类静态属性static int childStaticNum = 3;// 子类静态代码块static { System.out.println("子类静态代码块:" + childStaticNum); }// 子类非静态属性int childNum = 4;// 子类非静态代码块{ System.out.println("子类非静态代码块:" + childNum); }// 子类构造方法public Child() { System.out.println("子类构造方法执行"); }
}
public class TestInitOrder {public static void main(String[] args) {new Child(); // 输出顺序:// 父类静态代码块:1// 子类静态代码块:3// 父类非静态代码块:2// 父类构造方法执行// 子类非静态代码块:4// 子类构造方法执行}
}
方法重写
重写应用场景
父子类继承关系中,当子类需要父类的功能,而继承的方法不能完全满足子类的需求,子类里面有特殊的功能,此时可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容。
方法重写细节
前提:父子类继承关系中
子类新增方法,和从父类继承的方法,方法名完全相同
两个方法的参数列表完全相同
重写方法的访问权限修饰符可以被扩大,但是不能被缩小:public > protected > default > private
方法的返回类型可以相同,也可以不同
(方法重写时,返回值类型需满足 “协变返回类型” 规则:
若父类方法返回类型为引用类型,子类重写方法的返回类型可以是父类返回类型的子类类型;若父类方法返回类型为基本类型,子类重写方法的返回类型必须与父类完全相同。)
一般情况下,子类进行方法重写时,最好方法的声明完全一致
案例展示:
定义一个基础的员工类Employee ,再定义程序员子类Programmer ,再定义经理子类Manager ,要求两个子类中分别重写父类中doWork() 方法。
//定义父类:员工
class Employee {//工号、姓名private String id;private String name;public Employee() {}public Employee(String id, String name) {this.id = id;this.name = name;}public void doWork() {System.out.println("员工开始工作...");}
}
//定义子类:程序员
class Programmer extends Employee {//等级:初级、中级、高级、资深private String grade;public Programmer() {//默认会调用父类无参构造器//super();}public Programmer(String id,String name,String grade) {//显式调用父类构造器super(id,name);this.grade = grade;}//重写方法 // @Override是注解,用于标识该方法为重写的方法@Overridepublic void doWork() {//super.doWork();System.out.println("我是程序员,级别为" + grade + ", 开始工作:写代码");}
}
//定义子类:经理
class Manager extends Employee {//奖金private double bonus;public Manager() {super();}public Manager(String id,String name,double bonus) {super(id,name);this.bonus = bonus;}//重写方法@Overridepublic void doWork() {System.out.println("我是经理,有津贴" + bonus + ", 开始工作:安排部门员工任务");}
}
//测试类
public class Test{public static void main(String[] args) {//父类对象 调用 doWork方法Employee e = new Employee("008", "larry");e.doWork();System.out.println("------------");//程序员子类对象 调用 重写方法Programmer p = new Programmer("010", "kevin", "初级");p.doWork();System.out.println("------------");//经理子类 调用 重写方法Manager m = new Manager("006", "robin", 201.5);m.doWork();}
}
结论:
子类继承父类,在调用方法的时候,如果子类中没用重写,那么调用从父类继承的方法,如果子类重写了这个方法,那么调用到子类重写的方法。( 非常重要 )
特殊情况:
注意1:父类中的静态方法(属于类方法)不能被子类重写
static静态方法算不上重写,它是属于父类所独有的,只是书写格式上跟重写相似
注意2:父类中的私有方法(未被继承)不能被子类重写
#大家可以回顾我之前的重载,对比一下易混淆的这哥俩
final 关键字
我们在上面中小小穿插了一点final,说他不能被继承不能被重写,其实它很简单
final 变量:父类中的 final 变量(常量)子类可继承,但不能修改(final 变量一旦赋值,不可更改)。若父类 final 变量通过构造方法赋值,子类需在构造方法中通过 super() 传递该变量的初始值(因 final 变量必须在初始化阶段赋值)
class Parent {private final String name; // final 变量,必须初始化// 父类构造方法为 final 变量赋值public Parent(String name) { this.name = name; }public String getName() { return name; }
}
class Child extends Parent {public Child() {// 子类必须通过 super() 为父类 final 变量传递初始值super("子类名称");}
}
总结
继承作为面向对象编程的核心特性,其价值不仅在于 “代码复用”,更在于构建清晰的类层次结构、支撑多态特性,是大型项目模块化与可维护性的基石。
1. 继承的 “科学性”:符合现实逻辑与工程设计原则
映射现实世界的层级关系:继承的 “is-a” 关系完美映射现实中的分类逻辑(如 “动物→哺乳动物→猫”、“电子设备→手机→智能手机”),让代码结构与人类认知一致,降低理解成本。这种 “分类 - 特化” 的设计,使类的职责边界更清晰,避免功能混乱。
遵循 “开闭原则”(OCP):通过继承扩展功能时,无需修改父类代码(关闭修改),只需新增子类或重写父类方法(开放扩展)。新增时无需改动原有代码,符合工程化项目的可扩展需求。
平衡复用与定制的矛盾:继承既实现了 “通用逻辑在父类统一维护”(如所有 “用户” 的 “登录”“注册” 逻辑放在父类 “User” 中),又允许子类通过 “重写” 定制特殊逻辑(如 “管理员用户” 重写 “权限校验” 方法),避免 “完全复制代码” 的冗余,同时保留子类的灵活性。
2. 继承的 “必要性”:支撑多态与大型项目架构
多态的唯一前提:多态(父类引用指向子类对象,调用方法时表现出子类特性)的实现完全依赖继承的 “方法重写” 机制。没有继承,就无法实现 “统一接口、不同实现” 的多态效果 —— 例如,通过 “List list = new ArrayList ()” 或 “List list = new LinkedList ()”,只需切换子类对象,即可在 “数组存储” 与 “链表存储” 之间灵活切换,而调用 “add ()”“get ()” 方法的代码无需修改,这是大型项目 “解耦” 的关键。
降低大型项目的维护成本:在百万级代码的项目中,继承让 “通用逻辑的修改” 只需在父类执行一次,所有子类自动生效。例如,若 “订单系统” 中所有订单的 “计算税费” 逻辑需从 “10%” 调整为 “8%”,只需修改父类 “Order” 的 “calculateTax ()” 方法,子类 “NormalOrder”“VipOrder” 无需任何改动,极大减少维护工作量,避免 “遗漏修改” 导致的 bug。
3. 理性使用继承:规避风险,发挥最大价值
继承虽有诸多优势,但需避免 “过度继承”(如类层次超过 3 层)或 “滥用继承”(如为复用一个方法而强行建立 “is-a” 关系)。实际开发中,应结合 “组合” 灵活选择:当类间无明确 “is-a” 关系时(如 “汽车” 与 “发动机”),优先用组合;当需要多态或明确分类关系时,再用继承。只有理性平衡继承的 “耦合性” 与 “复用性”,才能充分发挥其在面向对象编程中的核心价值,构建健壮、可扩展的代码体系。
前几天身体抱恙,未能连续更新,实在抱歉,如果这期有什么错误,请各位在评论区为我指出,感谢大家。上面我们又说到了多态,我会在之后的更新为大家总结,感谢点赞,感谢收藏,感谢关注,我会加油持续更新的⁄(⁄⁄•⁄ω⁄•⁄⁄)⁄