前言
这一篇进入面向对象最核心、也最容易懵的部分:多态,顺便把对象数组一起讲了,因为多态和数组经常配合出现。期待和你的一起进步
一、对象数组的声明和初始化
1.1 什么是对象数组?
对象数组就是"装对象的数组"。普通数组装的是int、double这种基本类型,对象数组装的是对象的引用(地址)。
1.2 声明方式
java
// 方式一:先声明,再分配空间 Student[] students = new Student[3]; // 方式二:声明 + 初始化 Student[] students = new Student[]{s1, s2, s3}; // 方式三:简写 Student[] students = {s1, s2, s3};1.3 代码示例
java
public class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public void show() { System.out.println("姓名:" + name + ",年龄:" + age); } }java
public class Test { public static void main(String[] args) { // 声明一个长度为 3 的对象数组 Student[] students = new Student[3]; // 分别给每个位置赋值 students[0] = new Student("张三", 18); students[1] = new Student("李四", 19); students[2] = new Student("王五", 20); // 使用 students[0].show(); // 姓名:张三,年龄:18 students[1].show(); // 姓名:李四,年龄:19 students[2].show(); // 姓名:王五,年龄:20 } }1.4 我的理解
对象数组就像一排快递柜。new Student[3]就是造了 3 个空柜子,柜子里暂时啥都没有(null)。然后students[0] = new Student(...)就是往第一个柜子里放一个包裹。
注意:造柜子 ≠ 放包裹。new Student[3]只是造了 3 个空柜子,里面的包裹还得你自己一个个放。
核心一句话:对象数组装的是对象的地址,不是对象本身。
二、对象数组的遍历和基本使用
2.1 普通 for 循环
java
for (int i = 0; i < students.length; i++) { students[i].show(); }2.2 增强 for 循环
java
for (Student stu : students) { stu.show(); }2.3 两种遍历的区别
| 对比项 | 普通 for | 增强 for |
|---|---|---|
| 能否拿到下标 | ✅ 能 | ❌ 不能 |
| 能否修改数组元素 | ✅ 能 | ❌ 不能(只能读) |
| 代码简洁度 | 一般 | 简洁 |
| 适用场景 | 需要下标时 | 只读遍历时 |
2.4 代码示例
java
public class Test { public static void main(String[] args) { Student[] students = { new Student("张三", 18), new Student("李四", 19), new Student("王五", 20) }; // 普通 for for (int i = 0; i < students.length; i++) { System.out.println("第 " + (i + 1) + " 个学生:"); students[i].show(); } // 增强 for System.out.println("--- 增强 for ---"); for (Student stu : students) { stu.show(); } } }2.5 我的理解
遍历对象数组就是把每个柜子打开,拿出里面的包裹用一用。
增强 for 写起来简洁,但拿不到下标。如果你想"把第 2 个学生换成李四",那就得用普通 for,因为你需要students[1] = ...这种下标操作。
核心一句话:要下标用普通 for,只读遍历用增强 for。
三、对象数组的内存结构
3.1 内存图
java
Student[] students = new Student[3]; students[0] = new Student("张三", 18); students[1] = new Student("李四", 19); students[2] = new Student("王五", 20);内存结构:
text
栈内存 堆内存 +-----------+ +---------------------+ | students | ---> | Student[] 数组 | +-----------+ +---------------------+ | [0] | 地址A | ----+ | [1] | 地址B | --+ | | [2] | 地址C | + | | +---------------------+ | | | | +------------------------+ | | Student 对象(张三) | | name = "张三" | | age = 18 | +-------------------------+ +------------------------+ | Student 对象(李四) | | name = "李四" | | age = 19 | +------------------------+
3.2 关键点
students是栈里的引用,指向堆里的数组数组本身在堆里,每个元素存的都是对象的地址
对象实体也在堆里,和数组是分开的
数组里存的不是对象,是对象的地址
3.3 我的理解
这个内存图画几遍就记住了。可以这么想:
students是你手里的遥控器(栈)Student[]数组是一排快递柜(堆)柜子里放的不是包裹,是包裹的取件码(地址)
真正的包裹(对象)在另一个地方(堆里的另一块区域)
所以你通过students[0]拿到取件码,再根据取件码找到真正的包裹。
核心一句话:数组在堆里,对象也在堆里,数组元素存的是对象的地址。
四、对象数组中的空指针异常陷阱
4.1 什么是空指针异常?
NullPointerException(NPE)是 Java 最常见的异常之一。当你用一个null去调用方法或访问属性时,就会抛出它。
4.2 陷阱示例
java
Student[] students = new Student[3]; // 此时 students[0]、students[1]、students[2] 都是 null students[0].show(); // ❌ NullPointerException!
原因:new Student[3]只造了 3 个空柜子,柜子里没有包裹。你直接对null调用show(),当然报错。
4.3 正确写法
java
Student[] students = new Student[3]; students[0] = new Student("张三", 18); // 先放包裹 students[0].show(); // ✅ 再使用4.4 遍历时的判空
java
for (Student stu : students) { if (stu != null) { // 判空,防止 NPE stu.show(); } }4.5 我的理解
这个坑很容易踩。new Student[3]之后直接遍历调用方法,程序直接崩了,还纳闷"我不是创建数组了吗"。
后来才明白:创建数组 ≠ 创建对象。数组只是柜子,柜子里是空的。你得先往柜子里放包裹,才能用包裹。
核心一句话:对象数组创建后元素默认是 null,使用前必须判空。
五、多态——向上转型
5.1 什么是向上转型?
父类引用指向子类对象,叫向上转型。
java
父类 变量名 = new 子类();
5.2 代码示例
java
public class Animal { public void eat() { System.out.println("动物在吃东西"); } } public class Dog extends Animal { @Override public void eat() { System.out.println("狗在啃骨头"); } public void bark() { System.out.println("狗在汪汪叫"); } }java
Animal animal = new Dog(); // 向上转型 animal.eat(); // 输出:狗在啃骨头 // animal.bark(); // ❌ 编译报错!父类引用不能调用子类特有方法
5.3 向上转型的特点
自动发生,不需要强转
父类引用只能调用父类中定义的方法
如果子类重写了该方法,调用的是子类重写后的版本
子类特有方法不能调用
5.4 我的理解
向上转型就是"把狗当动物看"。你牵一条狗,跟别人说"这是我的动物",因为狗本来就是动物。
但你说"这是我的动物,它会汪汪叫",别人就懵了——因为"动物"这个身份里没有"汪汪叫"这个技能。你得说"这是我的狗",才能让它汪汪叫。
核心一句话:向上转型 = 父类引用指向子类对象,只能调用父类定义的方法。
六、多态——动态绑定
6.1 什么是动态绑定?
运行时才确定调用哪个方法,叫动态绑定(也叫运行时多态)。
6.2 代码示例
java
public class Test { public static void main(String[] args) { Animal animal1 = new Dog(); Animal animal2 = new Cat(); animal1.eat(); // 输出:狗在啃骨头 animal2.eat(); // 输出:猫在吃鱼 } }编译时:编译器只知道animal1是Animal类型,检查Animal里有没有eat(),有,通过。
运行时:JVM 发现animal1实际指向的是Dog对象,于是调用Dog重写的eat()。
6.3 动态绑定的前提
有继承关系
子类重写了父类方法
父类引用指向子类对象
三个条件缺一不可。
6.4 我的理解
动态绑定就是"看实际对象,不看声明类型"。
你写Animal animal = new Dog(),编译器看左边(Animal),知道你有eat()方法,放行。但真正执行时,JVM 看右边(Dog),发现是条狗,就调用狗版本的eat()。
核心一句话:编译看左边,运行看右边。
七、多态——向下转型
7.1 什么是向下转型?
把父类引用转回子类引用,叫向下转型。需要强制类型转换。
java
子类 变量名 = (子类) 父类引用;
7.2 代码示例
java
Animal animal = new Dog(); // 向上转型 Dog dog = (Dog) animal; // 向下转型 dog.bark(); // ✅ 现在可以调用子类特有方法了
7.3 向下转型的风险
如果转错了类型,会抛ClassCastException:
java
Animal animal = new Dog(); Cat cat = (Cat) animal; // ❌ ClassCastException!
原因:animal实际是Dog对象,你硬要把它当Cat,JVM 不干。
7.4 为什么要向下转型?
因为向上转型后,子类特有方法不能调用。如果你确实需要调用子类特有方法,就得转回去。
java
Animal animal = new Dog(); // animal.bark(); // ❌ 报错 Dog dog = (Dog) animal; dog.bark(); // ✅ 可以了
7.5 我的理解
向上转型是"把狗当动物看",向下转型是"把动物当狗看"。
问题是:你手里这个"动物",它到底是不是狗?如果是狗,转回狗没问题;如果其实是猫,你硬转成狗,那就出事了。
所以向下转型之前,必须先判断类型,这就引出了instanceof。
核心一句话:向下转型 = 父类引用转回子类,必须强制转换,转错会抛异常。
八、instanceof 关键字
8.1 什么是 instanceof?
instanceof用来判断一个对象是不是某个类的实例,返回boolean。
java
对象 instanceof 类名
8.2 代码示例
java
Animal animal = new Dog(); System.out.println(animal instanceof Dog); // true System.out.println(animal instanceof Cat); // false System.out.println(animal instanceof Animal); // true(狗也是动物) System.out.println(animal instanceof Object); // true(所有类都是 Object 子类)
8.3 配合向下转型使用
java
public void doSomething(Animal animal) { if (animal instanceof Dog) { Dog dog = (Dog) animal; dog.bark(); } else if (animal instanceof Cat) { Cat cat = (Cat) animal; cat.meow(); } }8.4 我的理解
instanceof就是"你是不是这个东西?"。
向下转型前先问一句:"你是不是狗?"是的话再转,不是就别转,这样就不会出ClassCastException。
核心一句话:instanceof 判断类型,向下转型前先用它防翻车。
九、核心口诀
| 知识点 | 口诀 |
|---|---|
| 对象数组 | 造柜子 ≠ 放包裹,元素默认 null |
| 对象数组内存 | 数组在堆,对象在堆,数组存地址 |
| 空指针陷阱 | 创建数组 ≠ 创建对象,用前必判空 |
| 向上转型 | 父类引用指向子类对象,只能调父类方法 |
| 动态绑定 | 编译看左边,运行看右边 |
| 向下转型 | 父转回子,必须强转,转错抛异常 |
| instanceof | 判断类型,向下转型前先用它防翻车 |
十、常见易错点
| 易错点 | 说明 |
|---|---|
| 对象数组元素忘赋值 | new Student[3]后元素都是 null,直接用会 NPE |
| 向上转型后调子类方法 | 编译报错,父类引用不能调子类特有方法 |
| 向下转型不判断类型 | 直接强转可能抛 ClassCastException,先用 instanceof |
| 混淆编译和运行 | 编译看左边类型,运行看右边对象 |
| 静态方法当多态 | 静态方法看左边类型,不参与动态绑定 |
| 数组 length 忘写 | 数组用length属性,不是length()方法 |