登录
首页 >  文章 >  java教程

Java选课系统开发教程与实战解析

时间:2026-01-28 20:00:56 460浏览 收藏

在文章实战开发的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天golang学习网就整理分享《Java学生选课系统开发实战》,聊聊,希望可以帮助到正在努力赚钱的你。

应使用双向Map(学生→课程集、课程→学生集)建模多对多关系,值类型用Set并重写equals/hashCode;并发时用ConcurrentHashMap.newKeySet()或细粒度同步;删除需安全遍历清理;统计宜预计算或加锁保障一致性。

在Java里开发学生选课系统_Java集合关系实战说明

学生和课程之间该用什么集合建模

一对多关系是选课系统的核心,一个Student可选多门Course,一门Course也可被多个Student选择——这是典型的多对多。硬编码用两个ArrayList来回查效率低、易出错,必须借助集合关系结构。

推荐用 Map> 表达“学生→所选课程”,同时用 Map> 表达“课程→选课学生”。两者互补,缺一不可;只存单向会导致无法快速回答“这门课有哪些人选了”或“这个学生选了哪些课”。

  • HashMap 足够满足日常查询性能,除非有并发写入,否则不必上 ConcurrentHashMap
  • 值类型必须用 Set(如 HashSet),避免同一学生重复选同一门课
  • 务必重写 StudentCourseequals()hashCode(),否则 Map 查不到、Set 去不了重

选课操作中如何避免并发修改异常

多个线程同时调用 addCourseToStudent()removeStudentFromCourse() 时,若直接操作 HashSetArrayList,极大概率触发 ConcurrentModificationException。这不是数据错乱的前兆,而是 Java 集合的 fail-fast 机制在报错。

最轻量解法是加同步块,但粒度要细:只锁具体键对应的集合,而不是整个 Map

public void addCourseToStudent(Student student, Course course) {
    synchronized (studentToCourses.getOrDefault(student, Collections.emptySet())) {
        studentToCourses.computeIfAbsent(student, k -> ConcurrentHashMap.newKeySet()).add(course);
        courseToStudents.computeIfAbsent(course, k -> ConcurrentHashMap.newKeySet()).add(student);
    }
}
  • ConcurrentHashMap.newKeySet() 替代 HashSet,它返回的是线程安全的 Set
  • 不要对 studentToCourses 整个 Map 加锁,否则所有选课请求串行化,吞吐崩盘
  • 如果系统要求强一致性(比如防止超限选课),还需额外加业务锁或使用数据库事务,纯内存集合做不到

删除学生时怎样安全清理双向关联

删一个 Student 对象,不只是从 studentToCourses 里移除键,还必须遍历他选的所有 Course,从每个 courseToStudents 的对应 Set 中删掉该学生。漏掉任何一处,都会导致脏数据:查课程列表时看到已注销学生,或统计选课人数时虚高。

典型错误写法是边遍历边删 Set,引发 ConcurrentModificationException

// ❌ 危险:迭代中修改
for (Course c : studentToCourses.get(student)) {
    courseToStudents.get(c).remove(student); // 可能抛异常
}

正确做法是先收集、再清理:

public void removeStudent(Student student) {
    Set<course> courses = studentToCourses.remove(student);
    if (courses != null) {
        for (Course c : courses) {
            Set<student> students = courseToStudents.get(c);
            if (students != null) {
                students.remove(student);
                if (students.isEmpty()) {
                    courseToStudents.remove(c); // 空了就清键,省内存
                }
            }
        }
    }
}</student></course>
  • remove() 获取旧值,比先 get()remove() 更原子
  • courseToStudents.get(c) 可能返回 null,必须判空,否则 NPE
  • 删完记得检查 Set 是否为空,及时清理 courseToStudents 中的冗余键

用 Java 8 Stream 处理选课统计时的陷阱

想查“选了超过 5 门课的学生”,很容易写出这样的流式代码:

studentToCourses.entrySet().stream()
    .filter(e -> e.getValue().size() > 5)
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

看起来干净,但要注意:如果 studentToCoursesConcurrentHashMap,它的 entrySet() 不保证快照一致性。并发修改下,size() 可能和实际元素数不一致,导致漏掉或误判。

  • 生产环境做统计类查询,优先用 compute 系列方法维护预计算字段(如给 StudentcourseCount 字段)
  • 若必须用 Stream,且需强一致性,应先对整个 Map 加读锁(如用 ReentrantReadWriteLock.readLock()),或转为不可变副本:new HashMap(studentToCourses)
  • Collectors.groupingBy() 做“每门课的选课人数”时,注意 courseToStudents.get(c) 可能为 null,得用 getOrDefault(c, Collections.emptySet()).size()
真正难的不是写对一次选课,而是让所有关联操作在任意并发节奏、任意删除顺序、任意统计视角下都不留缝隙。集合关系一旦松动,数据就会像没拧紧的水管接头,开始滴漏。

今天关于《Java选课系统开发教程与实战解析》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于的内容请关注golang学习网公众号!

前往漫画官网入口并下载 ➜
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>