Skip to content

Java 首批课程:类型、集合、资源与学习记录CLI ​

JDK 编译、Java 启动和 check 测试的执行表述沿用原文 2026-10-17 历史批次;本轮未重跑,示例输出供读者自行核对。

前置只需会保存文件和使用终端,不要求先配置Spring、Tomcat或Maven。JDK工具→类型与方法→对象和接口→集合/泛型→资源与异常→完整项目,构成首批路径。已有语言基础、对象接口可用于进一步巩固;本篇项目不依赖框架。

JDK、版本与首次运行 ​

本批选择Temurin21 LTS,实际工具版本以java --version、javac --version为准,下载的21.0.12.1构建已按发行方SHA256核验。Windows amd64 PowerShell与WSL Linux amd64分别实际运行。21是教学稳定路径,不宣称是最高版本;不启用预览特性。macOS不纳入本轮验收。

从Temurin官方发行页选JDK及本平台架构,解压后可用bin内完整路径,不必改系统JAVA_HOME。JDK包含编译/诊断工具,JVM执行字节码;不要只装运行环境后期待javac存在。两个命令应来自同一套JDK,安装IDE不是终端配置成功的证据。

保存Hello.java:

java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

执行javac -encoding UTF-8 Hello.java再java Hello,预期Hello, Java!。公开类名与文件名一致;启动类名不带.class。PowerShell调用含空格的可执行文件路径时用& '完整路径/java.exe'。本文项目把类输出放out,避免把编译产物和源码混在一起。

类型、对象与值传递 ​

基本类型值与对象引用不是同一种东西;Java始终按值传参,复制引用值后双方可以指向同一对象,不等于按引用传递变量本身。String不可变,数组和多数集合可变;final限制变量再赋值,不自动冻结所指对象。int溢出不提供业务范围校验,外部输入仍需限制。

下面ValueDemo.java完整可运行:

java
import java.util.ArrayList;
import java.util.List;
public class ValueDemo {
    record Task(String title, boolean done) {}
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("java"));
        List<String> alias = names;
        alias.add("test");
        if (names.size() != 2) throw new AssertionError();
        if (!new Task("read", false).equals(new Task("read", false))) throw new AssertionError();
        String first = new String("java"), second = new String("java");
        if (first == second || !first.equals(second)) throw new AssertionError();
        System.out.println("values passed");
    }
}

编译后运行,预期values passed。引用的==比较是否同一对象,equals遵循类的实现;record按组件值提供相等实现,但其组件若引用可变对象,并不自动深度不可变。重写equals通常要一并正确实现hashCode。String的length按UTF-16代码单元计数,不通用等于视觉字符数。

方法、接口与泛型集合 ​

先把处理写成参数/返回值清晰的方法,再按职责组合对象。类可实现多个接口;重载由参数列表区分,重写为继承方法提供实现,不能仅改返回类型创造重载。泛型如Map<String,Integer>让编译器检查键值类型,不能把所有集合都降成Object再强制转换。

List表达顺序和重复,Set表达去重,Map表达键值;Map不继承Collection。HashMap不承诺迭代顺序,LinkedHashMap保留相应顺序,TreeMap按比较规则排序。选择实现先看业务语义。本项目用TreeMap让输出稳定,merge累加分类时长;最多10000×600,int在所定义输入范围内可容纳结果。

Stream可描述转换和聚合,但并不自动更快;parallelStream更不是给任何任务提速的开关。先用易读循环完成输入校验,再决定是否值得换流。Optional也不替代异常原因或全部空值判断,首批不因API形式增加无用层次。

资源、异常与测试约定 ​

GC管理内存回收,不代表文件、锁或网络资源都应等GC关闭。try-with-resources在离开作用域时关闭AutoCloseable;库方法若只借用调用方Reader,应说明不关闭它,CLI拥有文件时负责关闭。IOException属于需要声明或处理的受检异常,IllegalArgumentException表达本项目的非法记录;不捕获Throwable把严重运行错误伪装为正常空结果。

测试类用显式check抛AssertionError,避免Java的assert默认未启用而出现“测试没有检查”的误解。若使用语言assert,运行时必须理解-ea的影响。单测验证函数,真实CLI测试另验证文件、stderr/stdout及进程退出码。

完整学习时长TSV汇总 ​

路径examples/java/study-report。输入无表头,每行category<TAB>minutes;TAB为实际制表符,不是反斜线加t。分类为1–32个ASCII字母/数字/下划线/短横线、首字符字母;分钟字段为1–3位ASCII十进制数字,数值1–600,因此0001也不符合格式。最多10000行,单行最多256个UTF-16代码单元,空文件合法、空行失败。UTF-8解码明确指定,不把它称为任意CSV支持。

src/StudyReport.java
java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.SortedMap;
import java.util.TreeMap;

public class StudyReport {
    record Session(String category, int minutes) {}

