第四单元 · 类、对象、封装、综合 · 第 12 / 12 节
综合:数组版学生管理模型
用 Student[] + int count 保存多个学生对象,用 switch 菜单串起添加、列表、按学号查找、修改、删除,处理找不到/数组已满/删除前移等边界。
这一节把前两节的 Student 类放进数组,做一个能在终端里增、查、改、删的小模型。它不是数据库:所有数据只存在内存里,程序退出就没了;本节也不连接 MySQL、不读写文件。
核心结构只有两样:
Student[] students = new Student[3];:一个容量为 3 的数组,每个格子放一个Student对象的引用。int count = 0;:当前真正放了多少个学生。count同时是“下一个空位的下标”。
数组的 length 是容量,创建后固定;count 是实际人数,可以增加、减少。这两个数必须分清:遍历时用 count,判满时用 students.length。
说明 先说明 null 从哪里来
new Student[3] 只创建了 3 个“放引用的格子”,并没有创建 3 个 Student 对象。三个格子一开始都是 null,表示“这个格子还没有指向任何对象”。只有执行 students[0] = new Student(...) 之后,第 1 个格子才指向一个真实对象。后面第 7 步会专门看这件事;现在先记住:空格子不能直接调用对象方法。
本节写两个文件:Student.java(数据模型)和 StudentManager.java(入口 + 管理方法)。仍然是一个文件一个 public 类,类名和文件名完全一致。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 7 步
第 1 步:写 Student.java,给模型加学号
在上一节的基础上增加 id(学号)。学号一旦创建就不应该改,所以只提供 getId(),不提供 setId();姓名和年龄仍可通过 setter 修改,年龄仍走 0 到 120 的校验。
$ source ~/java-practice/env.sh
$ mkdir -p ~/java-practice/lesson12
$ cd ~/java-practice/lesson12
$ if [ -e Student.java ]; then mv Student.java "Student.java.bak.$(date +%s)"; fi
$ nano Student.java
nano 打开后粘贴代码,按 Ctrl+O,回车确认文件名,再按 Ctrl+X 退出。本节每次 nano 都按这三个动作保存退出,后面不再重复提醒。
public class Student {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
setAge(age);
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
if (age >= 0 && age <= 120) {
this.age = age;
} else {
System.out.println("年龄不合法,未修改:" + age);
}
}
public void printInfo() {
System.out.println("学号:" + id + ",姓名:" + name + ",年龄:" + age);
}
}
这里没有无参构造方法,因为管理器创建学生时一定会拿到学号、姓名、年龄三项数据,直接用带参构造更合适。id 没有 setter,是故意的:学号是身份标识,创建后不允许改;要改就删掉重新添加。
printInfo() 把一条学生记录打印成一行,后面列表功能直接调用它,避免在管理器里重复拼接字符串。
第 2 步:写 StudentManager.java,数组 + count + 菜单
这个文件比较长,但结构很清楚:main 里准备数组和 count,用 while 反复显示“请选择”,用 switch 分到六个分支;每个分支调用一个 static 辅助方法。
$ if [ -e StudentManager.java ]; then mv StudentManager.java "StudentManager.java.bak.$(date +%s)"; fi
$ nano StudentManager.java
import java.util.Scanner;
public class StudentManager {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
Student[] students = new Student[3];
int count = 0;
boolean running = true;
printMenu();
while (running) {
System.out.println("请选择:");
int choice = sc.nextInt();
switch (choice) {
case 1:
count = addStudent(sc, students, count);
break;
case 2:
listStudents(students, count);
break;
case 3:
findStudent(sc, students, count);
break;
case 4:
updateAge(sc, students, count);
break;
case 5:
count = deleteStudent(sc, students, count);
break;
case 0:
running = false;
break;
default:
System.out.println("没有这个选项,请重新输入。");
}
}
System.out.println("程序结束。");
}
public static void printMenu() {
System.out.println("===== 学生管理 =====");
System.out.println("1. 添加学生");
System.out.println("2. 列出全部");
System.out.println("3. 按学号查找");
System.out.println("4. 修改年龄");
System.out.println("5. 按学号删除");
System.out.println("0. 退出");
}
public static int addStudent(Scanner sc, Student[] students, int count) {
if (count == students.length) {
System.out.println("数组已满,不能再添加。");
return count;
}
System.out.println("请输入学号:");
int id = sc.nextInt();
System.out.println("请输入姓名:");
String name = sc.next();
System.out.println("请输入年龄:");
int age = sc.nextInt();
students[count] = new Student(id, name, age);
count++;
System.out.println("添加成功,当前人数:" + count);
return count;
}
public static void listStudents(Student[] students, int count) {
if (count == 0) {
System.out.println("还没有学生记录。");
return;
}
for (int i = 0; i < count; i++) {
students[i].printInfo();
}
}
public static int findIndex(Student[] students, int count, int id) {
for (int i = 0; i < count; i++) {
if (students[i].getId() == id) {
return i;
}
}
return -1;
}
public static void findStudent(Scanner sc, Student[] students, int count) {
System.out.println("请输入要查找的学号:");
int id = sc.nextInt();
int index = findIndex(students, count, id);
if (index == -1) {
System.out.println("没有找到学号为 " + id + " 的学生。");
} else {
students[index].printInfo();
}
}
public static void updateAge(Scanner sc, Student[] students, int count) {
System.out.println("请输入要修改的学号:");
int id = sc.nextInt();
int index = findIndex(students, count, id);
if (index == -1) {
System.out.println("没有找到学号为 " + id + " 的学生。");
return;
}
System.out.println("请输入新的年龄:");
int age = sc.nextInt();
students[index].setAge(age);
System.out.println("修改后的记录:");
students[index].printInfo();
}
public static int deleteStudent(Scanner sc, Student[] students, int count) {
System.out.println("请输入要删除的学号:");
int id = sc.nextInt();
int index = findIndex(students, count, id);
if (index == -1) {
System.out.println("没有找到学号为 " + id + " 的学生。");
return count;
}
for (int i = index; i < count - 1; i++) {
students[i] = students[i + 1];
}
students[count - 1] = null;
count--;
System.out.println("删除成功,当前人数:" + count);
return count;
}
}
几个关键点:
Student[] students = new Student[3];的容量是 3,不能自动变长;满了就拒绝添加,这是本节的明确边界。count从 0 开始,添加一个就加 1;students[count]正好是“下一个空位”。所以添加时先判断满不满,再往students[count]放。switch的每个case末尾都有break,否则会继续执行下一个分支。default处理 1–5、0 以外的输入。addStudent、deleteStudent都返回新的count,main里写count = addStudent(...)。int是按值传递的,方法里改局部count不会自动改到main的count,所以要把新值返回出来。listStudents循环条件是i < count,不是i < students.length。为什么,第 7 步会用实际输出说明。
第 3 步:编译,添加 3 个学生并列出
两个文件一起编译,然后按下面的固定输入操作:添加 3 个学生,列出全部,退出。
$ javac -encoding UTF-8 Student.java StudentManager.java
$ java StudentManager
交互输入(示例,只读)。在终端里一行一行输入;自动验收时这些行就是固定 stdin:
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
2
0
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:3
请选择:
学号:2026001,姓名:张三,年龄:20
学号:2026002,姓名:李四,年龄:21
学号:2026003,姓名:王五,年龄:19
请选择:
程序结束。
三次添加后 count 是 3,数组三个格子都指向真实对象;列出时循环 i < 3,正好打印三条。printInfo() 一行一条,学号、姓名、年龄都在。
输入块要按行给全。少给一行,程序会在下一次 sc.next() / sc.nextInt() 等待输入;在终端里看起来像“卡住”,在固定 stdin 下则会因为读不到而报错。按上面的输入块从头补齐,不要只补一半。
第 4 步:按学号查找,再修改年龄
先添加两个学生,然后查一个存在的学号、一个不存在的学号,最后把李四的年龄改成 22。
$ java StudentManager
1
2026001
张三
20
1
2026002
李四
21
3
2026001
3
9999999
4
2026002
22
0
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入要查找的学号:
学号:2026001,姓名:张三,年龄:20
请选择:
请输入要查找的学号:
没有找到学号为 9999999 的学生。
请选择:
请输入要修改的学号:
请输入新的年龄:
修改后的记录:
学号:2026002,姓名:李四,年龄:22
请选择:
程序结束。
findIndex 从头到尾找 getId() == id,找到就返回下标,找不到返回 -1。查找和修改都先调用它:返回 -1 就打印“没有找到”,直接 return,不再往下读年龄;找到就通过 students[index] 操作那个具体对象。
修改年龄调用的是 students[index].setAge(age),所以 lesson11 的 0–120 校验仍然生效。试试输入 200,你会看到“年龄不合法,未修改:200”,记录保持原年龄。
第 5 步:删除一个学生,观察后面的记录前移
添加 3 个学生,删除学号 2026001 的张三条,再列出全部;最后再删一次同一个学号,确认“找不到”分支。
$ java StudentManager
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
5
2026001
2
5
2026001
0
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:3
请选择:
请输入要删除的学号:
删除成功,当前人数:2
请选择:
学号:2026002,姓名:李四,年龄:21
学号:2026003,姓名:王五,年龄:19
请选择:
请输入要删除的学号:
没有找到学号为 2026001 的学生。
请选择:
程序结束。
删除不是把后面的对象“搬走”,而是把后面的引用依次往前复制一格:
删除前:students[0]=张三 students[1]=李四 students[2]=王五 count=3
删除后:students[0]=李四 students[1]=王五 students[2]=null count=2
循环 for (int i = index; i < count - 1; i++) 从删除位置开始,把 students[i + 1] 赋给 students[i]。循环结束后,最后一个格子被复制了两次,所以显式写 students[count - 1] = null; 清掉多余引用,再把 count--。这样列表只打印前 count 个,已经删除的记录不会再出现。
第 6 步:边界场景:空数组和数组已满
先看“一个学生都没有”时列出全部:
$ java StudentManager
2
0
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
还没有学生记录。
请选择:
程序结束。
count 是 0,listStudents 一进来就走 if (count == 0) 分支打印提示并 return,不会去访问 students[0]。空数组不是错误,是一个必须正常处理的边界。
再看容量 3 已满时继续添加:
$ java StudentManager
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
1
0
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:3
请选择:
数组已满,不能再添加。
请选择:
程序结束。
注意:addStudent 在读取学号、姓名、年龄之前就判断 count == students.length。满的时候直接打印并 return count,不会再向输入要三个数据。这个顺序很重要:如果先读数据再判满,满的时候会多消耗三行输入,后面的菜单选择就会错位。
本节的容量固定为 3,是为了让“满”这个边界很快出现。真实项目会用更大的容量或可变容器,但那超出本阶段;这里先把固定数组 + count 的边界处理清楚。
第 7 步:空位是 null,为什么遍历必须用 count
写一个只观察数组、不调用对象方法的检查文件,看看 new Student[3] 刚创建时三个格子是什么:
$ if [ -e CountCheck.java ]; then mv CountCheck.java "CountCheck.java.bak.$(date +%s)"; fi
$ nano CountCheck.java
public class CountCheck {
public static void main(String[] args) {
Student[] students = new Student[3];
int count = 0;
System.out.println("数组容量:" + students.length);
System.out.println("当前人数:" + count);
System.out.println("第 1 个格子:" + students[0]);
}
}
编译运行:
$ javac -encoding UTF-8 Student.java CountCheck.java
$ java CountCheck
数组容量:3
当前人数:0
第 1 个格子:null
new Student[3] 只做了“3 个可以放引用的格子”,没有 new Student(...),所以格子里是 null。如果你在 count 还是 0 的时候写:
for (int i = 0; i < students.length; i++) {
students[i].printInfo();
}
循环会从 0 走到 2,第一次 students[0] 就是 null,再调用 printInfo() 就会触发:
Exception in thread "main" java.lang.NullPointerException
这不是数组下标越界(下标 0 对长度 3 的数组是合法的),而是“格子里没有对象,却让对象做事”。NullPointerException 的来源就是 null 引用被拿来访问字段或调用方法。
所以本节的遍历一律写 i < count:count 是多少,就只访问前多少个已经放了对象的格子。删除时把最后一个格子清成 null、count--,也是同样的道理:让 count 之外的位置明确地“没有对象”,不再被列表访问。
注意 NPE 和越界不是一回事
ArrayIndexOutOfBoundsException 是下标超出 0..length-1;NullPointerException 是下标合法但格子里是 null。看到 NPE 先找“这个引用最后一次 new 或赋值在哪里”,不要先去改循环边界。
常见错误
- **
NullPointerException出现在students[i].printInfo()**:循环用了i < students.length,访问到还没放对象的空位。改成i < count;删除后把最后一个格子设为null。 - **
ArrayIndexOutOfBoundsException**:students[count] = ...之前没有判断count == students.length,或者删除后count没有减。先判满再写入,删除后先移动再count--。 - 添加后列表少一条/多一条:
count没有随添加增加,或删除后没有减少。检查addStudent是否count++并返回,main是否count = addStudent(...)。 - 删除后列表出现重复的最后一条:只做了前移,没有把
students[count - 1]清成null,或者count没减。前移、清尾、减一,三步都要有。 - 输入错位、程序读到奇怪的学号:某个分支多读或少读了 stdin。比如数组已满时仍去读学号/姓名/年龄,就会把下一次菜单选择吃掉。先判边界,再读数据。
- **
cannot find symbol ... Student**:Student.java没有和StudentManager.java一起编译。用javac -encoding UTF-8 Student.java StudentManager.java。
练习
- 写
AnswerLesson12.java:创建一个容量为 3 的Student[],只放两个学生(2026001 张三 20、2026002 李四 21),count设为 2;调用StudentManager.listStudents(students, count)打印列表;再调用StudentManager.findIndex(...)分别查找 2026002 和 9999999,打印返回值;最后打印students[2]。先预测输出,再运行。 - 如果把
listStudents的循环改成i < students.length,在上面这个“只放 2 个”的数组上运行,会发生什么?说明是数组越界还是空引用。
先自己写,再展开对答案。答案文件叫 AnswerLesson12.java,不会覆盖你的 StudentManager.java。
提示 先只放两个对象
第三个格子故意留空,才能看到 students[2] 是 null。listStudents 只接收 count,不会自己去猜数组里哪些格子有对象;先想清楚 count 是 2 时循环会走到哪个下标。
参考答案 对答案
public class AnswerLesson12 {
public static void main(String[] args) {
Student[] students = new Student[3];
int count = 0;
students[count] = new Student(2026001, "张三", 20);
count++;
students[count] = new Student(2026002, "李四", 21);
count++;
System.out.println("数组容量:" + students.length);
System.out.println("当前人数:" + count);
StudentManager.listStudents(students, count);
System.out.println("查找 2026002 的下标:" + StudentManager.findIndex(students, count, 2026002));
System.out.println("查找 9999999 的下标:" + StudentManager.findIndex(students, count, 9999999));
System.out.println("第 3 个格子:" + students[2]);
}
}
编译运行(StudentManager.java 也要一起编译,因为答案调用了它的静态方法):
$ javac -encoding UTF-8 Student.java StudentManager.java AnswerLesson12.java
$ java AnswerLesson12
数组容量:3
当前人数:2
学号:2026001,姓名:张三,年龄:20
学号:2026002,姓名:李四,年龄:21
查找 2026002 的下标:1
查找 9999999 的下标:-1
第 3 个格子:null
第二题:students[2] 是 null,循环走到 i = 2 时调用 students[2].printInfo() 会抛 NullPointerException。下标 2 对长度 3 的数组没有越界,问题是这个格子里还没有对象;所以列表必须用 i < count,而不是 i < students.length。
做到这里
做到这里就是本阶段的终点。你已经会:
- 用
Student[]保存多个对象,用int count表示真实人数; - 区分数组容量
length和实际人数count,遍历用count; - 用
switch菜单把添加、列表、查找、修改、删除串起来; - 处理数组已满、找不到、空数组、删除前移、空位
null这些边界; - 知道数据只在内存里,程序退出就消失,本节没有数据库、文件或网络存储。
继承、多态、接口、集合(ArrayList 等)、异常处理、JDBC/Maven/Spring 都不在本阶段;本页不提供这些内容的空链接,也不假装它们已经上线。
来源 来源与改编说明(非官方)
主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P092–P095 的对象数组综合训练主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。