背景与动机
封装、继承、多态是 Java 面向对象编程的核心。
它们分别解决不同问题:
- 封装:对象内部状态如何被保护。
- 继承:已有类型能力如何复用和扩展。
- 多态:同一调用如何在运行时表现出不同行为。
核心原理拆解
1. 封装:隐藏字段,暴露受控方法
class User { private int age;
public void setAge(int age) { if (age < 0) { throw new IllegalArgumentException("age must be >= 0"); } this.age = age; }
public int getAge() { return age; }}字段设为 private 后,外部不能随意修改,只能通过公开方法进入对象内部规则。
2. 继承:使用 extends 扩展父类
class Animal { String name;
Animal(String name) { this.name = name; }
void eat() { System.out.println(name + " is eating"); }}
class Dog extends Animal { Dog(String name) { super(name); }
void bark() { System.out.println(name + " is barking"); }}Dog 继承 Animal 后,可以直接复用父类已经定义好的字段和方法,同时再补充自己独有的行为。
可以把继承先理解成两件事:
- 代码层面:子类拿到父类可继承的成员,减少重复定义。
- 建模层面:子类和父类之间通常要满足明确的“是一个”关系,比如
Dog是一种Animal。
继承最常见的价值,不只是“少写几行代码”,而是把公共状态和公共行为收拢到父类里,让一组相似对象共享同一套抽象。
2.1 子类构造方法会先走父类初始化
class Animal { Animal(String name) { System.out.println("Animal constructor: " + name); }}
class Dog extends Animal { Dog(String name) { super(name); System.out.println("Dog constructor: " + name); }}当创建 new Dog("Buddy") 时,会先执行父类构造方法,再执行子类构造方法。
这是因为子类对象内部其实包含了父类那一部分状态。父类部分没有初始化完成,子类对象也就不能算完整创建。
结果
Animal constructor: BuddyDog constructor: Buddy2.2 super 用来访问父类实现
class Animal { Animal(String name) { System.out.println("Animal constructor: " + name); } void eat() { System.out.println("animal eat"); }}
class Dog extends Animal { Dog(String name) { super(name); System.out.println("Dog constructor: " + name); } @Override void eat() { super.eat(); System.out.println("dog eat more bones"); }}super.eat() 表示先调用父类版本,再补充子类自己的逻辑。它常用于:
- 调用父类构造方法
super(...) - 调用被重写的父类普通方法
super.method() - 在字段同名时访问父类字段
super.name
public class Main { public static void main(String[] args) { Dog time = new Dog("Buddy"); time.eat();
}}结果
Animal constructor: BuddyDog constructor: Buddyanimal eatdog eat more bones2.3 继承不是所有成员都能随便拿来用
子类可以继承父类的大部分可访问成员,但有几个边界要记住:
private成员属于父类内部细节,子类不能直接访问。- 构造方法不会被继承,子类只能通过
super(...)调用父类构造方法。 final类不能被继承,final方法不能被重写。
所以继承并不等于“父类全部内容原封不动复制给子类”,而是在访问控制规则下复用可继承的部分。
2.4 final 常见用法要分开看
final 不是只有一种意思,要看它修饰什么:
- 修饰变量:值不能再重新赋值
- 修饰方法:子类不能重写这个方法
- 修饰类:这个类不能被继承
final class Config { final String env;
Config(String env) { this.env = env; }}final class:从类型层面直接封住继承链final method:类能被继承,但这个具体方法不能被子类改写
而 final 修饰变量时,关注点则不是继承,而是“这个引用或值能不能再次赋值”。
3. 方法重写:子类改变父类行为
class Dog extends Animal { @Override void eat() { System.out.println("dog eat"); }}@Override 能让编译器检查是否真的重写了父类方法,建议保留。
重写的关键点是:方法名相同、参数列表相同,真正变化的是实现逻辑。
子类重写父类方法,通常是因为“父类给出了通用定义,但具体子类需要自己的执行方式”。比如:
Animal.eat()只是一个通用动作Dog.eat()、Cat.eat()则可以表现出各自差异
重写时还要注意几个基本规则:
- 子类方法访问权限不能比父类更严格。
- 返回值类型要兼容父类方法声明。
static方法谈不上典型的运行时重写,它更接近方法隐藏。
初学阶段最需要记住的一条是:看到父类和子类里出现“同签名方法”,优先想到“这里可能是为了多态而设计的重写点”。
4. 多态:父类引用指向子类对象
Animal animal = new Dog("Buddy");animal.eat();变量类型是 Animal,实际对象是 Dog。调用被重写的方法时,会执行子类版本,这就是动态绑定。
多态的重点不是“语法写法变了”,而是“调用方只依赖抽象类型,但运行结果由真实对象决定”。
这样做的好处是,调用方可以统一处理一组父类对象,而不用为每个具体子类各写一套逻辑。

