Java 实战项目
学完 18 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可编译运行的 Java 项目——学生成绩管理系统、多线程下载模拟器、JSON 解析器。每个项目都用纯 Java 标准库,不依赖 Maven / Gradle,把代码保存为 .java 文件直接 javac + java 就能跑。
三个项目分别覆盖 Java 三大主力场景:面向对象 + 文件 IO(命令行应用)、并发编程(多线程协作)、字符串处理 + 算法(编译器雏形)。建议你亲手敲一遍、跑通、再改造——做过一遍比读十遍更扎实。
实战 1:学生成绩管理系统
需求:做一个命令行成绩管理程序,支持录入学生(姓名 + 分数)、计算平均分、找最高分、按分数排名、把数据保存到文件 / 从文件加载。
涉及知识:面向对象封装(Student 类、私有字段、getter)、ArrayList 动态数组、Comparator 排序(lambda)、BufferedReader / BufferedWriter 文件读写、try-with-resources 自动关流。
import java.io.*;
import java.util.*;
public class StudentGradeSystem {
// 学生类(演示封装:字段私有 + getter)
static class Student {
private final String name;
private final int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() { return name; }
public int getScore() { return score; }
@Override
public String toString() {
return name + "(" + score + "分)";
}
// 序列化为一行文本: 张三,88
public String toLine() {
return name + "," + score;
}
// 反序列化: 从一行文本构造学生
public static Student fromLine(String line) {
String[] parts = line.split(",");
return new Student(parts[0].trim(), Integer.parseInt(parts[1].trim()));
}
}
private final List<Student> students = new ArrayList<>();
public void add(Student s) { students.add(s); }
public int size() { return students.size(); }
// 计算平均分
public double average() {
if (students.isEmpty()) return 0;
long sum = 0;
for (Student s : students) sum += s.getScore();
return (double) sum / students.size();
}
// 找最高分学生
public Student top() {
if (students.isEmpty()) return null;
Student best = students.get(0);
for (Student s : students) {
if (s.getScore() > best.getScore()) best = s;
}
return best;
}
// 按分数降序排名
public List<Student> rank() {
List<Student> copy = new ArrayList<>(students); // 不动原顺序
copy.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()));
return copy;
}
// 保存到文件
public void save(String path) throws IOException {
try (BufferedWriter w = new BufferedWriter(new FileWriter(path))) {
for (Student s : students) {
w.write(s.toLine());
w.newLine();
}
}
}
// 从文件加载
public void load(String path) throws IOException {
students.clear();
try (BufferedReader r = new BufferedReader(new FileReader(path))) {
String line;
while ((line = r.readLine()) != null) {
if (!line.isEmpty()) {
students.add(Student.fromLine(line));
}
}
}
}
public static void main(String[] args) throws IOException {
StudentGradeSystem sys = new StudentGradeSystem();
sys.add(new Student("张三", 88));
sys.add(new Student("李四", 95));
sys.add(new Student("王五", 72));
sys.add(new Student("赵六", 95));
sys.add(new Student("钱七", 60));
System.out.println("学生总数: " + sys.size());
System.out.printf("平均分: %.2f%n", sys.average());
System.out.println("最高分: " + sys.top());
System.out.println("排名榜:");
List<Student> ranked = sys.rank();
for (int i = 0; i < ranked.size(); i++) {
System.out.printf(" %d. %s%n", i + 1, ranked.get(i));
}
// 文件持久化
sys.save("grades.txt");
System.out.println("已保存到 grades.txt");
StudentGradeSystem loaded = new StudentGradeSystem();
loaded.load("grades.txt");
System.out.println("从文件重新加载: " + loaded.size() + " 名学生");
}
}
// 输出:
// 学生总数: 5
// 平均分: 82.00
// 最高分: 李四(95分)
// 排名榜:
// 1. 李四(95分)
// 2. 赵六(95分)
// 3. 张三(88分)
// 4. 王五(72分)
// 5. 钱七(60分)
// 已保存到 grades.txt
// 从文件重新加载: 5 名学生要点讲解:
- 封装:
Student类的字段用private final,只通过 getter 暴露——外部不能随便改成绩,这是 OOP 的基本素养。final让字段构造后不可变,天然线程安全。 ArrayListvs 数组:数组长度固定,ArrayList能动态扩容,日常开发 99% 用ArrayList。注意List<Student>的泛型约束——编译期就杜绝了"放进去 Cat、拿出来 Dog"的错误。- lambda 排序:
copy.sort((a, b) -> Integer.compare(b.getScore(), a.getScore()))等价于"按分数降序";Integer.compare比a - b更安全(不会溢出)。 - try-with-resources:
try (BufferedWriter w = ...)语法保证流在 try 结束时自动关闭,即使中途抛异常也不泄漏资源——这是 Java 7 起的标准写法,告别手写finally。 - 不要原地排序:
rank()先new ArrayList<>(students)复制一份再排,原列表顺序保持不变,这是良好的函数式习惯。 - 序列化:用最朴素的
姓名,分数一行一条,简单到能用记事本打开检查;真实项目可改用 JSON / CSV 库或Serializable。
实战 2:多线程下载模拟器
需求:同时下载 4 个文件(模拟),每个任务在自己的线程里跑,显示进度;主线程等所有任务都完成后,统计总耗时并打印每个文件的大小。
涉及知识:Thread / Runnable / Callable 三种线程形式、ExecutorService 线程池、Future 异步结果、CountDownLatch 闭锁同步、lambda 闭包。
import java.util.*;
import java.util.concurrent.*;
public class ConcurrentDownloader {
// 单个下载任务(实现 Callable,可以返回结果、可以抛受检异常)
static class DownloadTask implements Callable<String> {
private final String url;
private final int sizeMb;
public DownloadTask(String url, int sizeMb) {
this.url = url;
this.sizeMb = sizeMb;
}
@Override
public String call() throws InterruptedException {
String t = Thread.currentThread().getName();
System.out.printf("[%s] 开始 %s (%dMB)%n", t, url, sizeMb);
// 模拟下载:每轮下载 10MB,休眠 100ms 模拟网络延迟
int downloaded = 0;
while (downloaded < sizeMb) {
int chunk = Math.min(10, sizeMb - downloaded);
downloaded += chunk;
int percent = downloaded * 100 / sizeMb;
System.out.printf("[%s] %s %d%%%n", t, url, percent);
Thread.sleep(100);
}
long bytes = (long) sizeMb * 1024 * 1024;
return url + " (" + bytes + " bytes)";
}
}
public static void main(String[] args) throws InterruptedException {
List<DownloadTask> tasks = Arrays.asList(
new DownloadTask("https://example.com/a.zip", 30),
new DownloadTask("https://example.com/b.zip", 50),
new DownloadTask("https://example.com/c.zip", 20),
new DownloadTask("https://example.com/d.zip", 40)
);
System.out.println("=== 多线程下载模拟器 ===");
System.out.println("任务数: " + tasks.size());
// 1. 创建固定大小线程池(4 个工作线程复用)
ExecutorService pool = Executors.newFixedThreadPool(4);
// 2. CountDownLatch:初始计数 = 任务数,每完成一个 countDown 一次
CountDownLatch latch = new CountDownLatch(tasks.size());
long start = System.currentTimeMillis();
List<Future<String>> futures = new ArrayList<>();
// 3. 提交所有任务
for (DownloadTask task : tasks) {
Future<String> f = pool.submit(() -> {
try {
return task.call();
} finally {
latch.countDown(); // 关键:无论成功失败都要 countDown
}
});
futures.add(f);
}
// 4. 主线程阻塞,等所有任务完成(最多 30 秒,防止卡死)
latch.await(30, TimeUnit.SECONDS);
long elapsed = System.currentTimeMillis() - start;
// 5. 汇总结果
System.out.println();
System.out.println("=== 下载完成 ===");
for (Future<String> f : futures) {
try {
System.out.println("OK " + f.get());
} catch (ExecutionException e) {
System.out.println("FAIL " + e.getCause());
}
}
System.out.printf("总耗时: %.2f 秒%n", elapsed / 1000.0);
// 6. 关闭线程池(否则 JVM 不会退出)
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
}
}
// 输出(各线程打印顺序可能不同,因为并发执行):
// === 多线程下载模拟器 ===
// 任务数: 4
// [pool-1-thread-1] 开始 https://example.com/a.zip (30MB)
// [pool-1-thread-2] 开始 https://example.com/b.zip (50MB)
// [pool-1-thread-3] 开始 https://example.com/c.zip (20MB)
// [pool-1-thread-4] 开始 https://example.com/d.zip (40MB)
// [pool-1-thread-1] https://example.com/a.zip 33%
// [pool-1-thread-3] https://example.com/c.zip 50%
// [pool-1-thread-4] https://example.com/d.zip 25%
// ... (进度持续刷新)
//
// === 下载完成 ===
// OK https://example.com/c.zip (20971520 bytes)
// OK https://example.com/a.zip (31457280 bytes)
// OK https://example.com/d.zip (41943040 bytes)
// OK https://example.com/b.zip (52428800 bytes)
// 总耗时: 0.65 秒要点讲解:
- 为什么用线程池:
new Thread()手动建线程开销大、不可控;Executors.newFixedThreadPool(4)一次性创建 4 个常驻线程,任务排队复用——这是生产环境的标配。注意真实代码用ThreadPoolExecutor显式构造更可控,Executors工厂方法在阿里规范里被限制使用。 CallablevsRunnable:Runnable.run()返回void,Callable.call()能返回结果并允许抛受检异常——需要结果就用Callable。Future:pool.submit(task)立刻返回一个Future,主线程可以继续干别的,需要结果时调f.get()(会阻塞直到任务完成)。CountDownLatch:本项目的核心。new CountDownLatch(n)设初始计数,countDown()减 1,await()阻塞到计数为 0。这是"主线程等 N 个工作线程都完成"最简洁的写法。- finally 里 countDown:必须放在
finally块,否则任务一旦抛异常,计数永远不减,主线程会一直等下去——并发 bug 排查的噩梦。 - 线程安全:本例多个线程同时调
System.out.printf,Java 标准库的PrintStream是同步的,所以输出不会撕碎;真实场景下访问共享对象一定要用synchronized、ConcurrentHashMap等机制。 - 关闭线程池:
pool.shutdown()不再接受新任务但把已提交的跑完;shutdownNow()强行中断;程序退出前必须 shutdown,否则 JVM 不会终止(非 daemon 线程)。
实战 3:JSON 解析器(简易版)
需求:不用任何第三方库(不用 Jackson / Gson),手写一个递归下降解析器,支持 JSON 的 6 种值类型——对象、数组、字符串、数字、布尔、null,字符串支持常见转义。能解析、能验证、能格式化输出。
涉及知识:字符串遍历、Character 工具方法、StringBuilder、Map / List 容器、递归下降(recursive descent)算法、自定义异常、switch 语句。
import java.util.*;
/**
* 简易 JSON 解析器(递归下降)
* 支持:Object / Array / String / Number / true / false / null
* 字符串支持常见转义:\n \t \r \" \\ \/
*/
public class JsonParser {
static class ParseError extends RuntimeException {
public ParseError(String msg) { super(msg); }
}
private final String src;
private int pos = 0;
public JsonParser(String src) {
this.src = src;
}
// 跳过空白字符
private void skipWs() {
while (pos < src.length() && Character.isWhitespace(src.charAt(pos))) {
pos++;
}
}
// 入口:根据首字符分派到具体解析方法
private Object parseValue() {
skipWs();
char c = src.charAt(pos);
if (c == '{') return parseObject();
if (c == '[') return parseArray();
if (c == '"') return parseString();
if (c == 't' || c == 'f') return parseBool();
if (c == 'n') return parseNull();
return parseNumber();
}
// 解析对象:{"key": value, ...}
private Map<String, Object> parseObject() {
Map<String, Object> m = new LinkedHashMap<>();
pos++; // 跳过 {
skipWs();
if (src.charAt(pos) == '}') { pos++; return m; } // 空对象 {}
while (true) {
skipWs();
String key = parseString(); // JSON 规定 key 必须是字符串
skipWs();
if (src.charAt(pos) != ':') throw error("期望 ':'");
pos++; // 跳过 :
Object value = parseValue();
m.put(key, value);
skipWs();
char c = src.charAt(pos);
if (c == ',') { pos++; continue; }
if (c == '}') { pos++; break; }
throw error("期望 ',' 或 '}'");
}
return m;
}
// 解析数组:[value, value, ...]
private List<Object> parseArray() {
List<Object> list = new ArrayList<>();
pos++; // 跳过 [
skipWs();
if (src.charAt(pos) == ']') { pos++; return list; } // 空数组 []
while (true) {
Object value = parseValue();
list.add(value);
skipWs();
char c = src.charAt(pos);
if (c == ',') { pos++; continue; }
if (c == ']') { pos++; break; }
throw error("期望 ',' 或 ']'");
}
return list;
}
// 解析字符串:"abc"(含转义)
private String parseString() {
if (src.charAt(pos) != '"') throw error("期望字符串");
pos++; // 跳过开始的 "
StringBuilder sb = new StringBuilder();
while (pos < src.length()) {
char c = src.charAt(pos++);
if (c == '"') return sb.toString(); // 闭合引号
if (c == '\\') { // 转义序列
if (pos >= src.length()) throw error("转义未闭合");
char e = src.charAt(pos++);
switch (e) {
case 'n': sb.append('\n'); break;
case 't': sb.append('\t'); break;
case 'r': sb.append('\r'); break;
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
default: sb.append(e);
}
} else {
sb.append(c);
}
}
throw error("字符串未闭合");
}
// 解析数字(整数 / 小数 / 负数 / 科学计数法)
private Double parseNumber() {
int start = pos;
if (src.charAt(pos) == '-') pos++;
while (pos < src.length()) {
char c = src.charAt(pos);
if (Character.isDigit(c) || "eE+-.".indexOf(c) >= 0) {
pos++;
} else {
break;
}
}
return Double.parseDouble(src.substring(start, pos));
}
// 解析 true / false
private Boolean parseBool() {
if (src.startsWith("true", pos)) { pos += 4; return Boolean.TRUE; }
if (src.startsWith("false", pos)) { pos += 5; return Boolean.FALSE; }
throw error("无效的字面量");
}
// 解析 null
private Object parseNull() {
if (src.startsWith("null", pos)) { pos += 4; return null; }
throw error("无效的字面量");
}
private ParseError error(String msg) {
return new ParseError("解析错误 @ " + pos + ": " + msg);
}
// === 对外入口 ===
public static Object parse(String json) {
JsonParser p = new JsonParser(json);
Object result = p.parseValue();
p.skipWs();
if (p.pos != p.src.length()) throw p.error("尾部多余字符");
return result;
}
// 把解析结果重新序列化为可读格式(便于查看结构)
@SuppressWarnings("unchecked")
private static String pretty(Object o, int indent) {
String pad = " ".repeat(indent);
String padChild = " ".repeat(indent + 1);
if (o == null) return "null";
if (o instanceof String) return "\"" + o + "\"";
if (o instanceof Number || o instanceof Boolean) return o.toString();
if (o instanceof Map) {
Map<String, Object> m = (Map<String, Object>) o;
if (m.isEmpty()) return "{}";
StringBuilder sb = new StringBuilder("{\n");
int i = 0;
for (Map.Entry<String, Object> e : m.entrySet()) {
sb.append(padChild)
.append("\"").append(e.getKey()).append("\": ")
.append(pretty(e.getValue(), indent + 1));
if (i++ < m.size() - 1) sb.append(",");
sb.append("\n");
}
sb.append(pad).append("}");
return sb.toString();
}
if (o instanceof List) {
List<Object> l = (List<Object>) o;
if (l.isEmpty()) return "[]";
StringBuilder sb = new StringBuilder("[\n");
for (int i = 0; i < l.size(); i++) {
sb.append(padChild).append(pretty(l.get(i), indent + 1));
if (i < l.size() - 1) sb.append(",");
sb.append("\n");
}
sb.append(pad).append("]");
return sb.toString();
}
return o.toString();
}
// === 测试入口 ===
public static void main(String[] args) {
String json = "{\"name\": \"小明\", \"age\": 20, \"scores\": [95, 88, 92], \"passed\": true, \"note\": null}";
System.out.println("=== 原始 JSON ===");
System.out.println(json);
Object obj = JsonParser.parse(json);
System.out.println("\n=== 解析后(格式化输出)===");
System.out.println(pretty(obj, 0));
// 类型验证
Map<String, Object> map = (Map<String, Object>) obj;
System.out.println("=== 字段类型验证 ===");
System.out.println(" name = " + map.get("name")
+ " (" + map.get("name").getClass().getSimpleName() + ")");
System.out.println(" age = " + map.get("age")
+ " (" + map.get("age").getClass().getSimpleName() + ")");
System.out.println(" passed = " + map.get("passed")
+ " (" + map.get("passed").getClass().getSimpleName() + ")");
System.out.println(" note = " + map.get("note"));
@SuppressWarnings("unchecked")
List<Object> scores = (List<Object>) map.get("scores");
System.out.println(" scores 长度 = " + scores.size() + ", 第一项 = " + scores.get(0));
// 转义字符测试
String withEscape = "{\"msg\": \"hello\\nworld\"}";
@SuppressWarnings("unchecked")
Map<String, Object> m2 = (Map<String, Object>) JsonParser.parse(withEscape);
System.out.println("\n=== 转义测试 ===");
System.out.println("解析出的 msg = [" + m2.get("msg") + "]");
// 错误格式测试
System.out.println("\n=== 异常路径测试 ===");
try {
JsonParser.parse("{\"key\": }"); // value 缺失
} catch (ParseError e) {
System.out.println("捕获预期错误: " + e.getMessage());
}
}
}
// 输出:
// === 原始 JSON ===
// {"name": "小明", "age": 20, "scores": [95, 88, 92], "passed": true, "note": null}
//
// === 解析后(格式化输出)===
// {
// "name": "小明",
// "age": 20.0,
// "scores": [
// 95.0,
// 88.0,
// 92.0
// ],
// "passed": true,
// "note": null
// }
// === 字段类型验证 ===
// name = 小明 (String)
// age = 20.0 (Double)
// passed = true (Boolean)
// note = null
// scores 长度 = 3, 第一项 = 95.0
//
// === 转义测试 ===
// 解析出的 msg = [hello
// world]
//
// === 异常路径测试 ===
// 捕获预期错误: 解析错误 @ 9: 期望字符串要点讲解:
- 递归下降:这是手写解析器的经典套路。
parseValue看一眼当前字符:{就调parseObject,[就调parseArray,"就调parseString……每个函数自己推进pos。对象/数组的元素又回到parseValue——这就是"递归"。 - 游标 + 源字符串:
pos是当前解析位置,全程只读不修改原字符串——比不断substring高效得多。这是几乎所有编译器/解析器的标准做法。 LinkedHashMap:对象用LinkedHashMap而非HashMap,保留 key 的插入顺序,格式化输出和原文一致;这对 JSON 文件 diff、可读性都很重要。- 异常分类:用专门的
ParseError extends RuntimeException而不是Exception——解析失败是程序错误(代码 bug),不该让调用方强制 try/catch;并且带位置信息@ pos N便于调试。 - 转义处理:遇到
\不直接 append,而是看下一个字符决定真实含义——\n是换行、\t是制表符等。这是字符串字面量解析的通用模式。 - 健壮性:做了 3 类测试——正常路径、转义字符、错误格式;若解析完还有剩余字符,主动报"尾部多余字符",防止静默错误。真实解析器还应处理 Unicode 转义
\uXXXX、整数 vs 浮点精度、科学计数法边界等。 - 为什么手写解析器:不为了重复造轮子,而是体会"字符串如何变成结构化数据"的过程。看懂这个就理解了 Jackson / Gson 在做什么、配置文件 / SQL 语句是怎么被机器读懂的——所有"语言"底层都是这套机制。
小结
三个项目串起了 Java 入门阶段最重要的能力点:OOP 设计 + IO(成绩系统)、并发(多线程下载)、算法 + 字符串(JSON 解析器)。它们都不依赖外部库,但每一个都映射到真实工程——成绩系统是任何业务后台的雏形,多线程下载是 RPC / 消息队列的简化版,JSON 解析器则是所有框架底层(序列化、配置、协议)的缩影。
建议你挑一个项目做二次开发:给成绩系统加"按姓名查找 / 删除 / 修改"和"按平均分分级";给下载器加"失败重试"和"断点续传";给 JSON 解析器加 Unicode 转义和整数 / 浮点区分。改造的过程,才是真正把 Java 内化为手感的过程。
恭喜!到这里 Java 系列就真正完结了。接下来挑一个方向深入——Web 后端(Spring Boot)、Android、大数据(Hadoop / Spark)、消息中间件(Kafka)——边做边学,进步最快。
← 上一篇 Java 多线程
返回 Java 教程目录