Java 封装、继承与多态

笔记/Java/Java基础/面向对象编程 (OOP)/Java 封装、继承与多态

背景与动机#

封装、继承、多态是 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: Buddy
Dog constructor: Buddy

2.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: Buddy
Dog constructor: Buddy
animal eat
dog eat more bones

2.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。调用被重写的方法时,会执行子类版本,这就是动态绑定。

多态的重点不是“语法写法变了”,而是“调用方只依赖抽象类型,但运行结果由真实对象决定”。

这样做的好处是,调用方可以统一处理一组父类对象,而不用为每个具体子类各写一套逻辑。

img
img

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 里最典型的多态,主要体现在实例方法重写上,而不是字段访问上。

img
img

4.4 向下转型要谨慎#

Animal animal = new Dog("Buddy");
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark();
}

父类引用想调用子类特有方法时,需要向下转型。但向下转型有前提:真实对象本来就得是那个子类,否则会抛出 ClassCastException

所以能不向下转型时就尽量少转,必须转时先做类型判断。

img
img

最小可运行代码示例#

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 bones
Mimi 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. superthis 有什么区别#

this 代表当前对象,super 代表父类那部分实现。this(...) 用于调用本类其他构造方法,super(...) 用于调用父类构造方法。

6. 为什么构造方法不能被继承#

构造方法的职责是创建并初始化“当前类对象”。父类构造方法只能被子类通过 super(...) 调用,用来完成父类部分初始化,但它本身不属于可被重写和复用的普通成员方法。

7. 字段有没有多态#

通常说的 Java 多态,主要是实例方法的运行时分派。字段访问不参与这种动态绑定,它看的是引用的编译期类型。

一句话总结#

封装控制访问边界,继承组织“共同部分和差异部分”,多态则让调用方依赖父类抽象、把具体行为交给运行时真实对象决定。

文章目录

文章目录