继承和多态是 Java 面向对象体系的两大支柱。继承解决的是代码复用问题,让子类自动获得父类的属性和方法;多态解决的是扩展性问题,让同一段调用代码能够表现出不同的具体行为。这两个概念看似简单,但背后的语法细节相当多,比如构造器的隐式调用链、方法重写的访问权限约束、动态绑定的触发条件等等,任何一处理解偏差都可能导致运行时的意外结果。这篇文章会系统梳理这些语法基础,配合可运行的代码示例,把容易混淆的点逐一讲清。

继承的基本语法与成员访问规则
Java 中使用 extends 关键字实现类的继承,子类会继承父类的非私有成员。需要注意 Java 只支持单继承,一个类只能有一个直接父类,这与 C++ 的多继承形成鲜明对比。下面是一个最基础的继承结构:
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " 正在吃东西");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类构造器,必须是第一条语句
}
public void fetch() {
System.out.println(name + " 在捡球");
}
}
这段代码里有几个关键点值得展开。第一,构造器不会被继承,子类必须在自己的构造器中通过 super(...) 显式调用父类构造器;如果不写,编译器会自动插入一条 super() 调用,前提是父类存在无参构造器,否则编译直接报错。这也是为什么建议给每个类保留无参构造器的原因之一。
第二,成员的访问控制直接影响继承效果。private 成员虽然本质上存在于子类对象中,但子类代码无法直接访问;protected 成员则对子类和同包类开放;默认访问级别只对同包开放。经验做法是父类中希望被子类直接使用的字段声明为 protected,而更推荐的方式是保持字段 private,通过 public 或 protected 方法暴露访问能力,这样父类对内部实现保留修改余地。
第三,方法解析遵循就近原则。当子类定义了与父类同名同参数的方法时,子类实例调用的是子类版本,这就是方法重写的前奏。但如果只是方法名相同而参数列表不同,那就是方法重载,两者经常被混淆,后文会专门对比。
方法重写与方法重载的区别
重写和重载虽然只有一字之差,语义却完全不同。重写是子类覆盖父类的同签名方法,运行时由实际对象类型决定调用哪个版本;重载是同一个类中定义多个同名但参数不同的方法,编译期就确定了调用目标。可以用一张表来对比:
| 对比项 | 方法重写 | 方法重载 |
|---|---|---|
| 发生范围 | 父子类之间 | 同一个类中 |
| 参数列表 | 必须完全相同 | 必须不同 |
| 返回类型 | 相同或为子类型(协变返回) | 无要求 |
| 访问权限 | 不能比父类更严格 | 无要求 |
| 异常 | 不能抛出更宽的检查异常 | 无要求 |
| 绑定时机 | 运行时(动态绑定) | 编译期(静态绑定) |
重写有几条容易被忽视的硬性规则。访问权限只能放宽不能收紧,比如父类方法是 protected,子类重写时可以改成 public,但不能改成 private。static 方法不能被重写,子类中定义同名静态方法属于方法隐藏,调用结果取决于引用类型而非对象类型,这是面试和实际开发中常见的坑。此外,加 @Override 注解是个好习惯,编译器会帮你检查方法签名是否真的匹配上了父类的方法,避免因拼写错误造成假重写。
class Father {
protected Number getValue() {
return 10;
}
static void sayHello() {
System.out.println("Father hello");
}
}
class Son extends Father {
@Override
public Integer getValue() { // 访问权限放宽,返回类型协变,合法
return 20;
}
static void sayHello() { // 这不是重写,是静态方法隐藏
System.out.println("Son hello");
}
}
多态的实现原理与动态绑定
多态在 Java 中有三个前提条件:存在继承关系、子类重写了父类方法、父类引用指向子类对象。满足这三点后,调用方法时 JVM 会根据对象的实际类型而非引用类型来决定执行哪个版本,这个机制叫动态绑定。看下面的例子:
public class Shape {
public double area() {
return 0;
}
}
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class Demo {
public static void main(String[] args) {
Shape s1 = new Circle(2.0); // 向上转型
Shape s2 = new Rectangle(3.0, 4.0);
System.out.println(s1.area()); // 输出圆的面积
System.out.println(s2.area()); // 输出矩形的面积
}
}
向上转型是自动完成的,体现了多态的核心价值:方法接收 Shape 参数,就能处理所有现在以及未来新增的子类,调用方代码无需任何修改。这种开闭原则的落地正是多态存在的意义。但要注意多态的边界,成员变量没有多态性,通过父类引用访问的字段取决于引用类型;private 方法、static 方法和 final 方法也不参与动态绑定,它们在编译期就静态解析了。
向下转型则需要显式书写,并且必须在真实类型匹配的前提下进行,否则运行时会抛出 ClassCastException。稳妥的做法是先用 instanceof 判断:
Shape s = new Circle(2.0);
if (s instanceof Circle) {
Circle c = (Circle) s; // 安全的向下转型
System.out.println("半徑处理逻辑执行");
}
JDK 14 以后 instanceof 还支持模式匹配写法 if (s instanceof Circle c),判断和转换一步完成,代码更简洁。理解了动态绑定的边界和转型的安全规则,再面对接口回调、框架扩展点设计这类场景时,就能准确预判方法调用的实际走向,写出真正灵活可扩展的 Java 程序。