控制台版学生成绩管理系统,本质上是把一组学生对象放入内存集合中,通过命令行菜单完成增删改查和排序。这个项目对Java初学者非常友好:没有数据库连接,没有Web框架,只需要运用类与对象、Scanner输入、ArrayList动态数组、流程控制语句以及Comparator排序,就能形成一个可以反复运行的小工具。本文会从数据模型设计开始,逐步拆解菜单循环、成绩操作和输入容错,最后给出一些可扩展的思路。

一、先定义Student类,把成绩数据封装起来
做管理系统之前,第一步不是写菜单,而是想清楚要存储什么数据。一个学生至少包含学号、姓名、数学成绩、英语成绩和Java成绩。学号适合用String而不是int,因为学号可能存在前导零,而且String在后续比较和查找时更灵活。成绩统一使用double,可以兼容小数分,也为将来计算平均分、加权总分留下空间。
把这些字段放进Student类中,通过构造方法初始化,通过getter和setter访问。getTotal方法用来计算三科总分,toString方法方便在控制台直接打印对象信息。这个类不包含任何业务逻辑,只负责数据的承载和简单计算,符合Java面向对象中单一职责的基本思路。下面是一个可用的Student类,注意setter方法在修改成绩功能中会被调用。
public class Student {
private String id;
private String name;
private double math;
private double english;
private double javaScore;
public Student(String id, String name, double math, double english, double javaScore) {
this.id = id;
this.name = name;
this.math = math;
this.english = english;
this.javaScore = javaScore;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public double getMath() {
return math;
}
public double getEnglish() {
return english;
}
public double getJavaScore() {
return javaScore;
}
public void setName(String name) {
this.name = name;
}
public void setMath(double math) {
this.math = math;
}
public void setEnglish(double english) {
this.english = english;
}
public void setJavaScore(double javaScore) {
this.javaScore = javaScore;
}
public double getTotal() {
return math + english + javaScore;
}
@Override
public String toString() {
return "学号:" + id + ",姓名:" + name + ",数学:" + math +
",英语:" + english + ",Java:" + javaScore + ",总分:" + getTotal();
}
}
与普通数组相比,ArrayList的优势在于容量可以动态变化。学生数量在运行前并不确定,如果用String[]或Student[],要么一开始就指定一个可能浪费的固定长度,要么在添加时频繁复制数组。ArrayList在内部封装了扩容机制,开发者只需要调用add、remove、get、size、sort等方法,代码更简洁,也更贴近真实业务中的集合操作。
二、用while循环和switch搭建菜单主体
控制台程序的核心交互是菜单循环。程序启动后打印可选项,用户输入数字后进入对应分支,执行完再回到菜单,直到用户选择退出。Java里可以用while(true)配合switch实现。Scanner负责从标准输入读取字符串,菜单选择也使用nextLine读取,避免nextInt留下换行符导致后续读取被跳过的问题。
主类StudentManager中维护一个静态的ArrayList<Student>作为全局数据源,同时维护一个静态Scanner对象。静态成员可以让多个方法直接访问,不需要传递参数。菜单方法printMenu只负责输出提示,main方法负责分流。这样做的好处是每个功能方法只处理自己的逻辑,结构清晰。下面给出主类和菜单循环的骨架,后续的功能方法会逐步补充进这个类。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Scanner;
public class StudentManager {
private static ArrayList<Student> students = new ArrayList<>();
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
while (true) {
printMenu();
String choice = scanner.nextLine();
switch (choice) {
case "1":
addStudent();
break;
case "2":
listStudents();
break;
case "3":
updateStudent();
break;
case "4":
deleteStudent();
break;
case "5":
sortByTotal();
break;
case "6":
System.out.println("程序已退出");
return;
default:
System.out.println("请输入1到6之间的数字");
}
}
}
private 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("6. 退出");
System.out.print("请选择操作:");
}
}
需要注意的是,菜单循环本身不会自动处理异常。如果用户输入了非数字,switch会直接进入default分支,不会导致崩溃;但成绩读取时如果用户输入abc,就可能在Double.parseDouble处抛出NumberFormatException。所以成绩输入必须单独做容错,这一点会在下一节展开。
三、实现添加、查看、修改、删除和排序
添加学生时,先检查学号是否已经存在。因为ArrayList允许重复元素,但学号在业务上应该是唯一标识。遍历students集合,比较getId,如果相同就提示添加失败。成绩输入通过一个readDouble方法循环读取,直到用户输入合法的数字为止。这样即使误输入字母,程序也不会中断,而是提示重新输入。
private static void addStudent() {
System.out.print("请输入学号:");
String id = scanner.nextLine();
for (Student s : students) {
if (s.getId().equals(id)) {
System.out.println("该学号已存在,添加失败");
return;
}
}
System.out.print("请输入姓名:");
String name = scanner.nextLine();
System.out.print("请输入数学成绩:");
double math = readDouble();
System.out.print("请输入英语成绩:");
double english = readDouble();
System.out.print("请输入Java成绩:");
double javaScore = readDouble();
students.add(new Student(id, name, math, english, javaScore));
System.out.println("添加成功");
}
private static double readDouble() {
while (true) {
try {
return Double.parseDouble(scanner.nextLine());
} catch (NumberFormatException e) {
System.out.print("成绩必须是数字,请重新输入:");
}
}
}
查看全部学生非常简单,先判断students.isEmpty,空列表时直接提示,避免输出空行。随后使用增强for循环打印每个Student对象,此时会自动调用Student的toString方法。修改成绩则需要先根据学号找到索引,再让用户逐项输入新值。为了降低操作成本,这里采用回车保持原值的策略,只在实际输入非空字符串时才更新姓名,只在实际输入数字时才更新对应成绩。findIndexById返回-1表示未找到,这样删除和修改都可以复用同一个查找方法。
private static void listStudents() {
if (students.isEmpty()) {
System.out.println("当前没有学生数据");
return;
}
for (Student s : students) {
System.out.println(s);
}
}
private static void updateStudent() {
System.out.print("请输入要修改的学生学号:");
String id = scanner.nextLine();
int index = findIndexById(id);
if (index == -1) {
System.out.println("未找到该学生");
return;
}
Student s = students.get(index);
System.out.println("当前信息:" + s);
System.out.print("请输入新的姓名(回车保持原值):");
String name = scanner.nextLine();
if (!name.trim().isEmpty()) {
s.setName(name);
}
s.setMath(readUpdatedScore("数学", s.getMath()));
s.setEnglish(readUpdatedScore("英语", s.getEnglish()));
s.setJavaScore(readUpdatedScore("Java", s.getJavaScore()));
System.out.println("修改成功");
}
private static double readUpdatedScore(String subject, double oldScore) {
while (true) {
System.out.print("请输入新的" + subject + "成绩(回车保持" + oldScore + "):");
String input = scanner.nextLine();
if (input.trim().isEmpty()) {
return oldScore;
}
try {
return Double.parseDouble(input);
} catch (NumberFormatException e) {
System.out.println("成绩格式不正确,请重新输入");
}
}
}
private static int findIndexById(String id) {
for (int i = 0; i < students.size(); i++) {
if (students.get(i).getId().equals(id)) {
return i;
}
}
return -1;
}
删除操作是ArrayList最直接的remove(int index)调用。因为remove会将被删除元素后面的所有元素向前移动一位,所以如果使用遍历删除,必须注意索引变化。这里先查找索引,再删除,不会出现漏删或越界。删除前可以打印被删除对象的信息,便于确认操作结果。
private static void deleteStudent() {
System.out.print("请输入要删除的学生学号:");
String id = scanner.nextLine();
int index = findIndexById(id);
if (index == -1) {
System.out.println("未找到该学生");
return;
}
Student removed = students.remove(index);
System.out.println("已删除:" + removed.getName());
}
按总分排序使用List接口提供的sort方法,传入一个Comparator实现。匿名内部类中compare方法返回Double.compare(s2.getTotal(), s1.getTotal()),注意参数顺序是s2在前、s1在后,这样得到从高到低的降序排列。如果希望总成绩相同时按学号升序,可以在compare中追加一个二级判断。排序完成后调用listStudents展示结果。
private static void sortByTotal() {
students.sort(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
if (Double.compare(s2.getTotal(), s1.getTotal()) != 0) {
return Double.compare(s2.getTotal(), s1.getTotal());
}
return s1.getId().compareTo(s2.getId());
}
});
System.out.println("已按总分从高到低排序");
listStudents();
}
这五个功能合起来已经构成一个可运行的控制台程序。将Student类和StudentManager类放在同一目录下,先编译Student.java再编译StudentManager.java,或者直接使用IDE运行,即可在命令行中体验完整的流程。建议读者不要只看代码,而是亲手输入并故意制造几次错误输入,观察程序是否能继续运行,这样对异常处理和边界条件的理解会更深刻。
四、常见错误与演进方向
初学者在这个项目中容易遇到三类问题。第一类是Scanner混用nextInt和nextLine,导致读取菜单选项后多出一个空行,后续的姓名或学号被直接跳过。统一使用nextLine并手动parse可以避免这个坑。第二类是成绩输入未做异常捕获,用户输入非数字后程序直接抛异常退出。第三类是删除时使用for循环正向遍历,边遍历边删除,容易跳过连续满足条件的元素。本文先用findIndexById定位再删除,规避了这一问题。
从项目演进角度看,当学生数量变大时,使用ArrayList按学号查找的时间复杂度是O(n)。如果希望更快,可以引入HashMap<String, Student>,以学号为键、学生对象为值,查找接近O(1)。但HashMap本身不保证顺序,如果需要按添加顺序展示,还要配合LinkedHashMap或用ArrayList保存顺序。另一个方向是持久化,当前数据在程序退出后就丢失。可以把学生信息写入文本文件、CSV或使用Serializable序列化,也可以接入SQLite、MySQL等数据库,让系统成为真正的数据管理工具。
如果继续用控制台版本练习,还可以增加按单科成绩统计最高分、最低分和平均分,或者加入学分权重计算加权总分。这些功能不需要改变现有结构,只需在StudentManager中增加方法。通过这个项目把类、集合、异常和接口串起来,比单纯记忆语法要有效得多。