一、继承基础概念
继承的定义
继承是面向对象编程(OOP)中的核心概念之一,它允许一个类(称为子类或派生类)基于另一个类(称为父类或基类)来构建。子类会自动继承父类的属性和方法,并可以在此基础上进行扩展或修改。继承的主要目的是实现代码复用和层次化分类。
关键术语
- 父类(Superclass):被继承的类,也称为基类。
- 子类(Subclass):继承父类的类,也称为派生类。
- 扩展(Extends):Java 中通过
extends
关键字实现继承。
继承的作用
-
代码复用
子类可以直接使用父类中已定义的属性和方法,无需重复编写相同代码。例如:class Animal { void eat() { System.out.println("Animal is eating"); } } class Dog extends Animal { // Dog 继承 Animal // 无需重新定义 eat(),直接复用父类方法 }
-
层次化分类
通过继承可以模拟现实世界的分类关系。例如:
Animal
→Mammal
→Dog
,每一层添加更具体的特性。 -
多态的基础
继承是实现多态的前提条件(需结合方法重写和父类引用指向子类对象)。 -
功能扩展
子类可以在继承的基础上添加新方法或重写父类方法。例如:class Dog extends Animal { void bark() { // 扩展新方法 System.out.println("Dog is barking"); } @Override void eat() { // 重写父类方法 System.out.println("Dog is eating bones"); } }
继承的语法
在 Java 中,通过 extends
关键字实现单继承(一个子类只能继承一个父类):
class Subclass extends Superclass {
// 子类特有的属性和方法
}
注意事项
-
单继承限制
Java 不支持多继承(一个子类不能直接继承多个父类),但可以通过接口(interface
)实现类似功能。 -
访问权限控制
子类只能继承父类中public
、protected
和默认(包私有)的成员,无法继承private
成员。 -
构造方法不继承
子类不会继承父类的构造方法,但必须通过super()
调用父类构造方法(隐式或显式)。 -
慎用继承
继承应满足 “is-a” 关系(如Dog
是一种Animal
),避免滥用导致代码耦合度过高。
父类与子类的关系
概念定义
在Java中,父类(Parent Class/Superclass) 和 子类(Child Class/Subclass) 是通过继承(extends
)建立的一种层级关系。
- 父类:是被继承的类,包含通用的属性和方法。
- 子类:继承父类的类,可以复用父类的功能,并扩展或修改父类的行为。
核心特点
- 代码复用:子类自动继承父类的非私有成员(
public
/protected
/默认修饰的属性和方法)。 - 扩展性:子类可以新增属性和方法,或通过重写(
@Override
)修改父类方法的行为。 - 层级结构:一个父类可以有多个子类,但一个子类只能直接继承一个父类(单继承)。
使用场景
-
通用功能抽象
例如:Animal
是父类,包含eat()
方法;Dog
和Cat
是子类,可以复用eat()
并新增bark()
或meow()
。class Animal { void eat() { System.out.println("Eating..."); } } class Dog extends Animal { void bark() { System.out.println("Barking!"); } }
-
多态实现
父类引用可以指向子类对象,实现运行时动态绑定:Animal myPet = new Dog(); // 多态 myPet.eat(); // 调用继承的父类方法
-
模板方法模式
父类定义算法骨架,子类实现具体步骤:abstract class Game { abstract void initialize(); void play() { initialize(); } } class Chess extends Game { void initialize() { System.out.println("Setup chessboard"); } }
注意事项
-
继承链
- 子类继承父类的所有非私有成员,包括父类的父类(多层继承)。
- 构造方法不继承,但子类必须调用父类构造器(隐式或显式通过
super()
)。
-
访问权限
- 父类的
private
成员对子类不可见,需通过protected
/public
方法间接访问。 - 子类重写方法时,访问修饰符不能比父类更严格(如父类为
protected
,子类不能为private
)。
- 父类的
-
慎用继承
- 避免过度继承导致层级过深(“菱形问题”)。
- 优先使用组合(
has-a
)而非继承(is-a
)来降低耦合。
示例代码
class Vehicle {
protected String brand = "Toyota";
public void honk() { System.out.println("Beep!"); }
}
class Car extends Vehicle {
private String model = "Camry";
public static void main(String[] args) {
Car myCar = new Car();
myCar.honk(); // 继承父类方法
System.out.println(myCar.brand + " " + myCar.model); // 访问父类属性+子类属性
}
}
常见误区
- 混淆继承与接口
继承是“是什么”关系(Car
是Vehicle
),接口是“能做什么”关系(Drivable
)。 - 忽略构造器链
若父类无默认构造器,子类必须显式调用super(args)
。 - 滥用重写
重写时应遵循里氏替换原则(子类不能破坏父类行为)。
extends 关键字
概念定义
extends
是 Java 中用于实现继承的关键字。它允许一个类(子类)继承另一个类(父类)的属性和方法。通过继承,子类可以复用父类的代码,并可以扩展或修改父类的行为。
基本语法
class 子类名 extends 父类名 {
// 子类的成员变量和方法
}
使用场景
- 代码复用:当多个类具有相同的属性和方法时,可以将这些共性提取到父类中,子类通过继承来复用这些代码。
- 扩展功能:子类可以在继承父类的基础上,添加新的属性和方法,或者重写父类的方法以实现不同的行为。
- 实现多态:通过继承,可以实现运行时多态,即父类引用指向子类对象。
示例代码
// 父类
class Animal {
String name;
void eat() {
System.out.println(name + " is eating.");
}
}
// 子类继承父类
class Dog extends Animal {
void bark() {
System.out.println(name + " is barking.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "Buddy";
dog.eat(); // 继承自父类的方法
dog.bark(); // 子类新增的方法
}
}
注意事项
- 单继承限制:Java 不支持多重继承,一个类只能直接继承一个父类。但可以通过接口实现多重继承的效果。
- 访问权限:子类不能继承父类的私有成员(
private
修饰的属性和方法),但可以通过父类提供的公共方法间接访问。 - 构造方法:子类的构造方法会隐式或显式调用父类的构造方法。如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法。
- 方法重写:子类可以重写父类的方法,但重写的方法不能比父类方法有更严格的访问权限(例如,父类方法是
public
,子类方法不能是private
)。
常见误区
- 误以为继承就是复制代码:继承是建立了一种关系,子类可以访问父类的成员,但并不是将父类的代码复制到子类中。
- 过度使用继承:继承会增加类之间的耦合度,如果只是为了复用少量代码而使用继承,可能会导致设计上的问题。在这种情况下,组合可能是更好的选择。
- 忽略构造方法的调用:如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法,否则会编译错误。
示例:构造方法的调用
class Parent {
Parent(int x) {
System.out.println("Parent constructor with x = " + x);
}
}
class Child extends Parent {
Child() {
super(10); // 显式调用父类的有参构造方法
System.out.println("Child constructor");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
}
}
总结
extends
关键字是 Java 中实现继承的核心机制,合理使用继承可以提高代码的复用性和可维护性。但在实际开发中,应根据具体需求谨慎选择继承或组合,以避免过度耦合和设计上的问题。
继承的层次结构
概念定义
继承的层次结构是指通过继承机制形成的类与类之间的层级关系。在Java中,一个类可以继承另一个类(单继承),而被继承的类又可以继承其他类,从而形成一个树状的层次结构。最顶层的类通常是Object
类(Java中所有类的隐式父类),其他类逐层向下扩展。
层次结构的特点
- 单根性:Java中所有类(除
Object
类外)都有且只有一个直接父类。 - 传递性:子类会继承父类的父类的成员(属性和方法)。
- 层次深度:理论上继承层次可以无限延伸,但实践中建议控制在合理范围内(通常不超过3-4层)。
示例代码
// 顶层父类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
// 第一层子类
class Mammal extends Animal {
void breathe() {
System.out.println("Mammal is breathing");
}
}
// 第二层子类
class Dog extends Mammal {
void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.eat(); // 继承自Animal
myDog.breathe(); // 继承自Mammal
myDog.bark(); // 自身方法
}
}
使用场景
- 代码复用:共性功能在父类中实现,子类直接继承使用
- 多态实现:通过父类引用指向不同子类对象
- 分类体系:如生物分类(界-门-纲-目-科-属-种)
注意事项
- 避免过深继承:超过3层的继承会降低代码可读性,建议用组合替代
- 合理设计父类:父类应包含真正通用的属性和方法
- 继承破坏封装:子类会知道父类的实现细节
- final类限制:被声明为final的类不能被继承
常见误区
- 滥用继承:不是所有"is-a"关系都适合用继承(如"正方形是矩形"在行为上可能不成立)
- 忽略Object类:所有类都隐式继承Object,其方法(如toString())可能需重写
- 混淆继承与接口:继承强调"是什么",接口强调"能做什么"
Java 单继承特性
概念定义
Java 单继承特性指的是在 Java 中,一个类只能直接继承自一个父类。这是 Java 面向对象编程中的一个基本原则,与某些支持多继承的语言(如 C++)不同。单继承的设计简化了类的层次结构,避免了多继承可能带来的复杂性和潜在问题(如“菱形继承”问题)。
使用场景
单继承适用于以下场景:
- 构建清晰的类层次结构:例如,
Animal
作为父类,Dog
和Cat
作为子类。 - 避免方法冲突:由于只能继承一个父类,不会出现多个父类中存在同名方法时的歧义问题。
- 实现代码复用:子类可以复用父类的属性和方法,同时通过重写(Override)或扩展来实现特定功能。
常见误区与注意事项
- 误以为单继承限制过多:虽然 Java 不支持多继承,但可以通过**接口(Interface)**实现多重继承的效果(一个类可以实现多个接口)。
- 过度继承导致层次过深:单继承容易导致类层次过深(如
A → B → C → D
),建议优先使用组合(Composition)而非继承。 - 混淆继承与接口:继承(
extends
)用于“是什么”(is-a)关系,接口(implements
)用于“能做什么”(can-do)关系。
示例代码
// 父类
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
// 子类(单继承)
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating bones");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 输出:Dog is eating bones
}
}
替代方案(接口实现多继承效果)
interface Swimmable {
void swim();
}
interface Flyable {
void fly();
}
// 通过接口实现多重能力
class Duck implements Swimmable, Flyable {
@Override
public void swim() {
System.out.println("Duck is swimming");
}
@Override
public void fly() {
System.out.println("Duck is flying");
}
}
总结
Java 的单继承特性通过限制类的直接父类数量,简化了对象模型的设计。开发者应合理使用继承和接口,避免过度依赖继承层次,优先选择组合或接口实现灵活的功能扩展。
二、super 关键字详解
super 的基本作用
super
是 Java 中的一个关键字,主要用于在子类中访问父类的成员(包括属性、方法和构造方法)。它的核心作用是解决子类与父类之间的成员冲突或明确调用父类的构造方法。
1. 访问父类的成员变量
当子类中定义了与父类同名的成员变量时,super
可以明确指定访问父类的变量。
class Parent {
String name = "Parent";
}
class Child extends Parent {
String name = "Child";
void printNames() {
System.out.println(name); // 输出子类的 name ("Child")
System.out.println(super.name); // 输出父类的 name ("Parent")
}
}
2. 调用父类的方法
如果子类重写了父类的方法,可以通过 super
调用父类的原始方法。
class Parent {
void show() {
System.out.println("Parent's show");
}
}
class Child extends Parent {
@Override
void show() {
super.show(); // 先调用父类的 show()
System.out.println("Child's show");
}
}
3. 调用父类的构造方法
在子类的构造方法中,super()
必须作为第一条语句(如果显式调用),用于初始化父类的构造逻辑。
class Parent {
Parent(String msg) {
System.out.println("Parent: " + msg);
}
}
class Child extends Parent {
Child() {
super("Hello"); // 调用父类的有参构造方法
System.out.println("Child's constructor");
}
}
注意事项
- 隐式调用:如果子类构造方法没有显式调用
super()
,编译器会自动插入无参的super()
。此时若父类没有无参构造方法,会编译报错。 - 必须为首行:
super()
或this()
必须在构造方法的第一行,二者不能共存。 - 静态上下文中无效:
super
不能用于静态方法或静态代码块。
super 调用父类构造方法
概念定义
在 Java 中,super
关键字用于调用父类的构造方法。它是子类构造方法中初始化父类部分的一种方式。每个子类构造方法的第一行(如果没有显式调用 super
或 this
)会隐式调用父类的无参构造方法 super()
。
使用场景
- 显式调用父类构造方法:当父类没有无参构造方法,或需要调用父类的特定构造方法时。
- 初始化父类成员:确保父类的成员变量被正确初始化。
- 避免隐式调用问题:防止因隐式调用父类无参构造方法而导致的编译错误。
语法
super(参数列表);
- 必须在子类构造方法的第一行。
- 参数列表必须与父类的某个构造方法匹配。
示例代码
class Parent {
private String name;
// 父类构造方法(带参数)
public Parent(String name) {
this.name = name;
}
}
class Child extends Parent {
private int age;
// 子类构造方法,显式调用父类构造方法
public Child(String name, int age) {
super(name); // 调用父类的构造方法
this.age = age;
}
}
注意事项
- 必须放在第一行:
super
调用必须位于子类构造方法的第一行,否则会编译错误。 - 隐式调用:如果子类构造方法中没有显式调用
super
或this
,编译器会自动插入super()
(调用父类无参构造方法)。 - 父类无无参构造方法:如果父类没有无参构造方法,且子类未显式调用父类的其他构造方法,会导致编译错误。
- 与
this
冲突:super
和this
不能同时出现在一个构造方法中,因为它们都必须位于第一行。
常见误区
- 忽略
super
调用:如果父类没有无参构造方法,子类必须显式调用父类的某个构造方法。class Parent { public Parent(String name) {} } class Child extends Parent { // 编译错误:父类没有无参构造方法,且子类未显式调用父类构造方法 public Child() {} }
super
不在第一行:public Child() { System.out.println("Child"); // 编译错误:super 必须在第一行 super(); }
总结
super
调用父类构造方法是子类初始化过程中不可或缺的一部分,确保父类的状态被正确设置。使用时需注意其语法规则和限制,避免常见的编译错误。
super 调用父类成员变量
概念定义
在 Java 中,super
关键字用于显式访问父类的成员变量(属性)。当子类中存在与父类同名的成员变量时,可以通过 super
来明确指定访问父类的成员变量,避免命名冲突。
使用场景
- 子类覆盖父类成员变量:当子类声明了与父类同名的成员变量时,直接使用变量名会访问子类的变量,此时若需要访问父类的变量,必须使用
super
。 - 明确调用父类变量:即使子类没有覆盖父类变量,也可以通过
super
显式调用父类变量,提高代码可读性。
示例代码
class Parent {
String name = "Parent";
}
class Child extends Parent {
String name = "Child";
void printNames() {
System.out.println("子类 name: " + name); // 输出子类变量
System.out.println("父类 name: " + super.name); // 输出父类变量
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.printNames();
}
}
输出结果
子类 name: Child
父类 name: Parent
注意事项
- 静态变量不可用
super
:静态变量属于类,直接通过类名访问(如Parent.name
),super
不能用于静态变量。 - 父类变量需可见:父类的成员变量必须是子类可访问的(如
protected
或public
),否则无法通过super
访问。 - 避免滥用:如果子类未覆盖父类变量,直接使用变量名即可,无需强制使用
super
。
常见误区
- 误认为
super
指向父类对象:super
只是语法关键字,并非引用父类对象,它仅用于明确访问父类的成员。 - 混淆
this
和super
:this
指向当前对象的成员,而super
指向从父类继承的成员。
super 调用父类成员方法
概念定义
super
关键字用于在子类中访问父类的成员方法。当子类重写了父类的方法,但仍需调用父类原始实现时,可以使用 super.methodName()
的形式调用父类方法。
使用场景
- 扩展父类方法:在子类重写方法时,先调用父类方法,再添加额外逻辑。
- 避免重复代码:复用父类已实现的方法逻辑。
- 解决命名冲突:当子类成员与父类成员同名时,明确指定调用父类方法。
示例代码
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
public void eat() {
super.eat(); // 先调用父类的eat()
System.out.println("Dog is eating bones"); // 再扩展子类逻辑
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat();
}
}
注意事项
- 必须在子类方法中使用:
super
只能在子类的实例方法中调用父类方法。 - 静态方法不可用:
super
不能用于调用父类的静态方法(应使用类名直接调用)。 - 构造方法调用:
super()
用于调用父类构造方法,语法与调用普通方法不同。 - 继承链访问:
super
只能访问直接父类的方法,不能跨级访问祖父类的方法。
常见误区
- 误用静态上下文:
class Child extends Parent { public static void foo() { super.bar(); // 编译错误!不能在静态方法中使用super } }
- 忽略方法重写:
class Parent { private void method() {} // 私有方法不能被重写 } class Child extends Parent { public void test() { super.method(); // 可以编译,但实际调用的是Parent的私有方法 } }
- 混淆 super 和 this:
class Child extends Parent { public void method() { this.method(); // 递归调用子类方法,导致栈溢出 super.method(); // 正确调用父类方法 } }
super 与 this 的区别
概念定义
-
super
- 是 Java 中的一个关键字,用于引用父类的成员(属性、方法或构造方法)。
- 主要用于子类中访问父类被覆盖的方法、隐藏的字段或调用父类的构造方法。
-
this
- 是 Java 中的一个关键字,用于引用当前对象的实例。
- 主要用于区分局部变量和实例变量、调用当前类的方法或构造方法。
使用场景
-
super 的使用场景
- 访问父类的成员变量:当子类隐藏了父类的同名变量时。
class Parent { String name = "Parent"; } class Child extends Parent { String name = "Child"; void printNames() { System.out.println(super.name); // 输出 "Parent" System.out.println(this.name); // 输出 "Child" } }
- 调用父类的方法:当子类覆盖了父类的方法时。
class Parent { void display() { System.out.println("Parent method"); } } class Child extends Parent { void display() { super.display(); // 调用父类的 display() System.out.println("Child method"); } }
- 调用父类的构造方法:在子类构造方法中使用
super()
调用父类构造方法(必须放在子类构造方法的第一行)。class Parent { Parent() { System.out.println("Parent constructor"); } } class Child extends Parent { Child() { super(); // 调用父类的无参构造方法 System.out.println("Child constructor"); } }
- 访问父类的成员变量:当子类隐藏了父类的同名变量时。
-
this 的使用场景
- 区分局部变量和实例变量:当方法参数与实例变量同名时。
class Person { String name; Person(String name) { this.name = name; // this.name 指实例变量,name 指参数 } }
- 调用当前类的方法:明确调用当前类的实例方法。
class Calculator { void add(int a, int b) { System.out.println(a + b); } void calculate() { this.add(5, 10); // 调用当前类的 add 方法 } }
- 调用当前类的构造方法:在一个构造方法中调用另一个构造方法(必须放在第一行)。
class Rectangle { int width, height; Rectangle() { this(10, 20); // 调用带参数的构造方法 } Rectangle(int width, int height) { this.width = width; this.height = height; } }
- 区分局部变量和实例变量:当方法参数与实例变量同名时。
常见误区与注意事项
-
super 的常见误区
super
不能用于静态方法或静态上下文中(因为静态成员属于类,而非实例)。super()
调用父类构造方法时,必须放在子类构造方法的第一行。- 如果父类没有无参构造方法,子类必须显式调用父类的其他构造方法(如
super(参数)
)。
-
this 的常见误区
this
不能用于静态方法(因为静态方法没有实例)。this()
调用当前类构造方法时,必须放在构造方法的第一行。- 避免在方法中过度使用
this
,可能导致代码冗余(除非需要区分同名变量)。
核心区别总结
特性 | super | this |
---|---|---|
指向对象 | 父类的实例 | 当前类的实例 |
用途 | 访问父类成员或构造方法 | 访问当前类成员或构造方法 |
静态上下文中 | 不可用 | 不可用 |
构造方法调用 | 必须第一行(super() ) | 必须第一行(this() ) |
三、方法重写与 super
方法重写的概念
方法重写(Method Overriding)是面向对象编程中继承的一个重要特性,它允许子类重新定义父类中已有的方法,以实现子类特有的行为。
定义
方法重写指的是在子类中定义一个与父类中方法签名完全相同的方法(包括方法名、参数列表和返回类型),从而覆盖父类的实现。重写后的方法会在运行时根据对象的实际类型调用相应的版本。
使用场景
- 修改或扩展父类行为:子类需要改变父类方法的实现逻辑。
- 实现多态:通过父类引用调用子类重写的方法,实现运行时多态。
- 遵循接口契约:子类需要满足父类方法的约定,但具体实现可能不同。
规则与注意事项
- 方法签名必须一致:方法名、参数列表和返回类型必须与父类方法完全相同(返回类型可以是父类方法返回类型的子类,称为协变返回类型)。
- 访问权限不能更严格:子类方法的访问修饰符不能比父类方法的更严格(例如,父类是
protected
,子类不能是private
)。 - 不能重写
final
或static
方法:final
方法禁止重写,static
方法是类方法,属于静态绑定,不参与重写。 - 异常限制:子类方法抛出的异常不能比父类方法抛出的异常更宽泛(可以抛出相同、更具体或不抛出异常)。
示例代码
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // 输出 "Dog barks"(调用子类重写的方法)
}
}
常见误区
- 混淆重载与重写:重载是同一类中方法名相同但参数列表不同,而重写是子类覆盖父类方法。
- 忽略
@Override
注解:建议使用@Override
注解显式标记重写方法,编译器会检查是否符合重写规则。 - 误用静态方法:静态方法通过类名调用,不存在重写,只有隐藏(子类定义同名静态方法会隐藏父类方法)。
通过方法重写,Java 实现了运行时的多态性,使得代码更加灵活和可扩展。
@Override 注解的作用
概念定义
@Override
是 Java 中的一个注解(Annotation),用于标识一个方法是覆盖(重写)父类或接口中的方法。它的主要作用是让编译器检查该方法是否正确地重写了父类或接口中的方法。如果方法签名不匹配(例如方法名拼写错误、参数类型不一致等),编译器会报错,从而帮助开发者尽早发现潜在的错误。
使用场景
- 方法重写:当子类需要重写父类的方法时,可以使用
@Override
注解。 - 接口实现:当实现接口中的抽象方法时,可以使用
@Override
注解。 - 避免拼写错误:通过编译器检查,防止因方法名或参数类型错误导致未正确重写方法。
示例代码
// 父类
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
// 子类
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // 输出: Dog barks
}
}
常见误区或注意事项
@Override
是可选的:即使不加@Override
,方法仍然可以重写父类或接口的方法。但加上它可以提高代码的可读性和安全性。- 仅用于重写方法:
@Override
只能用于重写父类或接口的方法,不能用于普通方法或静态方法。 - 编译时检查:如果方法签名不匹配(例如父类中没有该方法),编译器会报错。
- Java 5+ 支持:
@Override
在 Java 5 中仅能用于重写父类方法,从 Java 6 开始也可以用于实现接口方法。
错误示例
class Cat extends Animal {
@Override
public void makeNoise() { // 编译错误:父类中没有 makeNoise 方法
System.out.println("Cat meows");
}
}
总结
@Override
注解的主要作用是:
- 明确表示方法是重写父类或接口的方法。
- 通过编译器检查,避免因拼写错误或签名不匹配导致的重写失败。
- 提高代码的可读性和可维护性。
使用 super 调用被重写的方法
概念定义
在 Java 中,super
关键字用于引用父类的成员(包括方法、属性和构造函数)。当子类重写了父类的方法时,可以通过 super
关键字显式调用父类中被重写的方法。这种方式允许子类在扩展或修改父类行为的同时,仍然保留父类的原始逻辑。
使用场景
- 扩展父类方法:子类需要在父类方法的基础上添加额外逻辑时,可以先调用父类方法,再添加新代码。
- 部分覆盖:子类需要修改父类方法的某些行为,但仍需保留部分原始逻辑。
- 调试或日志记录:在调用父类方法前后插入调试或日志记录代码。
示例代码
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 调用父类的 makeSound 方法
System.out.println("Dog barks: Woof!"); // 添加子类特有行为
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.makeSound();
}
}
输出结果
Animal makes a sound
Dog barks: Woof!
注意事项
- 静态方法不可用:
super
不能用于调用父类的静态方法,静态方法调用遵循编译时绑定规则。 - 构造函数调用:在子类构造函数中,
super()
必须作为第一条语句(如果显式调用)。 - 多级继承:
super
只能调用直接父类的方法,无法跨级调用祖父类的方法。 - 访问权限:通过
super
调用的方法必须是子类可访问的(非private
方法)。
常见误区
- 误认为
super
指向父类对象:super
只是一个关键字,用于显式指定调用父类的成员,并不创建父类对象。 - 过度使用:如果子类完全覆盖父类方法且不需要父类逻辑,则不应使用
super
。 - 循环调用:在父类方法中调用子类方法(如通过多态),而子类方法又通过
super
调用父类方法,可能导致无限递归。
重写规则与限制
什么是方法重写
方法重写(Override)是面向对象编程中的一个重要概念,指子类重新定义父类中已有的方法。重写后的方法必须与父类方法具有相同的方法名、参数列表和返回类型(或子类返回类型),但可以有不同的实现逻辑。
重写的基本规则
- 方法签名必须一致:子类重写的方法必须与父类方法具有相同的方法名、参数列表和返回类型(或协变返回类型)。
- 访问权限不能更严格:子类方法的访问修饰符不能比父类方法更严格。例如:
- 父类方法是
public
,子类方法必须是public
- 父类方法是
protected
,子类方法可以是protected
或public
- 父类方法是
- 不能重写 final 方法:被
final
修饰的方法不能被重写。 - 不能重写 static 方法:静态方法属于类而非实例,不能被重写(但可以隐藏)。
- 异常限制:子类方法抛出的检查异常不能比父类方法抛出的异常更宽泛。
协变返回类型
Java 5+ 支持协变返回类型,即子类重写方法的返回类型可以是父类方法返回类型的子类。例如:
class Animal {
Animal getAnimal() { return new Animal(); }
}
class Dog extends Animal {
@Override
Dog getAnimal() { return new Dog(); } // 协变返回类型
}
@Override 注解
使用 @Override
注解可以显式声明方法重写,帮助编译器检查是否正确遵循了重写规则:
class Parent {
void show() { System.out.println("Parent"); }
}
class Child extends Parent {
@Override
void show() { System.out.println("Child"); }
}
常见误区
- 误认为参数类型不同是重写:实际上是方法重载(Overload)
- 试图重写 private 方法:private 方法对子类不可见,无法重写
- 混淆重写和隐藏:静态方法只能被隐藏,不能被重写
示例代码
class Vehicle {
protected void run() {
System.out.println("Vehicle is running");
}
}
class Car extends Vehicle {
@Override
public void run() { // 扩大访问权限
System.out.println("Car is running");
}
// 错误示例:试图缩小访问权限
// @Override
// void run() {} // 编译错误
}
重写与多态
方法重写是实现运行时多态的关键机制。当通过父类引用调用被子类重写的方法时,实际执行的是子类的方法实现:
Vehicle v = new Car();
v.run(); // 输出 "Car is running"
重写(Override)与重载(Overload)的区别
概念定义
-
重写(Override)
子类对父类中同名、同参数列表、同返回类型的方法进行重新实现。- 核心:运行时多态,由 JVM 根据实际对象类型决定调用哪个方法。
- 要求:方法签名必须完全一致,访问权限不能比父类更严格。
-
重载(Overload)
在同一个类中定义同名但参数列表不同(参数类型、数量或顺序不同)的方法。- 核心:编译时多态,由编译器根据调用时的参数决定具体方法。
- 要求:方法名相同,但参数列表必须不同(返回类型可相同也可不同)。
关键区别
特性 | 重写(Override) | 重载(Overload) |
---|---|---|
作用范围 | 父子类之间 | 同一个类中 |
方法签名 | 必须完全相同 | 必须不同(仅参数列表) |
返回类型 | 必须相同或为子类(协变返回类型) | 可以不同 |
访问权限 | 不能比父类更严格(如父类为protected ,子类不能为private ) | 无限制 |
多态阶段 | 运行时多态(动态绑定) | 编译时多态(静态绑定) |
@Override 注解 | 建议显式标注 | 不可使用(编译报错) |
示例代码
重写(Override)
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() { // 方法签名与父类一致
System.out.println("Bark!");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.makeSound(); // 输出 "Bark!"(运行时决定调用子类方法)
}
}
重载(Overload)
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) { // 参数类型不同
return a + b;
}
int add(int a, int b, int c) { // 参数数量不同
return a + b + c;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // 调用 int add(int, int)
System.out.println(calc.add(1.5, 2.5)); // 调用 double add(double, double)
}
}
常见误区
-
重载与返回类型无关
仅参数列表不同即可,返回类型相同或不同均可。
❌ 错误认知:认为返回类型不同就是重载(若参数列表相同,编译报错)。 -
重写必须严格一致
子类方法抛出的异常不能比父类更宽泛(如父类抛出IOException
,子类不能抛出Exception
)。 -
静态方法不能重写
静态方法属于类,若子类定义同名静态方法,属于隐藏(Hide)而非重写。
四、构造方法与继承
子类构造方法的执行过程
在 Java 中,子类构造方法的执行过程涉及父类构造方法的隐式或显式调用,以及子类自身构造方法的初始化逻辑。以下是详细解析:
1. 构造方法调用链
- 默认情况下,子类构造方法会隐式调用父类的无参构造方法(
super()
)。 - 如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法(
super(参数)
),否则编译报错。
2. 执行顺序
子类构造方法的执行遵循以下顺序:
- 父类构造方法:优先执行父类的构造方法(通过
super()
或super(参数)
)。 - 子类成员初始化:执行子类的成员变量初始化(如
private int x = 10;
)。 - 子类构造方法代码块:执行子类构造方法中
super()
之后的代码。
3. 代码示例
class Parent {
Parent() {
System.out.println("父类无参构造方法");
}
Parent(String msg) {
System.out.println("父类有参构造方法:" + msg);
}
}
class Child extends Parent {
private int value = 10;
Child() {
// 隐式调用 super(),即 Parent()
System.out.println("子类无参构造方法,value=" + value);
}
Child(String msg) {
super(msg); // 显式调用父类有参构造方法
System.out.println("子类有参构造方法:" + msg);
}
}
public class Main {
public static void main(String[] args) {
new Child(); // 输出顺序:父类无参 -> 子类无参
new Child("Hello");// 输出顺序:父类有参 -> 子类有参
}
}
输出结果:
父类无参构造方法
子类无参构造方法,value=10
父类有参构造方法:Hello
子类有参构造方法:Hello
4. 注意事项
super()
必须位于第一行:子类构造方法中若显式调用super()
,必须放在方法体的第一行。- 父类构造方法不可被继承:子类无法直接调用父类的构造方法名(如
Parent()
),必须通过super
关键字。 - 避免递归调用:错误地在父类构造方法中调用子类方法可能导致递归或未初始化的成员访问。
5. 常见误区
- 忽略父类构造方法的调用:若父类没有无参构造方法,子类必须显式调用
super(参数)
。 - 成员初始化与构造代码块的顺序:成员变量的初始化(如
private int x = 10;
)会在super()
之后、子类构造方法代码之前执行。
通过理解上述过程,可以避免因构造方法调用不当导致的初始化问题。
隐式调用 super()
概念定义
隐式调用 super()
指的是在子类构造函数中,如果没有显式调用父类构造函数,Java 编译器会自动在子类构造函数的第一行插入对父类无参构造函数的调用(即 super()
)。这是 Java 继承机制的一部分,确保父类的初始化逻辑能够正确执行。
使用场景
- 子类构造函数未显式调用
super
或this
:当子类构造函数既没有调用super(...)
(父类构造函数),也没有调用this(...)
(本类其他构造函数)时,编译器会自动插入super()
。 - 父类存在无参构造函数:隐式调用的前提是父类必须有一个可访问的无参构造函数(即
public
或protected
修饰,或默认包访问权限且在同一包内)。
示例代码
class Parent {
Parent() {
System.out.println("Parent 无参构造函数");
}
}
class Child extends Parent {
Child() {
// 编译器会在此隐式插入 super();
System.out.println("Child 无参构造函数");
}
}
public class Main {
public static void main(String[] args) {
new Child();
}
}
输出:
Parent 无参构造函数
Child 无参构造函数
常见误区与注意事项
-
父类无无参构造函数时编译错误:
- 如果父类没有无参构造函数(例如只定义了有参构造函数),且子类未显式调用
super(...)
,编译器会报错。 - 修正方法:必须在子类构造函数中显式调用父类的有参构造函数。
class Parent { Parent(int x) { /* 有参构造函数 */ } } class Child extends Parent { Child() { super(10); // 必须显式调用 } }
- 如果父类没有无参构造函数(例如只定义了有参构造函数),且子类未显式调用
-
显式调用
this()
时不会隐式调用super()
:- 如果子类构造函数中调用了
this(...)
(重载的其他构造函数),则不会隐式调用super()
,而是由this(...)
指向的构造函数负责调用super(...)
。
- 如果子类构造函数中调用了
-
隐式调用的位置:
super()
始终是构造函数的第一条语句,无论是隐式还是显式调用。
-
继承链的初始化顺序:
- 隐式调用
super()
会触发父类的初始化,依次向上直到Object
类,确保整个继承链的构造函数按顺序执行。
- 隐式调用
显式调用父类构造方法
概念定义
显式调用父类构造方法是指在子类的构造方法中,通过 super()
关键字明确调用父类的某个构造方法。这是 Java 继承机制中确保父类成员正确初始化的关键步骤。
使用场景
- 父类没有无参构造方法时:当父类只定义了带参数的构造方法,子类必须显式调用其中一个。
- 需要初始化父类特定状态时:当子类需要使用父类的某个特定构造方法初始化父类成员时。
- 构造方法重载时:父类有多个构造方法,子类需要选择其中一个进行调用。
语法规则
super(参数列表); // 必须作为子类构造方法的第一条语句
示例代码
class Animal {
private String name;
// 父类带参数构造方法
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
private String breed;
// 子类必须显式调用父类构造方法
public Dog(String name, String breed) {
super(name); // 显式调用父类构造方法
this.breed = breed;
}
}
注意事项
- 必须作为第一条语句:
super()
调用必须是子类构造方法中的第一个语句,否则会编译错误。 - 默认调用无参构造:如果没有显式调用,编译器会自动插入
super()
(前提是父类有无参构造方法)。 - 构造方法链:父类构造方法会继续调用它的父类构造方法,直到 Object 类。
- 参数匹配:调用时必须提供与父类某个构造方法签名匹配的参数。
常见错误
class Cat extends Animal {
public Cat() {
// 编译错误:父类没有无参构造方法
// 且没有显式调用其他构造方法
}
public Cat(String name) {
System.out.println("初始化"); // 错误:super() 不是第一条语句
super(name);
}
}
多重继承下的构造方法调用
class A {
public A() {
System.out.println("A");
}
}
class B extends A {
public B() {
super(); // 可省略,编译器会自动添加
System.out.println("B");
}
}
class C extends B {
public C() {
// 这里会隐式调用 B()
System.out.println("C");
}
}
// 创建 C 实例时会输出:A B C
构造方法链
概念定义
构造方法链(Constructor Chaining)是指在类的继承体系中,一个构造方法调用另一个构造方法的过程。在Java中,构造方法链主要通过this()
和super()
关键字实现:
this()
:调用当前类的其他构造方法super()
:调用父类的构造方法
使用场景
- 代码复用:避免在多个构造方法中重复相同的初始化代码
- 初始化顺序控制:确保父类先于子类完成初始化
- 提供多种初始化方式:通过不同参数的构造方法提供灵活的初始化选择
执行规则
- 子类构造方法必须直接或间接调用父类构造方法
this()
或super()
必须是构造方法的第一条语句- 如果没有显式调用,编译器会自动插入
super()
- 调用顺序:父类构造方法 → 子类构造方法
示例代码
class Animal {
private String name;
public Animal() {
this("Unknown"); // 调用本类另一个构造方法
System.out.println("Animal无参构造");
}
public Animal(String name) {
this.name = name;
System.out.println("Animal带参构造");
}
}
class Dog extends Animal {
private String breed;
public Dog() {
super(); // 可省略,编译器会自动添加
System.out.println("Dog无参构造");
}
public Dog(String breed) {
this(breed, "Unknown"); // 调用本类另一个构造方法
System.out.println("Dog单参构造");
}
public Dog(String breed, String name) {
super(name); // 必须放在第一行
this.breed = breed;
System.out.println("Dog双参构造");
}
}
// 测试
public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog();
Dog dog2 = new Dog("Golden");
Dog dog3 = new Dog("Husky", "Max");
}
}
输出结果
Animal带参构造
Animal无参构造
Dog无参构造
Animal带参构造
Animal无参构造
Dog双参构造
Dog单参构造
Animal带参构造
Dog双参构造
注意事项
- 循环调用:避免构造方法之间形成循环调用(如A调用B,B又调用A)
- 继承限制:如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法
- 初始化顺序:实例变量初始化在构造方法执行前完成
- final字段:必须在构造方法链完成前初始化final字段
常见误区
- 认为可以同时使用
this()
和super()
(实际上只能二选一) - 忘记父类没有默认无参构造方法时会导致编译错误
- 不了解构造方法调用的实际执行顺序
- 在静态方法或普通方法中使用
this()
或super()
(这是语法错误)
构造方法不能被继承
概念定义
在 Java 中,构造方法(Constructor)是一种特殊的方法,用于在创建对象时初始化对象的状态。构造方法与类同名,没有返回类型(连 void
也没有)。构造方法不能被继承,这意味着子类不会自动继承父类的构造方法。
原因分析
- 构造方法的特殊性:构造方法用于初始化对象,而子类对象的初始化通常需要先初始化父类部分,再初始化子类部分。如果构造方法被继承,可能会导致初始化顺序混乱。
- 命名规则:构造方法必须与类同名。如果子类继承了父类的构造方法,那么子类中将存在一个与父类同名的构造方法,这与子类的命名规则冲突。
- 设计哲学:Java 的设计者认为,子类对象的构造过程应该显式地调用父类的构造方法(通过
super()
),以确保父类的初始化逻辑被执行。
使用场景
虽然构造方法不能被继承,但子类可以通过 super()
调用父类的构造方法:
class Parent {
Parent() {
System.out.println("Parent constructor");
}
}
class Child extends Parent {
Child() {
super(); // 调用父类的无参构造方法
System.out.println("Child constructor");
}
}
注意事项
- 隐式调用:如果子类的构造方法中没有显式调用
super()
,Java 编译器会自动插入对父类无参构造方法的调用。如果父类没有无参构造方法,则会编译报错。 - 显式调用:如果父类没有无参构造方法,子类必须显式调用父类的有参构造方法:
class Parent { Parent(String name) { System.out.println("Parent constructor: " + name); } } class Child extends Parent { Child() { super("Tom"); // 必须显式调用父类的有参构造方法 System.out.println("Child constructor"); } }
- 构造方法链:构造方法的调用必须放在子类构造方法的第一行,否则会编译报错。
常见误区
- 误以为构造方法可以重写:构造方法不能被重写(Override),因为子类的构造方法名必须与子类同名,而父类的构造方法名与父类同名。
- 忽略
super()
的调用:如果父类没有无参构造方法,而子类没有显式调用父类的有参构造方法,会导致编译错误。
示例代码
class Animal {
String name;
Animal(String name) {
this.name = name;
System.out.println("Animal constructor: " + name);
}
}
class Dog extends Animal {
int age;
Dog(String name, int age) {
super(name); // 必须显式调用父类的有参构造方法
this.age = age;
System.out.println("Dog constructor: " + age);
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy", 3);
}
}
输出:
Animal constructor: Buddy
Dog constructor: 3
五、继承中的访问控制
父类 private 成员的访问限制
概念定义
在 Java 中,private
是一种访问修饰符,用于限制成员(字段或方法)的可见性。父类的 private
成员(包括字段和方法)无法被子类直接访问,即使子类继承了父类。这是 Java 封装性的重要体现。
为什么 private 成员无法被子类访问?
- 封装性设计:
private
修饰的成员仅对声明它的类可见,外部类(包括子类)无法直接访问。 - 继承的局限性:虽然子类继承了父类的所有成员(包括
private
成员),但这些private
成员对子类来说是“隐藏”的,子类无法通过名称直接引用它们。
如何间接访问父类的 private 成员?
如果子类需要访问父类的 private
成员,可以通过以下方式间接实现:
- 通过父类的 public/protected 方法:如果父类提供了
public
或protected
的 getter/setter 方法,子类可以通过这些方法访问private
字段。 - 通过反射机制(不推荐):通过 Java 反射 API 强行访问
private
成员(会破坏封装性,通常不建议使用)。
示例代码
class Parent {
private String privateField = "父类的private字段";
// 提供一个protected方法供子类间接访问private字段
protected String getPrivateField() {
return privateField;
}
}
class Child extends Parent {
public void printParentPrivateField() {
// 直接访问会编译错误:privateField 不可见
// System.out.println(privateField);
// 通过父类的protected方法间接访问
System.out.println(getPrivateField());
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.printParentPrivateField(); // 输出:父类的private字段
}
}
注意事项
- 不要滥用反射:虽然反射可以绕过访问限制,但会破坏代码的封装性和安全性。
- 合理设计父类:如果某些成员需要被子类使用,应该使用
protected
而非private
。 - 继承与封装的关系:
private
成员的存在提醒我们,继承不是“完全拥有”,而是“受控的复用”。
常见误区
- 误区:“子类继承了父类的 private 成员,所以可以直接使用它们。”
- 纠正:子类确实继承了 private 成员(它们在内存中存在),但无法通过名称直接访问。
- 误区:“可以通过子类对象强制转型为父类来访问 private 成员。”
- 纠正:转型不会改变访问权限,private 成员始终仅对声明它的类可见。
protected 成员的继承特性
概念定义
在 Java 中,protected
是一种访问修饰符,用于限制类成员的访问范围。protected
成员具有以下特性:
- 当前类内部:可以自由访问。
- 同一包内的其他类:可以自由访问。
- 不同包的子类:可以通过继承访问(直接访问或通过
super
关键字)。 - 不同包的非子类:不可访问。
protected
成员的继承特性主要体现在子类可以继承父类的 protected
成员,即使子类和父类不在同一个包中。
使用场景
protected
通常用于以下场景:
- 需要被子类继承但不想完全公开的成员:例如工具方法或某些核心实现细节。
- 框架或库设计:允许子类扩展功能,但不暴露给普通用户。
示例代码
// 父类(包 com.example)
package com.example;
public class Parent {
protected String protectedField = "Protected Field";
protected void protectedMethod() {
System.out.println("Protected Method");
}
}
// 子类(包 com.test)
package com.test;
import com.example.Parent;
public class Child extends Parent {
public void accessProtected() {
// 直接访问继承的 protected 成员
System.out.println(protectedField);
protectedMethod();
// 通过 super 访问父类的 protected 成员
System.out.println(super.protectedField);
super.protectedMethod();
}
}
常见误区与注意事项
-
不同包的子类必须通过继承访问:
- 子类可以直接访问继承的
protected
成员,但不能通过父类实例访问。例如:Parent parent = new Parent(); // 编译错误:不同包的子类不能通过实例访问 protected 成员 // System.out.println(parent.protectedField);
- 子类可以直接访问继承的
-
包内访问权限:
- 同一包内的类(无论是否有继承关系)都可以直接访问
protected
成员。
- 同一包内的类(无论是否有继承关系)都可以直接访问
-
子类覆盖
protected
方法时:- 子类可以覆盖父类的
protected
方法,但不能降低访问权限(例如改为private
)。
- 子类可以覆盖父类的
-
静态
protected
成员:- 静态
protected
成员同样遵循上述规则,但可以通过类名直接访问(在同一包或子类中)。
- 静态
总结
protected
成员的继承特性是 Java 封装与多态的重要体现,它平衡了“隐藏实现细节”和“允许子类扩展”的需求。正确使用 protected
可以提高代码的可维护性和扩展性。
默认访问修饰符的影响
概念定义
默认访问修饰符(也称为包级访问修饰符)是指在Java中没有显式声明任何访问修饰符(如public
、protected
、private
)时的默认权限。具有默认访问权限的类、方法或属性仅在同一个包内可见,对其他包中的类不可见。
使用场景
- 包内共享:当需要让某些类、方法或属性在同一包内的其他类中使用,但不需要暴露给包外时,可以使用默认访问修饰符。
- 封装性:适用于模块化设计,限制外部包对内部实现的直接访问。
常见误区或注意事项
- 跨包不可见:默认访问修饰符的成员对其他包中的类不可见,即使通过继承也无法访问。
- 子类限制:如果子类与父类不在同一个包中,子类无法继承父类的默认访问权限的成员(方法或属性)。
- 接口中的方法:接口中的方法默认是
public
的,即使不显式声明,这与类的默认访问修饰符不同。
示例代码
// 包 com.example.package1
package com.example.package1;
class DefaultAccessClass { // 默认访问修饰符的类
int defaultField = 10; // 默认访问修饰符的属性
void defaultMethod() { // 默认访问修饰符的方法
System.out.println("This is a default access method.");
}
}
// 包 com.example.package2(另一个包)
package com.example.package2;
import com.example.package1.DefaultAccessClass;
public class TestClass {
public static void main(String[] args) {
DefaultAccessClass obj = new DefaultAccessClass(); // 编译错误:DefaultAccessClass不可见
obj.defaultMethod(); // 即使能创建对象,此方法也无法访问
}
}
对比其他访问修饰符
访问修饰符 | 类内 | 包内 | 子类(不同包) | 其他包 |
---|---|---|---|---|
public | ✔️ | ✔️ | ✔️ | ✔️ |
protected | ✔️ | ✔️ | ✔️ | ❌ |
默认 | ✔️ | ✔️ | ❌ | ❌ |
private | ✔️ | ❌ | ❌ | ❌ |
实际开发建议
- 谨慎使用默认修饰符:在需要明确限制访问范围时使用,避免因疏忽导致意外暴露。
- 优先使用显式修饰符:即使需要包内可见,也建议显式使用
protected
或public
以提高代码可读性。 - 模块化设计:合理划分包结构,利用默认修饰符实现包内高内聚、包间低耦合。
继承与封装的关系
概念定义
继承和封装是面向对象编程(OOP)中的两大核心特性,它们共同构建了代码的复用性和安全性。
- 继承:允许一个类(子类)基于另一个类(父类)来构建,继承父类的属性和方法,同时可以扩展或修改其行为。
- 封装:通过将数据(属性)和操作数据的方法(行为)捆绑在一个类中,并对外隐藏内部实现细节,仅暴露必要的接口。
继承与封装的协同作用
-
继承增强封装
子类通过继承可以复用父类的封装逻辑,无需重新实现。例如:public class Animal { private String name; // 封装的私有属性 public String getName() { return name; } // 暴露的公共方法 } public class Dog extends Animal { // Dog 直接复用 Animal 的 getName() 方法,无需重新封装 name }
-
封装保护继承的完整性
- 父类通过
private
限制子类直接访问敏感数据,强制通过方法(如getter/setter
)操作,避免破坏业务逻辑。 - 若父类属性为
protected
,子类可直接访问,但需谨慎使用(可能破坏封装性)。
- 父类通过
冲突与注意事项
-
继承可能破坏封装
- 子类覆盖父类方法时,若未遵循父类契约(如修改输入/输出约束),会导致父类封装的行为失效。
- 示例风险代码:
public class Account { protected double balance; public void withdraw(double amount) { if (amount > 0 && amount <= balance) { // 封装校验逻辑 balance -= amount; } } } public class UnsecuredAccount extends Account { @Override public void withdraw(double amount) { balance -= amount; // 子类绕过校验,破坏封装 } }
-
设计原则
- 优先组合而非继承:若继承会导致封装泄露,使用组合(将父类作为成员变量)更安全。
- 使用
final
保护关键方法:防止子类覆盖破坏封装,如:public class SecureClass { public final void criticalMethod() { /* 不可覆盖 */ } }
示例:合理结合继承与封装
// 封装良好的父类
public class Vehicle {
private String engineType; // 私有属性
public final void startEngine() { // 禁止覆盖的关键方法
checkEngine();
System.out.println("Engine started");
}
protected void checkEngine() { // 允许子类扩展的受保护方法
if (engineType == null) throw new IllegalStateException();
}
}
// 子类在封装约束下扩展
public class ElectricCar extends Vehicle {
public ElectricCar() {
super.setEngineType("Electric"); // 通过方法修改私有属性
}
@Override
protected void checkEngine() { // 合法扩展
super.checkEngine();
System.out.println("Battery level checked");
}
}
访问控制的最佳实践
1. 概念定义
访问控制是面向对象编程中的重要机制,用于限制类、方法、变量的可访问范围。Java 提供了四种访问修饰符:
private
:仅在当前类中可见default
(默认,不写修饰符):同一包内可见protected
:同一包内及子类可见public
:所有类可见
2. 使用场景
-
private
使用场景- 封装类的内部实现细节
- 保护成员不被外部直接修改
public class BankAccount { private double balance; // 余额应被保护 }
-
default
使用场景- 同一包内的辅助类共享访问
- 包级私有工具方法
class PackageHelper { // 默认访问修饰符 void helperMethod() {...} }
-
protected
使用场景- 允许子类扩展父类功能
- 框架中需要被子类重写的方法
public class Shape { protected void drawInternal() {...} }
-
public
使用场景- 对外提供的API接口
- 常量定义
public class MathUtils { public static final double PI = 3.14159; }
3. 最佳实践原则
-
最小权限原则
- 所有成员默认使用最严格的
private
- 只有必须暴露的才放宽访问权限
- 所有成员默认使用最严格的
-
封装关键数据
- 字段永远使用
private
- 通过getter/setter控制访问
public class Employee { private String id; public String getId() { return this.id; } }
- 字段永远使用
-
接口与实现分离
- 公开接口使用
public
- 实现类使用
default
或private
- 公开接口使用
-
继承设计规范
- 允许子类重写的方法用
protected
- 慎用
protected
字段(优先使用方法)
- 允许子类重写的方法用
4. 常见误区
-
过度使用public
// 反例:不应公开字段 public class User { public String password; }
-
protected滥用
- 避免用
protected
暴露内部实现细节 - 非继承体系内的类不应看到
protected
成员
- 避免用
-
default的隐式风险
- 忘记写修饰符可能导致意外包内访问
- 建议显式写出
package-private
注释
5. 特殊场景处理
-
测试类访问
- 测试需要时使用
@VisibleForTesting
注解 - 而非放宽原有访问权限
- 测试需要时使用
-
记录类(record)
public record Point(int x, int y) {} // 编译器自动生成private final字段和public访问方法
-
模块化系统(JPMS)
- 需要额外考虑
module-info.java
中的exports控制 - 比普通的访问修饰符具有更高优先级
- 需要额外考虑
六、继承的高级特性
final 类与继承
概念定义
在 Java 中,final
关键字用于修饰类时,表示该类不能被继承。也就是说,final
类是继承链的终点,任何尝试继承 final
类的行为都会导致编译错误。
使用场景
- 安全性:当类的设计已经非常完善,不希望被其他类继承并修改其行为时,可以使用
final
修饰。 - 不可变性:例如
String
类被声明为final
,确保其不可变性,防止子类覆盖其方法。 - 性能优化:
final
类的方法可以被 JVM 内联优化,提高执行效率。
常见误区与注意事项
final
类的方法默认是final
的:即使没有显式声明为final
,final
类中的所有方法也不能被覆盖(因为类本身不能被继承)。final
类仍然可以实现接口:final
类可以正常实现接口,但不能被其他类继承。final
类可以有非final
的成员变量:final
仅限制类的继承性,不影响成员变量的可变性。
示例代码
// 定义一个 final 类
final class FinalClass {
public void display() {
System.out.println("This is a final class.");
}
}
// 尝试继承 final 类(编译错误)
// class SubClass extends FinalClass { } // 错误: 无法继承 final 类
public class Main {
public static void main(String[] args) {
FinalClass obj = new FinalClass();
obj.display(); // 输出: This is a final class.
}
}
常见 final
类示例
Java 标准库中的一些 final
类:
String
Integer
、Double
等包装类Math
System
抽象类与继承
抽象类的定义
抽象类是指用 abstract
关键字修饰的类,它不能被实例化,只能被继承。抽象类通常用于定义一些通用的属性和方法,而具体的实现由其子类完成。
抽象类的特点
- 不能被实例化:抽象类不能通过
new
关键字创建对象。 - 可以包含抽象方法:抽象方法是没有方法体的方法,用
abstract
修饰,子类必须实现这些方法。 - 可以包含普通方法和成员变量:抽象类可以像普通类一样定义非抽象方法和成员变量。
- 子类必须实现所有抽象方法:除非子类也是抽象类,否则必须实现父类的所有抽象方法。
抽象类的使用场景
- 定义通用模板:当多个类有共同的属性和方法,但某些方法的具体实现不同时,可以使用抽象类定义模板。
- 强制子类实现特定方法:通过抽象方法强制子类实现某些功能,确保子类的行为符合预期。
- 代码复用:抽象类可以包含通用的非抽象方法,子类可以直接继承使用,减少重复代码。
示例代码
// 定义一个抽象类
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
// 抽象方法,子类必须实现
public abstract void makeSound();
// 普通方法,子类可以直接继承
public void eat() {
System.out.println(name + " is eating.");
}
}
// 子类继承抽象类
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog("Buddy");
dog.makeSound(); // 输出: Woof!
dog.eat(); // 输出: Buddy is eating.
}
}
常见误区与注意事项
- 抽象类必须有子类:抽象类本身不能实例化,必须通过子类来使用。
- 抽象方法不能是私有的:抽象方法需要被子类实现,因此不能使用
private
修饰。 - 抽象类可以有构造方法:虽然抽象类不能实例化,但可以有构造方法供子类调用。
- 抽象类可以没有抽象方法:即使没有抽象方法,抽象类也不能实例化。
抽象类与接口的区别
- 实现方式:抽象类通过
extends
继承,接口通过implements
实现。 - 方法实现:抽象类可以有方法实现,接口在 Java 8 之前只能有抽象方法。
- 多继承:Java 不支持多继承,但一个类可以实现多个接口。
- 设计目的:抽象类用于代码复用和模板设计,接口用于定义行为规范。
继承与多态的关系
概念定义
继承和多态是面向对象编程(OOP)中的两个核心概念,它们密切相关但又各自独立。继承允许一个类(子类)继承另一个类(父类)的属性和方法,而多态则允许子类以不同的方式实现父类的方法,从而实现“一个接口,多种实现”的效果。
多态的实现方式
多态主要通过以下两种方式实现:
- 方法重写(Override):子类可以重写父类的方法,提供不同的实现。
- 向上转型(Upcasting):父类引用可以指向子类对象,从而在运行时调用子类的方法。
继承为多态提供基础
继承是多态的前提条件。只有子类继承了父类的方法,才能通过重写这些方法来实现多态。例如:
class Animal {
void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // 父类对象
Animal myDog = new Dog(); // 向上转型
Animal myCat = new Cat(); // 向上转型
myAnimal.sound(); // 输出: Animal makes a sound
myDog.sound(); // 输出: Dog barks(多态)
myCat.sound(); // 输出: Cat meows(多态)
}
}
多态的优势
- 代码复用性:通过继承复用父类代码。
- 扩展性:新增子类时无需修改父类代码。
- 灵活性:运行时动态绑定,提高程序的可维护性。
注意事项
- 方法重写的规则:
- 子类方法必须与父类方法同名、同参数列表、同返回类型(或子类返回类型)。
- 访问修饰符不能比父类更严格(如父类是
protected
,子类不能是private
)。
- 静态方法不支持多态:静态方法在编译时绑定,无法通过重写实现多态。
- 字段(成员变量)无多态:字段的访问由引用类型决定,而非实际对象类型。
示例:多态的实际应用
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Square extends Shape {
@Override
void draw() {
System.out.println("Drawing a square");
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = new Shape[3];
shapes[0] = new Shape();
shapes[1] = new Circle(); // 向上转型
shapes[2] = new Square(); // 向上转型
for (Shape shape : shapes) {
shape.draw(); // 多态调用
}
}
}
输出:
Drawing a shape
Drawing a circle
Drawing a square
继承与组合的对比
概念定义
-
继承(Inheritance)
继承是一种 “is-a” 关系,子类通过继承父类获得其属性和方法,并可以扩展或重写功能。- 示例:
Dog extends Animal
(狗是一种动物)。
- 示例:
-
组合(Composition)
组合是一种 “has-a” 关系,通过在一个类中引用其他类的对象来实现功能复用。- 示例:
Car
类包含Engine
类的实例(汽车有发动机)。
- 示例:
使用场景
继承 | 组合 |
---|---|
适用于逻辑上的层次关系(如动物→狗)。 | 适用于部分与整体的关系(如汽车→发动机)。 |
需要复用父类代码并扩展行为时。 | 需要动态切换组件或避免继承的紧耦合时。 |
子类与父类有强相关性(如方法重写)。 | 组件可独立存在或替换(如更换汽车引擎)。 |
代码示例
继承示例
class Animal {
void eat() { System.out.println("Eating..."); }
}
class Dog extends Animal { // Dog "is-a" Animal
void bark() { System.out.println("Barking..."); }
}
组合示例
class Engine {
void start() { System.out.println("Engine started."); }
}
class Car {
private Engine engine; // Car "has-a" Engine
Car() { this.engine = new Engine(); }
void start() { engine.start(); }
}
常见误区与注意事项
-
继承的缺点
- 紧耦合:父类修改可能破坏子类逻辑。
- 多继承问题:Java 不支持多继承(需用接口替代)。
- 灵活性差:无法在运行时动态切换父类行为。
-
组合的优势
- 松耦合:通过接口依赖,易于替换组件(如
Car
可换ElectricEngine
)。 - 复用性:可复用多个类的功能(如
Car
同时组合Engine
和Wheel
)。 - 符合设计原则:优先组合能更好地遵循 “组合优于继承”(Composition over Inheritance)原则。
- 松耦合:通过接口依赖,易于替换组件(如
如何选择?
- 使用继承:当需要 表达类型关系 且子类是父类的特殊化(如
Bird
→Penguin
)。 - 使用组合:当需要 复用功能 但不存在逻辑上的层级关系(如
Student
使用Address
类)。
Object 类的继承关系
概念定义
在 Java 中,Object
类是所有类的根类(或称为超类)。如果一个类没有显式地继承其他类,那么它默认继承 Object
类。例如:
class MyClass { // 隐式继承 Object
// ...
}
等价于:
class MyClass extends Object { // 显式继承 Object
// ...
}
继承关系的特点
- 单根继承:Java 是单继承语言,每个类(除
Object
外)只能有一个直接父类,但最终都会追溯到Object
。 - 默认继承:即使不写
extends Object
,编译器也会自动加上。 - 顶级父类:
Object
类本身没有父类。
继承关系示例
class Animal { // 隐式继承 Object
// ...
}
class Dog extends Animal { // Dog -> Animal -> Object
// ...
}
此时继承链为:
Dog
→ Animal
→ Object
Object 类的重要方法
由于所有类继承 Object
,以下方法可以被任何对象调用:
toString()
:返回对象的字符串表示(默认返回类名@哈希值)。equals(Object obj)
:比较对象内容是否相等(默认比较地址)。hashCode()
:返回对象的哈希码。getClass()
:返回对象的运行时类。clone()
:创建并返回对象的副本。finalize()
:垃圾回收前调用的方法(已废弃)。
使用场景
- 通用方法重写:
例如重写toString()
方便调试,或重写equals()
和hashCode()
用于集合比较。@Override public String toString() { return "Dog[name=" + name + "]"; }
- 多态兼容:
可以用Object
类型接收任意对象(如集合类ArrayList<Object>
)。Object obj = new Dog(); // 向上转型
注意事项
- 数组也是对象:
数组类型隐式继承Object
,例如int[]
可以直接调用Object
的方法。int[] arr = new int[10]; System.out.println(arr.toString()); // 合法
- 接口不继承 Object:
接口虽然可以声明Object
的方法(如toString()
),但这是语法糖,接口本身没有继承关系。 - 基本类型不是对象:
int
、char
等基本类型不继承Object
,但其包装类(如Integer
)继承Object
。
示例代码
public class Main {
public static void main(String[] args) {
// 所有对象都可以赋值给 Object
Object obj1 = new String("Hello");
Object obj2 = new Integer(100);
// 调用 Object 的方法
System.out.println(obj1.getClass()); // 输出 class java.lang.String
System.out.println(obj2.hashCode()); // 输出 100
}
}
七、常见问题与最佳实践
何时使用继承
继承是面向对象编程(OOP)的核心特性之一,它允许一个类(子类)继承另一个类(父类)的属性和方法。合理使用继承可以提高代码的复用性和可维护性,但滥用继承可能导致代码结构混乱。以下是继承的适用场景和注意事项。
1. 满足“is-a”关系
继承最适合用于表示“子类是父类的一种”的关系。例如:
Dog
是Animal
的一种。Car
是Vehicle
的一种。
示例代码:
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Dog is barking");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 继承自 Animal
dog.bark(); // Dog 的特有方法
}
}
2. 需要复用父类的代码
如果多个类有共同的属性和方法,可以将这些共性提取到父类中,子类通过继承复用这些代码,避免重复。
示例场景:
- 多个图形类(
Circle
、Rectangle
)都有color
属性和draw()
方法,可以提取到Shape
父类中。
3. 需要扩展或修改父类的行为
子类可以通过重写(Override)父类的方法来扩展或修改父类的行为。
示例代码:
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
4. 需要实现多态
继承是实现多态的基础。通过父类引用指向子类对象,可以在运行时调用子类的方法。
示例代码:
Shape shape = new Circle();
shape.draw(); // 输出 "Drawing a circle"
何时避免使用继承
1. 不满足“is-a”关系
如果两个类之间的关系不是“is-a”,而是“has-a”或“uses-a”,则应使用组合(Composition)而非继承。
反例:
Car
继承Engine
(错误,因为“Car has an Engine”更适合组合)。Student
继承Classroom
(错误,因为“Student uses a Classroom”更适合组合)。
2. 父类频繁变化
如果父类的实现经常变化,子类可能需要频繁调整,导致代码脆弱。此时应优先使用组合或接口。
3. 多重继承的需求
Java 不支持多重继承(一个类继承多个父类),如果需要多重继承的特性,应使用接口(Interface)实现。
总结
继承的最佳实践:
- 严格遵循“is-a”关系。
- 优先使用组合替代继承(尤其是“has-a”关系)。
- 避免过度继承,保持层级简单(通常不超过 3 层)。
- 多使用接口实现多态,减少对继承的依赖。
避免过度继承
概念定义
过度继承(Over-Inheritance)是指在面向对象编程中,过度使用继承机制导致类层次结构过于复杂、难以维护的现象。当子类继承父类时,虽然可以复用代码,但如果继承层次过深或继承关系设计不合理,会导致代码的可读性、可维护性和灵活性下降。
使用场景
继承通常用于以下场景:
- 代码复用:子类继承父类的属性和方法,避免重复代码。
- 多态性:通过父类引用指向子类对象,实现运行时多态。
- 扩展功能:子类可以在父类的基础上添加新的功能或覆盖父类的方法。
然而,继承并非适用于所有场景。以下情况应避免使用继承:
- 子类和父类之间没有真正的“is-a”关系(例如,
Student
继承Person
是合理的,但Student
继承Database
就不合理)。 - 需要频繁修改父类,导致子类行为不稳定。
- 继承层次过深(通常超过 3 层)。
常见误区或注意事项
- 违反单一职责原则:如果父类承担了过多的职责,子类可能会继承不必要的功能,导致代码臃肿。
- 脆弱的基类问题:父类的修改可能会影响所有子类,尤其是当继承层次较深时。
- 组合优于继承:如果可以通过组合(即在一个类中持有另一个类的实例)实现功能,通常比继承更灵活。
- 过度设计:为了“复用”而强行使用继承,导致类之间的关系混乱。
示例代码
以下是一个过度继承的反例:
// 过度继承的示例
class Animal {
void eat() { System.out.println("Eating..."); }
}
class Dog extends Animal {
void bark() { System.out.println("Barking..."); }
}
class GuardDog extends Dog {
void guard() { System.out.println("Guarding..."); }
}
class PoliceDog extends GuardDog {
void detectDrugs() { System.out.println("Detecting drugs..."); }
}
// 使用组合替代继承的改进方案
class Animal {
void eat() { System.out.println("Eating..."); }
}
class Dog {
private Animal animal;
Dog(Animal animal) { this.animal = animal; }
void eat() { animal.eat(); }
void bark() { System.out.println("Barking..."); }
}
class GuardDog {
private Dog dog;
GuardDog(Dog dog) { this.dog = dog; }
void eat() { dog.eat(); }
void bark() { dog.bark(); }
void guard() { System.out.println("Guarding..."); }
}
如何避免过度继承
- 优先使用组合:通过持有其他类的实例来实现功能,而不是继承。
- 遵循“is-a”关系:只有子类和父类之间存在明确的“是一种”关系时才使用继承。
- 限制继承层次:尽量避免超过 3 层的继承。
- 使用接口或抽象类:如果需要多态性,可以优先使用接口或抽象类。
继承中的设计原则
1. 里氏替换原则(LSP)
定义:子类必须能够替换其父类,而不影响程序的正确性。
核心思想:父类出现的地方,子类可以无缝替换,且行为一致。
示例:
class Bird {
void fly() {
System.out.println("Flying");
}
}
class Sparrow extends Bird { // 符合LSP
@Override
void fly() {
System.out.println("Sparrow flying");
}
}
// 违反LSP的反例
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("Penguins can't fly!");
}
}
注意事项:
- 子类不应修改父类已定义的行为(如抛出新异常或返回类型不兼容)。
- 子类可通过扩展增加新功能,但不应削弱父类的契约。
2. 单一职责原则(SRP)
定义:一个类应只有一个引起变化的原因。继承时应确保子类不会因父类职责过多而承担额外责任。
示例:
// 违反SRP的父类
class Worker {
void work() { /* ... */ }
void eat() { /* ... */ } // 非核心职责
}
// 改进:拆分职责
class Workable {
void work() { /* ... */ }
}
class Eatable {
void eat() { /* ... */ }
}
class Human extends Workable, Eatable { /* 多继承需通过接口实现 */ }
3. 开闭原则(OCP)
定义:对扩展开放,对修改关闭。通过继承扩展功能,而非修改父类。
示例:
abstract class Shape {
abstract double area();
}
class Circle extends Shape { // 扩展新形状无需修改父类
private double radius;
@Override
double area() {
return Math.PI * radius * radius;
}
}
4. 组合优于继承
场景:当父子类关系不满足“is-a”时,优先使用组合。
优势:
- 避免继承层次过深。
- 更灵活地切换行为(如通过注入依赖)。
示例:
// 继承方式(不推荐)
class Engine extends Car { /* ... */ }
// 组合方式(推荐)
class Car {
private Engine engine;
void setEngine(Engine e) { this.engine = e; }
}
5. 避免过度继承
问题:深度继承会增加代码复杂度,降低可维护性。
建议:
- 继承层次不超过3层。
- 使用接口或组合替代多层继承。
6. 迪米特法则(LoD)
影响:子类应尽量减少对父类内部细节的依赖,仅通过公开方法交互。
反例:
class Parent {
private String secret = "data";
}
class Child extends Parent {
void printSecret() {
System.out.println(secret); // 直接访问父类私有字段,违反LoD
}
}
继承的性能考虑
内存开销
-
继承层级深度:每增加一层继承关系,JVM 需要维护额外的类元数据(如方法表指针)。深度继承链会导致:
class A {} class B extends A {} // 1层 class C extends B {} // 2层 // 每层增加约8-16字节的元数据开销
-
字段继承:子类会包含所有父类的实例字段,即使未使用:
class Parent { int x; } class Child extends Parent { int y; } // Child对象实际占用内存:x + y
方法调用性能
-
虚方法表(vtable)查找:
- 非
private
/static
/final
方法会进入虚方法表 - 调用时需要1-2次指针跳转(HotSpot优化后通常1次)
class Animal { void eat() {} // 虚方法 } class Dog extends Animal { void eat() {} // 覆盖方法 } // 调用animal.eat()需要查vtable
- 非
-
final方法优化:
class Parent { final void foo() {} // 可直接内联调用 }
初始化成本
- 构造函数链调用:
class Base { Base() { /* 初始化逻辑 */ } } class Derived extends Base { Derived() { super(); // 隐含调用父类构造器 /* 子类初始化 */ } } // 多层继承会导致构造调用链变长
缓存局部性影响
- 对象布局:父类字段通常排在内存前部,可能导致:
- 子类频繁访问的字段与父类字段不在同一缓存行
- 示例内存布局:
[Parent fields][Child fields]
优化建议
- 控制继承深度:推荐≤3层(Java核心库如
ArrayList
仅2层继承) - 优先使用组合:特别是存在大量未使用父类成员时
// 优于继承: class Engine {} class Car { private Engine engine; // 组合 }
- final类/方法:阻止进一步继承可消除vtable查找
- 字段排列:高频访问字段尽量声明在子类
JVM层优化
- 类加载缓存:HotSpot会缓存解析好的方法调用
- 内联优化:对
final
/private
方法直接内联代码 - 虚调用优化:通过类型profile优化多态调用
性能测试对比
// 测试用例:直接调用 vs 继承调用
interface Worker { void work(); }
class FastWorker implements Worker {
public void work() { /* 无继承 */ }
}
class InheritedWorker extends SomeBase {
public void work() { /* 通过继承 */ }
}
// 通常直接调用快5-10纳秒/次(极端优化场景)
典型错误案例分析
1. 误用 super 调用父类构造方法
场景:子类构造方法中未正确调用父类构造方法,导致编译错误或运行时异常。
错误示例:
class Parent {
private String name;
public Parent(String name) {
this.name = name;
}
}
class Child extends Parent {
public Child() { // 编译错误:父类没有无参构造方法
System.out.println("Child constructor");
}
}
正确写法:
class Child extends Parent {
public Child() {
super("default"); // 必须显式调用父类有参构造方法
System.out.println("Child constructor");
}
}
2. super 调用位置错误
场景:在构造方法中,super() 或 this() 不是第一条语句。
错误示例:
class Child extends Parent {
public Child() {
System.out.println("Initializing..."); // 编译错误
super("default"); // super() 必须在第一行
}
}
注意事项:
- Java 要求 super() 或 this() 必须是构造方法的第一条语句
- 如果父类有无参构造方法,编译器会隐式添加 super()
3. 多层继承中的 super 混淆
场景:在多级继承中错误理解 super 的指向。
错误理解示例:
class GrandParent {
void show() {
System.out.println("GrandParent");
}
}
class Parent extends GrandParent {
void show() {
System.out.println("Parent");
}
}
class Child extends Parent {
void show() {
super.super.show(); // 语法错误:不能这样跳过父级
}
}
正确做法:
class Child extends Parent {
void show() {
super.show(); // 只能调用直接父类的方法
// 如果需要调用祖父类方法,需要在父类中提供访问方式
}
}
4. 静态方法中使用 super
场景:试图在静态方法中使用 super 关键字。
错误示例:
class Parent {
void instanceMethod() {}
}
class Child extends Parent {
static void staticMethod() {
super.instanceMethod(); // 编译错误:无法从静态上下文引用super
}
}
原因:
- super 关键字代表的是父类对象的引用
- 静态方法不依赖于具体实例,因此不能使用 super
5. 字段隐藏导致的混淆
场景:父子类同名字段引发的理解误区。
易混淆示例:
class Parent {
String value = "Parent";
}
class Child extends Parent {
String value = "Child";
void print() {
System.out.println(value); // 输出 Child
System.out.println(super.value); // 输出 Parent
System.out.println(((Parent)this).value); // 仍然输出 Parent
}
}
关键点:
- 字段不会被重写,只会被隐藏
- 通过 super 可以访问被隐藏的父类字段
- 即使强制转型,访问的也是声明类型的字段
6. 误用 super 调用被重写的静态方法
场景:试图用 super 调用父类的静态方法。
错误理解示例:
class Parent {
static void method() {
System.out.println("Parent static");
}
}
class Child extends Parent {
static void method() {
super.method(); // 编译警告:应该以静态方式访问静态方法
System.out.println("Child static");
}
}
正确做法:
class Child extends Parent {
static void method() {
Parent.method(); // 应该使用类名调用静态方法
System.out.println("Child static");
}
}
原理:
- 静态方法属于类而非实例
- 静态方法不存在重写概念,只有隐藏
- 应该使用类名而非 super 调用静态方法