ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java零基础实战:手写学生信息管理系统,吃透数组与面向对象

Java零基础实战:手写学生信息管理系统,吃透数组与面向对象 很多人学Java学到面向对象就开始迷糊数组和集合还没分清ArrayList和HashMap更是背了又忘。然后去刷面试题背八股文越学越空。我带了这么多年学生每次遇到这种状态给的建议都一样别急着往后赶进度先动手做一个完整的小系统。学生信息管理系统就是我反复推荐的项目没有之一。它足够简单不需要任何框架和数据库一个类、一个数组、几个方法就能跑起来又足够完整增删改查、面向对象、流程控制、输入输出全都能练到。这篇文章我会从零开始带你把这个系统一点一点写出来每一步都解释为什么这么做连环境变量都帮你配好最后再告诉你普通版本和进阶版本之间差了哪些东西。收藏这一篇就够用但更重要的是跟着敲一遍。1. 为什么第一个项目应该做学生信息管理系统1.1 这个项目到底在练什么很多入门教程会把知识点拆得特别碎今天讲变量明天讲循环后天讲数组。学的时候觉得都懂了合上书却什么也写不出来。问题就出在没有一个载体把这些知识串起来。学生信息管理系统恰好是那个最合适的载体。一个标准的学生信息管理系统至少要包含这些功能功能对应知识点难度添加学生对象创建、数组存储、方法调用基础查看所有学生遍历数组、循环输出基础按学号查找条件判断、break语句入门修改学生信息数组索引定位、重新赋值中等删除学生数组元素前移、size边界控制中等偏难主菜单交互Scanner输入、switch分支、while循环基础看见没有平时学的那些零散语法全部被这一个项目串起来了。而且它的规模刚好卡在一个临界点上代码量在200到300行左右一个下午能写完不至于中途放弃逻辑又足够让你体会到“设计”是怎么回事不是无脑敲代码。1.2 为什么控制台版本比带界面的版本更适合入门我见过不少初学者一上来就搜“Java学生管理系统带界面”然后用Swing拖了几个文本框感觉挺像回事其实核心逻辑根本没碰。等面试官问一句“你删除学生的时候数组是怎么处理的”就答不上来了。我的建议是第一遍必须做纯控制台版本。控制台版本强制你把所有注意力放在业务逻辑上——怎么存数据、怎么找数据、怎么删数据。界面是锦上添花的东西不是雪中送炭。等你能把一个纯控制台的学生管理系统讲得清清楚楚再考虑加界面、加数据库那是完全不同的层次。2. 开发环境准备三步搞定JDK和IDEA2.1 JDK安装与环境变量配置Window系统为例这一步卡住过大量零基础同学。配环境变量的本质就是告诉操作系统“java这个命令在哪个文件夹里”。你下载完JDK双击安装默认路径一般是C:\Program Files\Java\jdk-17。接下来做三件事新建系统变量JAVA_HOME值填你的JDK安装路径。编辑Path变量新增一行%JAVA_HOME%\bin。打开命令行窗口输入java -version能看到版本号就成功了。提示%JAVA_HOME%\bin这一行一定要加在Path变量的最前面避免和系统自带的旧版Java冲突。很多人配完还是提示“java不是内部或外部命令”九成是这一步没做对。有个细节容易被忽略配置完环境变量后必须重新打开命令行窗口才生效。如果你在一个旧的命令行窗口里敲java -version还是报错不是配置错了是窗口没刷新。2.2 IDEA安装与第一个项目创建IDE我这里推荐IntelliJ IDEA。社区版对学习完全够用不需要破解。安装好之后创建项目的路径是File - New - Project语言选Java构建工具可以不选直接选IntelliJ原生就行JDK选你刚装好的版本没有的话点Add JDK手动指定路径项目创建出来之后在src目录上右键新建Java类随便写一句System.out.println(hello);运行一下。能打印出来说明整个链路已经通了。接下来所有代码都在这个项目里写。顺便说一句好多教程一上来就让你搞Maven、搞Spring Boot然后建出来的项目一堆文件你根本分不清哪个是干嘛的。零基础阶段老老实实用最朴素的Java项目就够了把精力全部留给逻辑本身。3. 开始写代码前的设计从需求到类3.1 面向对象的建模Student类的设计很多人学面向对象时最困惑的一件事情类和对象到底什么关系其实特别简单。类是模板对象是拿模板造出来的具体东西。你说“学生”的时候它是一个抽象概念没法操作但你说“学号为2024001、名叫张三、考了90分的那个人”你就可以操作他了。前者是类后者是对象。在这个系统里Student类只需要四个字段public class Student { private String id; // 学号 private String name; // 姓名 private int age; // 年龄 private double score; // 成绩 public Student() { } public Student(String id, String name, int age, double score) { this.id id; this.name name; this.age age; this.score score; } public String getId() { return id; } public void setId(String id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } public int getAge() { return age; } public void setAge(int age) { this.age age; } public double getScore() { return score; } public void setScore(double score) { this.score score; } public String toString() { return 学号 id 姓名 name 年龄 age 成绩 score; } }字段为什么要用private私有这叫封装。外面的人不能直接改你的数据必须通过你提供的方法。好处是什么你可以在setAge里加逻辑public void setAge(int age) { if (age 0 || age 150) { System.out.println(年龄不合法); return; } this.age age; }这就保证了数据不会被随随便便赋值成离谱的值。这种设计思想在你将来写任何系统时都用得上。构造函数里搞一个无参构造和一个全参构造是JavaBean的规范写法现在你只要记住“最好两个都写”就行后面学习框架时会经常和这种类打交道。3.2 数据存储方案为什么先用数组而不是数据库很多零基础同学一听说管理信息系统第一反应是“是不是要装MySQL”我明确告诉你这个阶段不要碰数据库。原因有两点。第一数据库的引入会牵扯出JDBC、驱动、SQL语法一大堆新概念你的注意力会被严重分散。第二数据库在物理上是一个独立的东西它和“Java代码如何操作数据”这件事是解耦的。零基础阶段你应该先把“在内存里操作数据”练熟再过渡到“在磁盘上操作数据”。所以这个版本我们就用数组。但这里要留个心眼数组长度一旦确定就不可变。往里面不断加学生总有满的一天。所以光定义一个数组还不够还要有一个变量size记录当前实际存了多少个学生。public class StudentManager { private Student[] students new Student[10]; private int size 0; }students数组用来存放学生对象size表示已存入的学生数量。注意数组长度是10但size才表示真实数量。为什么这么设计因为数组里下标0到size-1的位置才算有效数据size之后的都是“空位子”等着被填充。这是整个系统最关键的一个设计。你后面写的所有方法本质都是在维护这个数组和size之间的关系。很多人写崩了就是没搞清“数组长度”和“实际存储数量”是两个不同的东西。4. 核心功能实现从菜单到增删改查4.1 主菜单与程序入口让系统先跑起来任何一个系统先别急着写细节先把骨架搭出来。主菜单就是骨架的入口。用户进来能看到什么、能选择什么操作第一眼要清楚。public class StudentManager { private Student[] students new Student[10]; private int size 0; private Scanner scanner new Scanner(System.in); public static void main(String[] args) { StudentManager manager new StudentManager(); manager.run(); } public void run() { while (true) { 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.println(); System.out.print(请输入你的选择); int choice scanner.nextInt(); switch (choice) { case 1: addStudent(); break; case 2: listStudents(); break; case 3: findStudentById(); break; case 4: updateStudent(); break; case 5: deleteStudent(); break; case 6: System.out.println(感谢使用再见); return; default: System.out.println(输入有误请重新选择); } } } }这里用while (true)加switch的组合是控制台菜单的经典写法。while(true)表示菜单会不停地显示除非遇到case 6里的return直接跳出整个run方法程序才终止。用return而不是break的原因要记住break只能跳出switch跳不出while循环。4.2 添加学生与数组扩容问题添加学生是第一个真正有业务逻辑的功能。思路很简单创建学生对象放进数组size加一。但有个前提要先处理——数组满了怎么办public void addStudent() { if (size students.length) { expandCapacity(); } System.out.print(请输入学号); String id scanner.next(); System.out.print(请输入姓名); String name scanner.next(); System.out.print(请输入年龄); int age scanner.nextInt(); System.out.print(请输入成绩); double score scanner.nextDouble(); Student student new Student(id, name, age, score); students[size] student; size; System.out.println(添加成功当前共有 size 名学生。); } public void expandCapacity() { Student[] newStudents new Student[students.length * 2]; System.arraycopy(students, 0, newStudents, 0, students.length); students newStudents; System.out.println(数组容量已扩展到 students.length); }expandCapacity这个方法我单独拎出来说。数组长度固定扩容的本质就是“另找一个更大的数组把数据搬过去然后让原来的变量指向新数组”。我见过有同学在add方法里反复创建数组那是在制造垃圾对象。正确做法是封装成一个独立方法只有满了才调用。扩多少合适我用的两倍。你想想如果每次满了只扩一个位置那每次插入都要搬一次数据效率很低。扩两倍空间换时间是很多集合底层扩容策略的雏形。Java的ArrayList底层也是类似的思路只不过细节更多。你现在把这个逻辑写明白之后学集合源码会轻松得多。4.3 查询功能遍历、条件判断、断点思维查询分两种列出所有学生和按学号查找单个学生。后者是前者的基础上加了一个条件判断。列出所有学生最简单就是遍历数组的有效区间public void listStudents() { if (size 0) { System.out.println(当前没有任何学生); return; } for (int i 0; i size; i) { System.out.println(students[i]); } }这里注意一个小细节System.out.println(students[i])直接打印对象之所以能输出好看的文本而不是一串看不懂的地址是因为我们重写了toString()方法。这就是为什么Student类里那个方法必须有——没有它打印出来的就是Student1b6d3586这种垃圾信息。按学号查找核心是一个循环加一个equals判断public void findStudentById() { System.out.print(请输入要查找的学号); String id scanner.next(); boolean found false; for (int i 0; i size; i) { if (students[i].getId().equals(id)) { System.out.println(students[i]); found true; break; } } if (!found) { System.out.println(未找到学号为 id 的学生); } }字符串比较要用equals()而不能用。这是一个老生常谈但永远有人错的点。简单理解比较的是“是不是同一个东西”equals比较的是“内容相不相等”。学号是内容当然用equals。你输入一个2024001和数组里存的2024001是两个不同的字符串对象内容相同而已。4.4 修改与删除数组操作里最考验逻辑的操作修改学生信息的套路是先找到那个人的下标再重新赋值。我把查找的价值放在这里体现因为修改和删除都需要“定位”。public void updateStudent() { System.out.print(请输入要修改的学号); String id scanner.next(); int index -1; for (int i 0; i size; i) { if (students[i].getId().equals(id)) { index i; break; } } if (index -1) { System.out.println(未找到该学生); return; } System.out.print(请输入新姓名); String name scanner.next(); System.out.print(请输入新年龄); int age scanner.nextInt(); System.out.print(请输入新成绩); double score scanner.nextDouble(); students[index].setName(name); students[index].setAge(age); students[index].setScore(score); System.out.println(修改成功); }index初始值是-1这个设计叫哨兵值。因为数组下标最小是0所以用-1表示“没找到”。找到之后直接对students[index]这个对象调用setter方法修改字段。注意不需要重新new一个Student对象因为我们只是修改已有对象的属性。删除是整个系统里最考验逻辑的操作。数组是连续存储的删掉中间一个元素后面的元素都得往前挪一位而且size要减一。如果直接删掉最后一个就不用挪。public void deleteStudent() { System.out.print(请输入要删除的学号); String id scanner.next(); int index -1; for (int i 0; i size; i) { if (students[i].getId().equals(id)) { index i; break; } } if (index -1) { System.out.println(未找到该学生); return; } for (int i index; i size - 1; i) { students[i] students[i 1]; } students[size - 1] null; size--; System.out.println(删除成功); }重点看那个前移的循环。假设数组是[A, B, C, D, E]要删除下标1也就是B循环从i 1开始students[1] students[2]C挤到B的位置students[2] students[3]D挤到C的位置students[3] students[4]E挤到D的位置。循环结束后数组是[A, C, D, E, E]。最后一步students[size - 1] null把最后一个位置清空防止多余的引用残留在数组里然后再size--。这一步很容易被漏掉。漏掉的话数组最后会残留一个已经“被删除”的对象的引用虽然size不包含它但后续扩容时它会被一起复制到新数组里造成内存泄漏的隐患。零基础阶段就要习惯性地把这种细节处理好。5. 零基础最容易踩的几个坑5.1 数组越界一半的报错都源于它Exception in thread main java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 10——这段报错可以说每个写这个系统的人都会遇到。原因也很直白你往数组里塞第11个元素但数组只能装10个。刚才的扩容方法就是解决这个问题的。但还有一个隐蔽的越界点出现在删除方法里。删除循环的终止条件是i size - 1不是i size。如果写成i size当删除的是最后一个元素时循环会尝试执行students[size - 1] students[size]直接越界。我自己让学生写这个系统时十个人里有七八个会在这里卡一次。这个坑踩过之后你就不怕了因为以后所有集合删除操作本质都是类似的边界控制思路完全相通。5.2 Scanner的nextLine和next混用导致的“吞回车”这是个经典中的经典。在你输入学号姓名时如果用next()都没事但如果你某个字段用了nextLine()并且它排在某个nextInt()后面你会发现程序会异常地跳过这次输入直接执行下一行。原因是nextInt()读取完数字后会在输入缓冲区里留下一个回车符紧接着的nextLine()会把那个回车符读出来当成一个空字符串。解决的办法有两种全部使用next()不接受带空格的输入。学生信息一般不会有空格这是最简单的方案。在每次nextInt()之后补一行scanner.nextLine();把残留的回车符消费掉。我建议初学者直接采用方案1省事且对当前场景足够。等你以后真正接触到需要读取一行文本的场景再来研究nextLine的细节也不迟。5.3 死循环与退出逻辑不清最后一个小坑是菜单里的选择输入了非数字类型比如字母a。scanner.nextInt()会直接抛出InputMismatchException程序崩溃。这在初学阶段很烦人因为你辛苦运行起来的程序居然被一个字母弄死了。最简单的兜底处理是做一次输入判断if (!scanner.hasNextInt()) { System.out.println(请输入数字); scanner.next(); continue; }hasNextInt()先判断下一个输入是不是整数不是就提示并清空缓冲区然后continue让用户重新选择。这个写法能挡住九成的手误。它不算多高级的技巧但能让你后续写工具类程序时显得专业得多。6. 进阶优化从能用走向优雅6.1 用ArrayList替换数组到底好在哪当你把数组版本写顺了之后我第一次建议升级的方向是用ArrayList替换Student[]。你会发现代码量骤减因为扩容和边界控制都不用手动做了public class StudentManager { private ListStudent students new ArrayList(); private Scanner scanner new Scanner(System.in); public void addStudent() { // ...输入代码省略 students.add(new Student(id, name, age, score)); } public void deleteStudentById(String id) { for (int i 0; i students.size(); i) { if (students.get(i).getId().equals(id)) { students.remove(i); return; } } System.out.println(未找到该学生); } }ArrayList内部其实还是数组但它把扩容、搬移这些脏活累活全部封装好了你只需要调用add和remove。用起来舒服是因为Java类库替你把复杂度处理掉了。但我要提醒你不要为了用集合而把数组的理解丢掉。面试官问你ArrayList的扩容机制时如果你能结合刚才自己写的expandCapacity来讲“它也是新建一个更大的数组再拷贝”这个回答是会加分的。这也是我坚持让你先写数组版本的原因——你对底层的理解是直接背答案的人永远比不了的。6.2 程序退出后数据还在吗聊聊持久化控制台版本有个致命缺陷每次关闭程序数据全没了。因为数据都存在内存里内存断电即失。要让数据保留下来至少得做到把数据写入文件。最简单的方案是文本文件存储。每次退出前遍历students数组一行一个学生打印到文件里下次启动时再按行读回来。不过这里有一个新的麻烦字符串转对象。怎么把一行文本2024001,张三,18,90还原成一个Student对象呢答案是split(,)方法String[] parts line.split(,); String id parts[0]; String name parts[1]; int age Integer.parseInt(parts[2]); double score Double.parseDouble(parts[3]); Student student new Student(id, name, age, score); students.add(student);一旦你学会了这种“把对象转成字符串存文件再读回来还原成对象”的操作你对数据存储的理解就又上了一个台阶。文件本身就是最朴素的数据库CSV格式就是最简朴的数据交换格式。6.3 分层架构与面向接口这个系统还能长多大最后如果你想继续深入把目光投向项目的结构组织。现在的代码所有功能都堆在StudentManager一个类里小项目没问题但你不能一辈子写这种“上帝类”。一个经典的三层结构是层次职责类名示例界面交互层控制台菜单、输入输出StudentUI业务逻辑层增删改查规则、数据校验StudentService数据存储层操作数组或集合后续可换数据库StudentRepository每一层只负责自己的事层与层之间通过方法调用交互。这样做的好处是今天用数组存数据明天想换成数据库只需要改数据存储层界面层和业务层一行不用动。这就是为什么企业级项目哪怕逻辑比这复杂一百倍结构也遵循同样的套路。你在学生管理系统里打下的分层意识会直接迁移到以后学Spring框架时的Controller、Service、Mapper三层架构上。注意升级分层架构时建议先把一个功能完整跑通比如只做“添加学生”的UI - Service - Repository调用链再逐步把其他功能迁移过去。一次全改很容易出各种耦合问题小步快跑反而更稳。如果看完这些你能从零把这个系统写出来并且清楚知道每一行代码在干什么那我可以告诉你你的Java基础已经稳了。下一个项目大胆去尝试带文件存储的版本或者给系统加一个简单的登录验证功能。你会发现自己已经具备了一种能力看到一个需求脑海里能自动拆出几个类、几条链路、几个接口。这种能力就是写代码经验的本质——它不是背出来的是敲出来的。
返回列表