一、Java 继承 (Inheritance) 秒懂指南:不做重复造轮子的“傻瓜”
在 Java 面向对象编程中,有三大核心特性:封装、继承、多态。今天我们来聊聊最容易理解,也是代码复用利器——继承(Inheritance)。
1. 什么是继承?为什么要用它?
在现实生活中,儿子可以继承父亲的财产。在 Java 的世界里,子类可以继承父类的属性和方法。
为什么要用继承?
想象一下,你要写一个系统,里面有“猫”和“狗”两种动物。
- 猫:有名字、年龄,会吃东西,会“喵喵叫”。
- 狗:有名字、年龄,会吃东西,会“汪汪叫”。
如果不使用继承,你要在猫和狗的类里把“名字、年龄、吃东西”的代码写两遍。如果有 100 种动物,你就要写 100 遍!这就是在重复造轮子。
继承就是为了解决这个问题:把大家共有的特征,抽离出来放到一个“父类”中。子类只需要继承父类,就能直接拥有这些共有的东西,自己再补充独有的功能即可。
2. 代码实战:extends 关键字
在 Java 中,我们使用 extends(扩展/继承)关键字来实现继承。
// 1. 提取共有特征,创建一个父类 (Parent/Superclass)
public class Animal {
String name;
int age;
public void eat() {
System.out.println(name + " 正在大口吃东西...");
}
}
// 2. 创建子类,使用 extends 继承父类
public class Dog extends Animal {
// 补充子类独有的方法
public void bark() {
System.out.println(name + " 在汪汪叫!");
}
}
// 3. 测试一下
public class Test {
public static void main(String[] args) {
Dog myDog = new Dog();
// name 属性明明在 Dog 类里没写,但因为继承了 Animal,可以直接用!
myDog.name = "大黄";
myDog.eat(); // 输出: 大黄 正在大口吃东西... (继承来的方法)
myDog.bark(); // 输出: 大黄 在汪汪叫! (自己的独有方法)
}
}
3. 继承的三大“铁律”
在使用继承时,新手一定要记住以下几条核心规则(面试必考):
-
单继承原则:一个子类只能有一个直接的父类。就像现实中一个人只能有一个亲生父亲。
class Dog extends Animal, Pet这种想同时认两个爹的写法,在 Java 里是绝对会报错的! -
私有财产神圣不可侵犯:父类中被
private修饰的属性和方法,子类虽然名义上也继承过来了,但不能直接访问(必须通过父类提供的 public getter/setter 方法去间接获取)。 -
构造器不能被继承:父类的构造方法(Constructor)子类是继承不了的。但是,子类在被
new出来创建对象时,默认会先悄悄调用父类的无参构造方法(底层通过隐式的super()实现),正所谓“先有父,后有子”。
总结
- 核心作用:代码复用,减少冗余,建立类与类之间的层级关系。
-
关键字:
extends(子类 extends 父类)。 - 口诀:子类拥有父类非 private 的所有属性和方法,并且可以尽情地扩展自己独有的属性和方法。
二、Java 权限修饰符秒懂:你的数据到底谁能看?
在 Java 的代码里,我们每天都在跟 public、private 这些词打交道。它们叫作权限修饰符(Access Modifiers)。
想象一下你的生活:有些秘密只能自己知道(日记本),有些东西可以和家人共享(电视机),有些家产只有亲儿子能继承(祖传秘方),而有些则是完全对外开放的(公园)。
Java 的权限修饰符就是用来控制类、属性、方法到底能被谁访问的。今天我们就用一张表、几个比喻,把这 4 种修饰符彻底搞懂。
1. 四大修饰符详解(从小到大)
① private(私有:只有我自己能看)
- 可见范围:只能在当前类内部访问。
- 生活比喻:你的带锁日记本。除了你自己,哪怕是亲儿子(子类)或者同住一个屋檐下的人(同包类)都绝对看不了。
- 最常用途:修饰实体类的成员变量,保护数据不被外部随意篡改(这就是“封装”的核心)。
② default(缺省/默认:家里人随便用)
- 可见范围:只能在同一个包(Package)内访问。注意,如果你什么修饰符都不写,默认就是它!
- 生活比喻:你家客厅的电视机。只要是和你住在同一个屋檐下(同一个包里)的类,都能随便看。但隔壁老王(其他包的类)是绝对看不了的。
③ protected(受保护:家里人和外地亲儿子能用)
- 可见范围:同一个包内 + 不同包的子类。
- 生活比喻:家里的祖传秘方。同村的人(同包类)可以看,就算你的亲儿子分家搬到了外地(不同包的子类),他也有权继承和使用。但是外地的陌生人(不同包且非子类)是拿不到的。
④ public(公开:全天下随便看)
- 可见范围:整个项目的所有地方,任何包、任何类。
- 生活比喻:城市里的街心公园。无论你是谁,住在哪里,只要你顺着地址找过来,就能随便进出。
-
最常用途:修饰供外部调用的核心方法、类的入口(如
main方法)或对外暴露的Getter/Setter方法。
2. 一张表总结(面试必考,建议收藏)
| 修饰符 | 当前类内部 | 同一个包 (同村) | 不同包的子类 (外地儿子) | 其他任何地方 (外地陌生人) |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
| protected | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
(注:✅ 代表可以访问,❌ 代表禁止访问)
3. 开发中的“潜规则”(最佳实践)
-
属性尽量私有(
private):除非有极其特殊的理由,否则所有类的成员变量都应该用private锁起来。如果别人想看或者想改,必须通过你提供的public级别get/set方法。这就像银行柜员机,钱(数据)藏在铁皮箱里,只能通过按键(方法)来操作。 -
方法按需开放:专门写给别人调用的核心业务逻辑定为
public;如果是仅在类内部自己使用的“辅助计算小方法”,定为private,不要暴露出去污染别人的视野。
三、Java 方法重写 (Override) 秒懂:青出于蓝的 4 条铁律
在 Java 的继承关系中,子类不仅可以继承父类的财产,还可以对父类原有的方法进行“改造升级”,这就叫做方法重写(Override)。
但是,重写不是你想怎么改就怎么改的,Java 编译器对此有严格的“家规”。今天我们就来盘点一下重写时必须牢记的规则,以及极易踩坑的注意事项。
1. 核心铁律:“两同、两小、一大”
为了方便记忆,你可以把重写最核心的规则总结为这句经典的口诀:两同、两小、一大。
① 两同:方法名和参数列表必须一模一样
这是重写的最基本要求。如果你把方法名拼错了,或者参数的类型、数量改了,那就不叫重写了,而是变成了“重载(Overload)”或者干脆是一个全新的方法。
② 两小:返回值类型和抛出的异常,只能更小(或相等)
-
返回值类型:如果父类方法返回的是
Animal,子类重写时可以返回Animal,也可以返回更具体的Dog(子类),但绝对不能返回一个更宽泛的Object。 - 抛出异常:子类重写的方法,不能抛出比父类更多、更宽泛的编译时异常。比如老爸的方法可能抛出“轻微感冒”,儿子重写的方法绝不能抛出“绝症”。
③ 一大:访问权限只能更大(或相等)
- 子类重写方法的访问权限,绝对不能比父类更严格。
-
生活比喻:老爸把一个绝活(
protected)公开给了全村,儿子继承后可以继续在村里公开(protected),或者直接开个网店向全网公开(public),但绝不能偷偷藏起来只给自己看(private)。
2. 哪些方法是“重写禁区”?
并不是所有的父类方法都能被子类改造,以下 3 种情况坚决不能重写:
-
private私有方法:父类的私有方法对子类本来就不可见,连看都看不到,谈何重写? -
static静态方法:静态方法属于“类”,不参与多态。如果子类写了一个一模一样的静态方法,那叫“隐藏”,不叫重写。 -
final方法:被final修饰的方法意味着它是“最终版本”,也就是明确下达了禁令:“任何人都不准再改了”。
3. 最佳实践:永远加上 @Override 注解
在实际开发中,只要你是想重写父类方法,请务必在方法上面加上 @Override 注解!
public class Dog extends Animal {
@Override // 强烈建议加上这个注解
public void eat() {
System.out.println("小狗正在快乐地啃骨头");
}
}
为什么要加?
这不仅是为了告诉阅读代码的同事“这是一个重写的方法”,更重要的是让编译器帮你把关。
如果你不小心拼错了方法名(比如手抖写成了 eatting()),如果有 @Override,编译器立马就会标红报错提醒你;如果不加,编译器会以为你单纯想写个新方法,从而引发极其隐蔽的 Bug。
四、Java 子类构造器秒懂:先有老爸,后有儿子
在 Java 继承体系中,当我们 new 一个子类对象时,底层其实发生了一件非常重要的事情:它会先去调用父类的构造器,然后再执行子类自己的构造器。
这就是 Java 宇宙中最朴素的真理:“先有父,后有子”。今天我们来彻底搞懂子类构造器背后的运行机制和常踩的坑。
1. 隐形的 super()
很多时候我们在写子类时,并没有觉得父类的构造器被调用了。这是因为 Java 编译器在背后默默帮你做了一件事。
如果你在子类的构造器里什么都没写,编译器会自动在第一行插入一个隐形的 super();,专门用来调用父类的无参构造器。
class Father {
public Father() {
System.out.println("1. 老爸诞生了");
}
}
class Son extends Father {
public Son() {
// 编译器其实在这里悄悄加了一行:super();
System.out.println("2. 儿子诞生了");
}
}
// 当我们在 main 方法里 new Son() 时,输出结果会是:
// 1. 老爸诞生了
// 2. 儿子诞生了
2. 手动调用 super(参数)
如果父类的属性需要赋值,我们就可以在子类构造器中,手动使用 super(参数) 来调用父类的有参构造器。
⚠️ 铁律:super() 必须放在子类构造器的第一行! (老爸都没安顿好,儿子不能先干活)。
class Animal {
String name;
// 父类的有参构造
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
int age;
public Dog(String name, int age) {
// 第一行:先把 name 传给父类,让父类完成初始化
super(name);
// 第二行:再初始化子类自己独有的属性
this.age = age;
}
}
3. 新手连环坑:父类没有无参构造器怎么办?
这是初学者写继承时最容易报错的地方!
还记得构造器的规则吗:“如果写了有参构造,系统默认赠送的无参构造就会消失”。
如果父类只有有参构造器,那么子类中那个隐形默认的 super(); 就找不到东西调用了,直接编译报错!
破解之法有两种:
- (强烈推荐) 老老实实给父类补上一个无参构造器。这能省去子类 99% 的麻烦。
- 在子类的所有构造器中,第一行手动写明
super(具体参数),明确告诉编译器去调用父类的哪个有参构造器。
总结
- 执行顺序:永远是先执行父类构造器,再执行子类构造器。
-
关键字:
super用于调用父类构造器,且必须写在子类构造器的第一行。 - 避坑口诀:写父类时,尽量养成随手写一个无参构造器的习惯。
五、 Java 多态 (Polymorphism) 秒懂:同一个指令,不同的表现
在了解了封装和继承之后,我们终于迎来了 Java 面向对象的三大特性之首——多态(Polymorphism)。
很多初学者觉得多态很抽象。其实用一句话就能解释:“同一个行为,不同的对象去执行,会有完全不同的结果。”
举个生活中的例子:同样是“按下 F 键”这个指令,如果在浏览器里,可能是全屏;如果在 Word 里,可能是打出一个字母 f;如果在某款游戏里,可能是“Pay Respects”。指令是一样的,但因为对象不同,表现就不同,这就是多态!
1. 触发多态的 3 大前提条件
在 Java 中,想要实现多态,必须同时满足以下三个条件,缺一不可:
-
必须有继承(或实现)关系:这是多态的基础(比如
Dog继承Animal)。 - 必须有方法重写(Override):子类必须对父类的方法进行改造。
-
父类引用指向子类对象:这是多态最核心的语法形式,即
父类类型 变量名 = new 子类对象();。
2. 代码实战:感受多态的魅力
我们用经典的“动物叫声”来演示:
// 1. 父类
class Animal {
public void makeSound() {
System.out.println("动物发出叫声...");
}
}
// 2. 子类狗 (继承 + 重写)
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}
// 2. 子类猫 (继承 + 重写)
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵喵~");
}
}
public class Test {
public static void main(String[] args) {
// 3. 父类引用指向子类对象 (向上转型)
Animal a1 = new Dog();
Animal a2 = new Cat();
// 发出同样的指令,表现完全不同!
a1.makeSound(); // 输出: 汪汪汪!
a2.makeSound(); // 输出: 喵喵喵~
}
}
3. 黄金法则:“编译看左边,运行看右边”
当你写出 Animal a1 = new Dog(); 时,这个 a1 到底算 Animal 还是算 Dog?
记住多态中最著名的一句口诀:对于成员方法来说,“编译看左边,运行看右边”。
-
编译看左边:Java 编译器在检查代码时,只看等号左边的类型(
Animal)。如果Animal类里没有makeSound()这个方法,代码直接报错,根本不给你运行的机会。 -
运行看右边:一旦编译通过,真正跑到内存里执行的时候,Java 会去看等号右边到底是个什么实体对象(
Dog)。发现是条狗,就坚决执行狗重写过的方法。
4. 我们为什么非要用多态?
很多新手会问:“我直接 Dog d = new Dog(); 不香吗?非要搞个 Animal a = new Dog(); 假装自己是普通动物干嘛?”
多态真正的威力在于扩展性和解耦。
想象一下你要写一个宠物医院的看病系统,里面有一个医生类给动物治病:
-
没有多态:你需要写
treat(Dog d)、treat(Cat c)、treat(Pig p)... 以后每加一种动物,你都要修改医生类的代码。 -
有了多态:你只需要写一个方法:
不管以后系统增加了多少种新动物,医生类一行代码都不用改!这就是设计模式中常常追求的“开闭原则”(对扩展开放,对修改关闭)。public void treat(Animal animal) { System.out.println("开始检查..."); animal.makeSound(); // 传进来什么动物,就会发出什么声音 }
总结
多态的本质就是屏蔽子类之间的差异,统一把它们当成父类来处理,从而写出极其灵活、可扩展的代码。记住三大前提和“编译看左,运行看右”,多态对你来说就再也没有秘密了。