    static Session parse(String line, int row) {
        String[] fields = line.split("\t", -1);
        if (fields.length != 2 || !fields[0].matches("[A-Za-z][A-Za-z0-9_-]{0,31}")
                || !fields[1].matches("[0-9]{1,3}")) {
            throw new IllegalArgumentException("row " + row + ": invalid TSV record");
        }
        int minutes = Integer.parseInt(fields[1]);
        if (minutes < 1 || minutes > 600) throw new IllegalArgumentException("row " + row + ": minutes must be 1..600");
        return new Session(fields[0], minutes);
    }

    public static SortedMap<String, Integer> summarize(Reader source) throws IOException {
        BufferedReader input = source instanceof BufferedReader reader ? reader : new BufferedReader(source);
        SortedMap<String, Integer> totals = new TreeMap<>();
        String line;
        int row = 0;
        while ((line = input.readLine()) != null) {
            if (++row > 10000 || line.length() > 256) throw new IllegalArgumentException("input limit exceeded");
            Session session = parse(line, row);
            totals.merge(session.category(), session.minutes(), Integer::sum);
        }
        return totals;
    }

    public static int run(String[] args, PrintStream out, PrintStream err) {
        if (args.length != 1) { err.println("usage: StudyReport FILE"); return 2; }
        try (BufferedReader input = Files.newBufferedReader(Path.of(args[0]), StandardCharsets.UTF_8)) {
            SortedMap<String, Integer> totals = summarize(input);
            totals.forEach((category, minutes) -> out.println(category + "=" + minutes));
            return 0;
        } catch (IOException | IllegalArgumentException error) {
            err.println("error: " + error.getMessage());
            return 1;
        }
    }

    public static void main(String[] args) {
        System.exit(run(args, System.out, System.err));
    }
}
test/StudyReportTest.java
java
import java.io.StringReader;
import java.util.Map;

public class StudyReportTest {
    static void check(boolean condition) {
        if (!condition) throw new AssertionError("check failed");
    }
    public static void main(String[] args) throws Exception {
        check(StudyReport.summarize(new StringReader("go\t20\r\njava\t30\ngo\t10\n")).equals(Map.of("go", 30, "java", 30)));
        check(StudyReport.summarize(new StringReader("")).isEmpty());
        for (String text : new String[]{"go\t0", "go\t601", "go\t0001", "go\t", "\t20", "go\t1\textra", "go\t+1", "go\t1x", "\n"}) {
            boolean caught = false;
            try { StudyReport.summarize(new StringReader(text)); }
            catch (IllegalArgumentException error) { caught = true; }
            check(caught);
        }
        System.out.println("study tests passed");
    }
}
sample.tsv(字段间为Tab)
text
go	20
java	30
go	10

进入项目目录,Windows与Linux均执行以下独立命令(使用已配置JDK或其完整路径):

shell
javac -encoding UTF-8 -d out src/StudyReport.java test/StudyReportTest.java
java -cp out StudyReportTest
java -cp out StudyReport sample.tsv

测试预期study tests passed;CLI预期两行go=30、java=30,按名称排序。项目使用JDK标准库,不需要Maven下载依赖。Reader逐行读取后检查长度,不承诺对任意超长行的固定内存占用。方法返回完整结果后才输出,防止半份报告被误当成功。

JVM工具、退出码与诊断 ​

有效输入0写stdout;缺少/多余参数2向stderr写usage;文件/UTF-8解码/格式错误1写error。示例只读输入,输出路径重定向不要与输入同名。相对文件名相对启动目录,不相对类文件位置。

ClassNotFoundException先检查-cp out、类名与包路径;UnsupportedClassVersionError表示运行时不支持当前字节码版本,先核对java/javac和--release约定,不随机改业务代码。NumberFormatException应在输入边界按契约处理,不用宽泛catch吞掉所有程序错误。

javap -classpath out StudyReport可检查已编译类的公开结构;jcmd -l列出可见的同权限JVM进程,进程退出后不能再附加。完整GC、线程转储与性能调优另批,需要运行环境和目标进程权限,不以一条工具命令代替诊断课程。

练习与参考解法 ​

  1. 为600/601、缺分钟、三个字段和空文件预测结果。2. 增加合格学习条数,定义阈值并测边界。3. 将分类输出改为插入顺序应改哪里?4. 解释record与List是否都不可变、库与CLI谁关闭Reader。
提示与参考解法

600合法、601失败;缺分钟和多字段失败,split保留末尾空字段用于校验;空文件输出为空且成功0。先为Session定义“分钟>=阈值”的行为,再在累计时统计,测试阈值前后。插入顺序选LinkedHashMap并相应修改返回类型契约,不能保留SortedMap承诺却返回无序实现。record只提供组件层的值语义,List可变与否由实现决定;summarize借用Reader,CLI的try-with-resources负责关闭它。

下一批覆盖并发、JVM调优、模块、Maven/Gradle深入及Spring,均不作为本项目强制前置。依据:Dev.java学习路线、Java21标准API、Temurin发行记录;国内讲解对照廖雪峰Java教程的学习顺序,不把其中不同版本示例当21特性依据。核对2026-10-17。

继续阅读