Java从环境到类与对象 · SSH 实操

第四单元 · 类、对象、封装、综合 · 第 12 / 12 节

综合:数组版学生管理模型

用 Student[] + int count 保存多个学生对象,用 switch 菜单串起添加、列表、按学号查找、修改、删除,处理找不到/数组已满/删除前移等边界。

约 40 分钟 · 7 个步骤 · 进度只存本机

这一节把前两节的 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 类,类名和文件名完全一致。

SSH 服务器终端Java 源码程序交互输入输出示例(只读)复制 Java 源码不带文件名;复制命令不带 $ 提示符;输出和输入示例没有复制按钮。
这是教程网页,不是在线运行环境。Java 代码和 SSH 命令都要在你的服务器上执行;网页不会执行代码、不会连接服务器、也不会接收密码。

已完成 0 / 7 步

第 1 步:写 Student.java,给模型加学号

在上一节的基础上增加 id(学号)。学号一旦创建就不应该改,所以只提供 getId(),不提供 setId();姓名和年龄仍可通过 setter 修改,年龄仍走 0 到 120 的校验。

SSH 服务器终端bash
$ 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 都按这三个动作保存退出,后面不再重复提醒。

Java 源码Student.java
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 辅助方法。

SSH 服务器终端bash
$ if [ -e StudentManager.java ]; then mv StudentManager.java "StudentManager.java.bak.$(date +%s)"; fi
$ nano StudentManager.java
Java 源码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 个学生,列出全部,退出。

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java StudentManager.java
$ java StudentManager

交互输入(示例,只读)。在终端里一行一行输入;自动验收时这些行就是固定 stdin:

交互输入(只读)input不可复制
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
2
0
输出示例(只读)output不可复制
===== 学生管理 =====
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。

SSH 服务器终端bash
$ java StudentManager
交互输入(只读)input不可复制
1
2026001
张三
20
1
2026002
李四
21
3
2026001
3
9999999
4
2026002
22
0
输出示例(只读)output不可复制
===== 学生管理 =====
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 的张三条,再列出全部;最后再删一次同一个学号,确认“找不到”分支。

SSH 服务器终端bash
$ java StudentManager
交互输入(只读)input不可复制
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
5
2026001
2
5
2026001
0
输出示例(只读)output不可复制
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:3
请选择:
请输入要删除的学号:
删除成功,当前人数:2
请选择:
学号:2026002,姓名:李四,年龄:21
学号:2026003,姓名:王五,年龄:19
请选择:
请输入要删除的学号:
没有找到学号为 2026001 的学生。
请选择:
程序结束。

删除不是把后面的对象“搬走”,而是把后面的引用依次往前复制一格:

输出示例(只读)text不可复制
删除前: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 步:边界场景:空数组和数组已满

先看“一个学生都没有”时列出全部:

SSH 服务器终端bash
$ java StudentManager
交互输入(只读)input不可复制
2
0
输出示例(只读)output不可复制
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
还没有学生记录。
请选择:
程序结束。

count 是 0,listStudents 一进来就走 if (count == 0) 分支打印提示并 return,不会去访问 students[0]。空数组不是错误,是一个必须正常处理的边界。

再看容量 3 已满时继续添加:

SSH 服务器终端bash
$ java StudentManager
交互输入(只读)input不可复制
1
2026001
张三
20
1
2026002
李四
21
1
2026003
王五
19
1
0
输出示例(只读)output不可复制
===== 学生管理 =====
1. 添加学生
2. 列出全部
3. 按学号查找
4. 修改年龄
5. 按学号删除
0. 退出
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:1
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:2
请选择:
请输入学号:
请输入姓名:
请输入年龄:
添加成功,当前人数:3
请选择:
数组已满,不能再添加。
请选择:
程序结束。

注意:addStudent 在读取学号、姓名、年龄之前就判断 count == students.length。满的时候直接打印并 return count,不会再向输入要三个数据。这个顺序很重要:如果先读数据再判满,满的时候会多消耗三行输入,后面的菜单选择就会错位。

本节的容量固定为 3,是为了让“满”这个边界很快出现。真实项目会用更大的容量或可变容器,但那超出本阶段;这里先把固定数组 + count 的边界处理清楚。

第 7 步:空位是 null,为什么遍历必须用 count

写一个只观察数组、不调用对象方法的检查文件,看看 new Student[3] 刚创建时三个格子是什么:

SSH 服务器终端bash
$ if [ -e CountCheck.java ]; then mv CountCheck.java "CountCheck.java.bak.$(date +%s)"; fi
$ nano CountCheck.java
Java 源码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]);
    }
}

编译运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java CountCheck.java
$ java CountCheck
输出示例(只读)output不可复制
数组容量:3
当前人数:0
第 1 个格子:null

new Student[3] 只做了“3 个可以放引用的格子”,没有 new Student(...),所以格子里是 null。如果你在 count 还是 0 的时候写:

输出示例(只读)text不可复制
for (int i = 0; i < students.length; i++) {
    students[i].printInfo();
}

循环会从 0 走到 2,第一次 students[0] 就是 null,再调用 printInfo() 就会触发:

输出示例(只读)text不可复制
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。

练习

  1. 写 AnswerLesson12.java:创建一个容量为 3 的 Student[],只放两个学生(2026001 张三 20、2026002 李四 21),count 设为 2;调用 StudentManager.listStudents(students, count) 打印列表;再调用 StudentManager.findIndex(...) 分别查找 2026002 和 9999999,打印返回值;最后打印 students[2]。先预测输出,再运行。
  2. 如果把 listStudents 的循环改成 i < students.length,在上面这个“只放 2 个”的数组上运行,会发生什么?说明是数组越界还是空引用。

先自己写,再展开对答案。答案文件叫 AnswerLesson12.java,不会覆盖你的 StudentManager.java。

提示 先只放两个对象

第三个格子故意留空,才能看到 students[2] 是 null。listStudents 只接收 count,不会自己去猜数组里哪些格子有对象;先想清楚 count 是 2 时循环会走到哪个下标。

参考答案 对答案
Java 源码AnswerLesson12.java
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 也要一起编译,因为答案调用了它的静态方法):

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java StudentManager.java AnswerLesson12.java
$ java AnswerLesson12
输出示例(只读)output不可复制
数组容量: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。

说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。