尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用ArrayList手写学生管理系统:Java集合实战与踩坑全记录

用ArrayList手写学生管理系统:Java集合实战与踩坑全记录 先直接给结论这个项目是Java初学者阶段最值得认真写完的一个练手项目它不依赖框架、不依赖数据库只用Java自带的核心类就能把增删改查跑通。我见过太多人学完集合框架之后只会背API真让自己写一个学生管理系统就卡在“从哪开始”。这篇就把我用ArrayList从零搭学生管理系统完整的思路、代码和踩坑过程全部拆开讲清楚。这个系统适合两类人刚学完Java基础语法想找项目巩固的以及准备Java面试想快速过一遍集合核心知识点的。前者能通过它把类、对象、封装、集合这些点串起来后者能从这个项目里挖出一堆面试喜欢问的底层细节。1. 为什么用ArrayList搭学生管理系统选型背后的真实考量先回答一个很多人没细想过的问题网上学生管理系统那么多有用数组的有用HashMap的有用文件的为什么我推荐用ArrayList作为第一次实现的选择最大的原因是ArrayList最贴近真实编程场景的“增删改查”语义。数组在Java里是一个静态结构长度一旦确定就不能改了。学生管理系统里学生数量是动态变化的今天插一个明天删一个如果用数组你就要自己写扩容逻辑新数组长度加一挨个复制旧数组元素再把新元素塞进去。这一套操作写下来既繁琐又容易下标越界。ArrayList把这段逻辑封装好了本质就是“一个会自动扩容的数组”你只管调用add和remove底层帮你处理容量和下标迁移。ArrayList的遍历顺序和插入顺序一致这一点和“学生名单”这个业务场景天然匹配。学生管理系统展示名单时通常默认就是按照录入的先后顺序排列ArrrayList保持插入顺序刚好不用额外做排序。如果改用HashMap遍历顺序是不可预测的你还得额外维护一个列表来记录顺序属于自己给自己加麻烦。第三个原因是调试成本低。系统里涉及索引的增删查操作ArrayList的indexOf、get(index)这些方法语义非常直白。你写代码时脑子里能随时“模拟执行”出集合里的数据在哪个位置。这对新手建立起直观的集合模型特别重要。当然也不是没有缺点。ArrayList的增删操作如果发生在中间位置需要把后面的元素整体向前或向后移动时间复杂度是O(n)。学生管理系统的数据量级撑死几百上千条这个开销完全感知不到。如果你的数据量到了几十万条且频繁在头部插删那再考虑LinkedList这是后话。学生管理系统用ArrayList还有一个隐藏优势它在市面上所有Java教程里出场率最高出问题后你搜解决方案、查源码、问AI都能得到大量可参考的资料。选型有时候也要考虑“生态”越常见的技术方案你在踩坑时得到的帮助越多。2. 系统设计与核心数据结构Student类与集合的搭配2.1 学生对象封装不要用散装变量存数据写管理系统第一步不是new集合而是先定义“学生”这个数据模型。很多新手喜欢拆开搞一个ArrayList存姓名一个ArrayList存学号一个ArrayList存成绩三个列表的下标对应对应同一个学生。这种设计在数据量小的时候看着能用但一旦你要做“删除某个学生”或“按学号查找”的操作就会陷入“删了一个列表的下标其他列表也要同步删”的泥潭三个列表数据一旦错位就是你拿学号查到了别人的成绩。正确做法是定义一个Student类把学生的所有属性封装到一个对象里集合里存的就是一个个Student对象而不是散装的字符串和数字。public class Student { private String id; // 学号用String而不是int因为学号可能含有字母或前导零 private String name; // 姓名 private int age; // 年龄 private double score; // 成绩 public Student(String id, String name, int age, double score) { this.id id; this.name name; this.age age; this.score score; } // 省略getter、setter日常开发用IDE自动生成没必要手敲 }这里有个细节值得说学号字段推荐用String而不是int。如果学号是001、002这种格式用int存会自动把前导零去掉打印出来就变成1、2看着别扭。还有一些学校的学号里带字母或者横杠比如“CS2024001”这种只能用String。虽然学生管理系统是练习项目但字段类型的选择本身就是一个训练点别因为偷懒全用String糊弄像年龄、成绩这种需要计算的字段必须用数值类型。2.2 集合维度的选择是存一个集合还是多个集合学生管理系统的核心数据就是学生的集合但往深一层想它还可以有“集合的维度”这个设计方案。我当时第一次写的时候犹豫过要不要用多个ArrayList分班存比如一个班一个列表或者大一一个列表、大二一个列表。后来我想明白了这种分级需求不适合用多个ArrayList“硬分”更适合用一个列表加一个“班级字段”来做维度区分。假如你有200个学生分布在5个班用5个ArrayList就得写5段几乎一样的遍历逻辑来查人一旦要迁移学生从A班转到B班你得先从A列表remove再往B列表add还得保证两个列表里的对象是同一份。这些代码又臭又容易出bug。直接在Student类里加一个grade或className字段一张表把所有学生装完查询的时候遍历加if条件就行逻辑清晰也容易扩展。当然如果你纯粹是为了训练“多个集合如何配合实现业务”那用两个集合分别存“学生信息”和“选课记录”也是一个不错的练习方向但那属于扩展项目了第一版系统不需要搞这么复杂。2.3 泛型约束为什么必须写定义集合变量的时候必须带泛型ArrayListStudent students new ArrayList();如果你是写ArrayList students new ArrayList()这种不带泛型的旧写法编译器会给你一个警告。它不报错但隐患很大这个集合里可以塞进任何类型的东西学生的List里混入了一个字符串遍历时你强制转型成Student就会抛ClassCastException。这种错误在编译期完全看不出来只有运行到那行代码才会炸排查起来费时费力。带泛型编译器帮你挡住了大部分类型错误这也是从大一统的容器思想过渡到类型安全编程的一个重要台阶。3. 核心功能实现与关键代码解析增删改查的完整闭环学生管理系统的核心功能就四个添加学生、删除学生、修改学生信息、查询学生信息。我按一个完整业务流程走下来讲。3.1 添加学生先查重再落库添加学生看起来就是一行students.add(student)的事但真实业务里有一个关键校验学号不能重复。学号是学生的唯一标识如果系统里已经存在学号“1001”的学生你又加一个学号“1001”的后续按学号查询时就会同时查到两条记录逻辑直接乱掉。public boolean addStudent(Student s) { for (Student existing : students) { if (existing.getId().equals(s.getId())) { return false; // 学号已存在添加失败 } } students.add(s); return true; }这里用equals而不是是一个高频考点。比较的是引用地址两个不同对象的Id即使字符串内容一模一样引用地址也不同用判断永远不相等。equals比较的是内容字符串内容相同就返回true。这个小细节在管理系统里不处理好查重功能就是摆设。3.2 删除学生按学号定位后再删删除的难点不在于remove这个动作而在于定位到要删的那个人。学生管理系统里用户输入的通常是学号你拿到学号后需要在集合里遍历查找。public boolean deleteStudent(String id) { int index findIndexById(id); if (index -1) { return false; } students.remove(index); return true; } private int findIndexById(String id) { for (int i 0; i students.size(); i) { if (students.get(i).getId().equals(id)) { return i; } } return -1; }我特意把查找索引抽成了一个独立方法而不是直接在remove的遍历中操作。这样做的好处是同样的查找逻辑可以被“删除”和“修改”反复复用代码不会出现两段几乎一样的遍历。凡是同一个逻辑出现两次以上就应该考虑抽方法这是让代码可维护的基本功。这里还有一个ArrayList使用层面的细节remove存在两个重载方法remove(int index)和remove(Object o)。如果你写了students.remove(id)这里的id是String类型编译器会匹配哪个答案是remove(Object o)它会把字符串id当成一个元素去删除而不是删除“id等于这个值的学生”。除非集合里本来存的是String否则这个调用不会有任何效果。写代码时一定要显式先算index再remove(index)不要想当然。3.3 修改学生信息查出来直接改别想着换对象修改的逻辑是用户传入学号和新的姓名/年龄/成绩系统找到那个学生把属性更新成新值。public boolean updateStudent(String id, String newName, int newAge, double newScore) { int index findIndexById(id); if (index -1) { return false; } Student s students.get(index); s.setName(newName); s.setAge(newAge); s.setScore(newScore); return true; }这里有一个新手容易绕晕的点为什么我拿到s这个对象改了属性集合里的数据就变了因为集合里存的是对象的引用也就是“存放地址”。students.get(index)返回的是这个地址你通过这个地址找到内存里那块Student区域然后写入新数据。你手上的s和集合里的其实是同一块内存区域改一个就相当于改另一个。另一种常见的错误写法是students.set(index, new Student(id, newName, newAge, newScore))。这个写法也能达到修改效果但它多创建了一个新对象并且要求你手动把原来的id又传一遍。从内存角度和逻辑角度都不如拿原对象改属性来得干净。只有在你需要整体替换一个对象比如老对象被引用到别的地方不能污染的时候才用set方法。3.4 查询学生精确查询与遍历展示查询功能分两类一类是按学号查单个学生逻辑就是上面findIndexById的变形找到后返回Student对象另一类是展示全部学生名单直接遍历集合打印。public Student findById(String id) { int index findIndexById(id); if (index -1) { return null; } return students.get(index); } public void printAllStudents() { if (students.isEmpty()) { System.out.println(当前系统还没有学生数据请先添加); return; } for (Student s : students) { System.out.println(学号 s.getId() 姓名 s.getName() 年龄 s.getAge() 成绩 s.getScore()); } }返回null表示找不到这个设计要配合调用方做非空判断。很多人在Controller层调用查询方法后不判空就直接findById(id).getName()一旦id不存在就是NullPointerException现场。这种问题在项目里排查半天核心原因就是“你太信任用户的输入了”。3.5 菜单循环主程序把操作串起来单独写一个方法管增、管删、管改、管查还不行系统要能循环接收用户指令。我用自增ID的方式模拟了完整入口。import java.util.Scanner; public class StudentManagerApp { public static void main(String[] args) { StudentManager manager new StudentManager(); Scanner scanner new Scanner(System.in); boolean running true; while (running) { 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. 退出系统); System.out.print(请选择操作); int choice scanner.nextInt(); scanner.nextLine(); // 吸收回车不然后续nextLine会读到空串 switch (choice) { case 1: System.out.print(请输入学号); String id scanner.nextLine(); System.out.print(请输入姓名); String name scanner.nextLine(); System.out.print(请输入年龄); int age scanner.nextInt(); scanner.nextLine(); System.out.print(请输入成绩); double score scanner.nextDouble(); scanner.nextLine(); boolean ok manager.addStudent(new Student(id, name, age, score)); System.out.println(ok ? 添加成功 : 添加失败学号已存在); break; case 2: System.out.print(请输入要删除的学号); String delId scanner.nextLine(); System.out.println(manager.deleteStudent(delId) ? 删除成功 : 删除失败学号不存在); break; case 3: // 类似修改逻辑 break; case 4: manager.printAllStudents(); break; case 5: // 查询单个学生逻辑 break; case 0: running false; System.out.println(系统已退出再见); break; default: System.out.println(无效输入请重新选择); } } scanner.close(); } }这段代码里有一个小坑必须提醒scanner.nextInt()之后不换行紧接着就scanner.nextLine()会先读到一个空字符串。所以在每个nextInt或nextDouble之后加上一行scanner.nextLine()把回车吸收掉这是一个很经典的控制台输入坑。你查一下“java scanner输入nextLine和nextInt失效”就能看到一堆人栽在这里原因就是下一个nextLine把前一次输入遗留的回车读走了。4. 常见问题与排查技巧实录4.1 ConcurrentModificationException遍历时删除的经典坑这是ArrayList使用里最常见、也最容易让新手摸不着头脑的异常。场景很典型你想把成绩低于60分的学生全部删掉于是写出了下面的代码for (Student s : students) { if (s.getScore() 60) { students.remove(s); // 运行时抛ConcurrentModificationException } }表面上看这个代码逻辑没问题遍历过程中发现一个不合格的就删一个。但ArrayList内部有一个modCount字段记录集合被结构性修改的次数add、remove都算。迭代器在创建时会保存一个expectedModCount每次next都会检查modCount和expectedModCount是否一致不一致就抛异常。你用students.remove(s)修改了集合的结构但迭代器并不知道它的expectedModCount还是旧值两边一对不上异常就抛出来了。正确的做法有三个第一种是用迭代器自身的remove方法IteratorStudent it students.iterator(); while (it.hasNext()) { Student s it.next(); if (s.getScore() 60) { it.remove(); } }it.remove()会把expectedModCount同步更新迭代器内部状态一致不会抛异常。第二种是倒着遍历for (int i students.size() - 1; i 0; i--) { if (students.get(i).getScore() 60) { students.remove(i); } }倒着删除不会影响还没遍历到的下标这个方案最简单也最好理解。第三种是收集后统一删除ArrayListStudent toRemove new ArrayList(); for (Student s : students) { if (s.getScore() 60) { toRemove.add(s); } } students.removeAll(toRemove);这个方案适合“删除条件比较复杂”的场景先把要删的收集起来最后一次性删干净。对了不止删除会触发ConcurrentModificationException只要在迭代过程中调用任何结构性修改方法add、remove、clear都可能炸。这是面试经常挂人的点。4.2 迭代删除后索引错位就算不用迭代器用普通for循环删除也存在另一个问题删除中间位置元素后后续元素自动前移原来的索引2变成了索引1而你的循环变量已经到2了相当于跳过一个学生没有检查。for (int i 0; i students.size(); i) { if (students.get(i).getScore() 60) { students.remove(i); } }比如集合里有两个不及格的学生索引分别是1和3。第一次循环i1删掉一个原来的索引3前移成了索引2但循环下一次i2直接跳过了这个前移过来的学生。最稳妥的做法就是在删除时让i--停留在一个位置再检查一次。这个细节处理不好删除功能的正确性堪忧。4.3 修改对象时没有同步到集合有一个容易混淆的场景用户通过查询拿到了一个Student对象修改了它但发现原集合数据没变。这种情况通常是因为你“复制”了一份而不是拿到了引用。比如你不小心写了Student s new Student()然后再往里set值这个新对象和集合里的对象没有任何关系你改一百遍也不会影响集合。另一种可能是你写了一个“拷贝构造方法”复制了一个新对象出来这就更要区分“引用”和“副本”的概念。修改功能要生效原则就是先去集合里找到那个对象在返回的对象上改属性。4.4 排序时直接Collections.sort报错学生管理系统做到后面很多人想加一个“按成绩排序”的功能于是直接写Collections.sort(students)结果编译报错或者运行报ClassCastException。原因是Student类没有实现Comparable接口Collections.sort的默认排序并不知道应该按什么规则比较两个Student对象。两种解决方式一种让Student类实现Comparable接口重写compareTo定义“默认排序规则”另一种调用Collections.sort(students, comparator)临时传入一个Comparator。学生管理系统的场景里“按照不同字段做不同排序”的需求是存在的所以用Comparator更灵活// 按成绩从高到低排序 Collections.sort(students, new ComparatorStudent() { Override public int compare(Student o1, Student o2) { return Double.compare(o2.getScore(), o1.getScore()); } });Java 8之后可以简写成Lambdastudents.sort((o1, o2) - Double.compare(o2.getScore(), o1.getScore()));这里有个隐藏知识点Double.compare是为了避免直接(int)(o1.getScore() - o2.getScore())造成的精度截断问题。成绩如果是80.5和80.4直接强转int结果是0排序就失灵了。凡是比较浮点数排序用compare方法准没错。5. 从ArrayList延伸到更广阔的学习路径这个项目能引出的进阶知识点5.1 从ArrayList到HashMap数据结构的第一次升级学生管理系统跑通之后你会发现一个经典痛点按学号查找一个人每次都要从头遍历整个集合数据量几十条的时候没感觉但如果要存储一万个学生查询一次最坏情况要比较一万次。这时候就引出HashMap了。HashMap用key直接定位value学号作为keyStudent对象作为value查找的效率在一个合理的散列设计下接近O(1)。你把ArrayListStudent换成HashMapString, Student管理系统依然能跑通但“按学号查学生”这个操作从遍历变成了直接get。这是让你第一次直观感受“不同数据结构解决不同问题”的好机会。我当时把这个系统改成HashMap版本之后回头再看ArrayList就能更清楚地理解两者的适用边界需要保持顺序用ArrayList需要按key快速定位用HashMap。5.2 从内存到文件数据持久化是管理系统的下一个台阶纯ArrayList的管理系统程序一关数据全没了。你再想起来这个系统所有学生数据都要重新录入一遍。想要让数据保留下来就要引入文件操作程序启动时从文件里读数据加载到ArrayList中每次增删改完后写回文件。这个升级看起来只是加了几行IO代码但它是从“临时用品”到“可用工具”的关键一步。更进一步可以用数据库。但这套练习的价值在于你在切换数据存储层的时候会发现业务逻辑代码不需要大改因为所有操作都基于Student对象和ArrayList的抽象。这就是“封装”的意义——数据访问方式变了业务代码纹丝不动。5.3 面试考点浓缩一个小项目串起Java基础半壁江山上面这些内容实际上就是Java面试高频问题的一个集合为什么ArrayList底层用数组扩容机制是什么初始容量10扩到1.5倍老数组复制到新数组ArrayList和LinkedList的区别数组vs双向链表随机访问vs插入删除ArrayList和Vector的区别线程安全vs非线程安全如果不用Vector单线程下用ArrayList为什么遍历时不能直接用集合的removefail-fast机制modCount校验equals和的区别引用比较vs内容比较可变参数、泛型的意义类型安全编译期检查一个看起来简单的学生管理系统把这些知识点全部暴露出来了。所以很多面试官喜欢让候选人手写一个“用集合实现一个简易学生信息管理模块”看着是在考代码能力实际上是在考你对集合底层机制的理解深度。5.4 后续还可以加的功能扩展如果学有余力我给几个明确的方向排序功能可以按照成绩排、按年龄排、按学号排分别练习Comparator的各种写法统计功能可以统计平均分、最高分、不及格人数搜索功能可以按姓名模糊匹配用contains方法练字符串查找在界面上可以改成Java Swing控制台版或者做成Web API但那些就对Java基础之外的框架有要求了适合下一步再啃。我个人实际体会是这个项目最大的价值不在于“它本身”有多厉害而在于它是一个承上启下的支点。你用它练完了类、对象、封装、集合、常用API未来学IO、学数据库、学框架的时候都会反过头来修改它、升级它、把它从80行改到800行。每一次重构都会让你对Java的理解更深一层。所以第一遍写的时候不用追求代码多精美能跑通、能理解每一行在干什么就已经完成任务了。先把这套“集合增删改查的思维模型”刻在脑子里面后面无论换什么技术栈核心逻辑都是一样的定义数据模型、往集合里放数据、从集合里取数据、把取出来的数据改好再放回去。就这样。
返回列表