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 名学生

要点讲解:

实战 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 秒

要点讲解:

实战 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: 期望字符串

要点讲解:

小结

三个项目串起了 Java 入门阶段最重要的能力点:OOP 设计 + IO(成绩系统)、并发(多线程下载)、算法 + 字符串(JSON 解析器)。它们都不依赖外部库,但每一个都映射到真实工程——成绩系统是任何业务后台的雏形,多线程下载是 RPC / 消息队列的简化版,JSON 解析器则是所有框架底层(序列化、配置、协议)的缩影。

建议你挑一个项目做二次开发:给成绩系统加"按姓名查找 / 删除 / 修改"和"按平均分分级";给下载器加"失败重试"和"断点续传";给 JSON 解析器加 Unicode 转义和整数 / 浮点区分。改造的过程,才是真正把 Java 内化为手感的过程。

恭喜!到这里 Java 系列就真正完结了。接下来挑一个方向深入——Web 后端(Spring Boot)、Android、大数据(Hadoop / Spark)、消息中间件(Kafka)——边做边学,进步最快。

← 上一篇 Java 多线程

返回 Java 教程目录

✈️💬