4.1 向上转型是多态最常见入口
Animal a1 = new Dog("Buddy");Animal a2 = new Cat("Mimi");把子类对象赋给父类引用,叫向上转型。这个过程通常是自动完成的,不需要显式强转。
向上转型后有两个关键现象:
- 能调用哪些方法,看左边引用的编译期类型。
- 真正执行哪个重写版本,看右边对象的运行时类型。
这两句话是理解多态时最容易混但也最核心的地方。
4.2 多态最典型的价值,是统一处理不同子类
class Animal { void eat() { System.out.println("animal eat"); }}
class Dog extends Animal { @Override void eat() { System.out.println("dog eat bones"); }}
class Cat extends Animal { @Override void eat() { System.out.println("cat eat fish"); }}
public class Demo { static void feed(Animal animal) { animal.eat(); }}这里 feed(Animal animal) 不关心传进来的是 Dog 还是 Cat,但最终执行结果会跟着真实对象走。这比写大量 if-else 判断类型更符合面向对象思路。
4.3 字段不参与典型的运行时多态
方法调用会发生动态绑定,但字段访问看的是引用类型,不是对象真实类型。比如:
class Animal { String type = "animal";}
class Dog extends Animal { String type = "dog";}
Animal animal = new Dog();System.out.println(animal.type);输出是 animal,不是 dog。
这说明 Java 里最典型的多态,主要体现在实例方法重写上,而不是字段访问上。

4.4 向下转型要谨慎
Animal animal = new Dog("Buddy");
if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.bark();}父类引用想调用子类特有方法时,需要向下转型。但向下转型有前提:真实对象本来就得是那个子类,否则会抛出 ClassCastException。
所以能不向下转型时就尽量少转,必须转时先做类型判断。

最小可运行代码示例
public class OopDemo { public static void main(String[] args) { Animal dog = new Dog("Buddy"); Animal cat = new Cat("Mimi");
feed(dog); feed(cat); }
static void feed(Animal animal) { animal.eat(); }}
class Animal { String name;
Animal(String name) { this.name = name; }
void eat() { System.out.println(name + " eats food"); }}
class Dog extends Animal { Dog(String name) { super(name); }
@Override void eat() { System.out.println(name + " eats bones"); }
void bark() { System.out.println(name + " barks"); }}
class Cat extends Animal { Cat(String name) { super(name); }
@Override void eat() { System.out.println(name + " eats fish"); }}输出:
Buddy eats bonesMimi eats fish常见陷阱与错误示例
1. 把继承当成代码复用的默认方案
继承会形成强耦合,父类变化可能影响所有子类。只有存在清晰的“是一个”关系时,才优先考虑继承。
2. 误以为子类能直接访问父类 private 字段
class Animal { private int age = 3;}
class Dog extends Animal { void printAge() { // System.out.println(age); // 编译失败 }}private 是“对当前类私有”,不是“对子类开放”。如果子类确实需要读取,通常应该通过父类公开方法暴露。
3. 忘记 @Override
如果方法名或参数写错,没有 @Override 时可能变成新方法而不是重写。加上 @Override 可以让编译器及时报错。
4. 父类引用不能直接调用子类特有方法
Animal animal = new Dog("Buddy");animal.bark();如果 bark 只定义在 Dog 中,这段代码无法通过编译。变量的编译期类型决定能调用哪些方法。
5. 把字段访问误认为多态
很多初学者看到“父类引用指向子类对象”,就会以为字段和方法都按子类版本执行。实际不是这样:
- 实例方法重写,走运行时多态。
- 字段访问,走编译期类型。
这一点如果没分清,后面读框架代码时会很容易误判。
6. 不判断类型就向下转型
Animal animal = new Cat("Mimi");Dog dog = (Dog) animal;这段代码可以编译,但运行时会抛出 ClassCastException。强制类型转换能不能成功,不看“你想转成什么”,而看对象真实类型是不是那个子类。
面试高频问题
1. 什么是封装
封装是把对象内部状态隐藏起来,通过公开方法提供受控访问,从而保护对象不被随意改坏。
2. 重载和重写有什么区别
重载发生在同一个类中,方法名相同、参数列表不同;重写发生在父子类之间,子类重新实现父类方法。
3. 多态的前提是什么
通常需要继承或接口、方法重写、父类或接口引用指向子类对象。
4. 为什么说多态提高了扩展性
因为调用方依赖的是父类或接口,不需要写死具体实现。新增一个子类时,只要遵守同一套抽象契约,原有调用代码通常不用改。
5. super 和 this 有什么区别
this 代表当前对象,super 代表父类那部分实现。this(...) 用于调用本类其他构造方法,super(...) 用于调用父类构造方法。
6. 为什么构造方法不能被继承
构造方法的职责是创建并初始化“当前类对象”。父类构造方法只能被子类通过 super(...) 调用,用来完成父类部分初始化,但它本身不属于可被重写和复用的普通成员方法。
7. 字段有没有多态
通常说的 Java 多态,主要是实例方法的运行时分派。字段访问不参与这种动态绑定,它看的是引用的编译期类型。
一句话总结
封装控制访问边界,继承组织“共同部分和差异部分”,多态则让调用方依赖父类抽象、把具体行为交给运行时真实对象决定。