Java 后端

常用 API 与文件网络数据

字符串与常量池、日期时间、精确数值与 Optional、正则、文件与 NIO.2、网络编程、XML 与 JSON、序列化与内置类。

标准库里最容易踩坑的不是 API 数量,而是几个反复出现的语义:字符串不可变、常量池、时间不可变、BigDecimal 不等于浮点。把这几条钉住,剩下的按用途查表即可。

一、字符串处理

String 不可变——所有“修改”方法都返回新串,原串不变。这条性质换来三件实际好处:字符串会参与类加载、网络协议和密钥这类敏感数据,内容不可改才不会在别处被悄悄篡改;不可变对象可以安全跨线程共享;相同字面量才能共享常量池里的同一个对象。

场景 选择
常量、配置键、错误消息 String
循环、频繁累加拼接 StringBuilder
多线程共享的可变容器 StringBuffer(方法带同步)
多值连接 String.join(",", "A", "B", "C")

+ 从左向右求值,一旦有一侧是 String 就转为拼接,这是初学者最常算错的地方:

java5 行
1 + 2              // 3
"1" + 2            // "12"
1 + 2 + "3"        // "33",先算出 3 再拼接
"v=" + 20 + 20     // "v=2020"
"v=" + (20 + 20)   // "v=40"

常量池与 intern 是必须理解的一段机制:字面量 "hello" 创建时先查池,没有就放进去、已有就复用;new String("hello") 明确要求再造一个对象,即便池里已经有。

java9 行
String a = "hello";
String b = "hello";
String c = new String("hello");
String d = c.intern();

a == b        // true,字面量复用同一对象
a == c        // false
a.equals(c)   // true,内容相同
a == d        // true,intern 返回池中该内容的规范引用

intern() 准确说是“返回常量池中与该内容对应的规范引用”,并非把 new 出来的对象搬进池。业务上比较内容永远用 equals。

常用方法按用途成组记,比逐个背快:

类别 方法
取长与取字符 length() charAt(i) isEmpty() isBlank()
截取 substring(begin, end) strip() lines()
查找 indexOf lastIndexOf contains startsWith endsWith
变换 toUpperCase toLowerCase replace concat
拆分合并 split String.join formatted

注意:索引从 0 开始;substring(begin, end) 的 end 不含在内,"abcdef".substring(1, 3) 得到 "bc";越界抛 StringIndexOutOfBoundsException 而不是返回 null;trim() 只去 ASCII 空格,新代码优先 strip()。

多行文本用文本块(Java 15):

java3 行
String json = """
        {"name":"John","age":30}
        """;

三个双引号后必须换行;共同缩进会被去掉,结束分隔符的位置决定末尾换行是否保留。

二、日期与时间

旧 Date / Calendar 的问题很明确:可变、非线程安全、Calendar 的月份从 0 开始。新代码统一用 java.time,它全部是不可变类型——运算返回新实例,原值不动。

类型 存什么 关键方法
LocalDate 年月日,无时区 of now plusDays withYear isBefore
LocalTime 时间,无日期 of withHour
LocalDateTime 本地钟面,无时区 of toLocalDate toLocalTime
ZonedDateTime 钟面 + 时区 + 偏移 atZone withZoneSameInstant
Instant UTC 时间点 now toEpochMilli atZone
DateTimeFormatter 格式规则 ofPattern format parse
java5 行
LocalDate today = LocalDate.of(2018, 2, 1);
today.plusDays(100);     // 2018-05-12,新实例,today 不变
today.isLeapYear();      // false
today.withYear(2016);    // 2016-02-01
long days = ChronoUnit.DAYS.between(today, LocalDate.now());

跨时区才需要 ZonedDateTime:本地时间挂上时区得到偏移,形如 2026-09-24T12:00+08:00[Asia/Shanghai];换算用 withZoneSameInstant,时间点不变、钟面变化。

java3 行
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsed = LocalDateTime.parse("2026-09-24 12:00:00", fmt);
Instant.atZone(ZoneId.of("Asia/Shanghai")); // 2026-09-24T12:00+08:00[Asia/Shanghai]

LocalDate.of(2018, 2, 1) 里月份是 2 而不是 1——这正是 java.time 相对 Calendar 少一个坑的地方。

提示:DateTimeFormatter 不可变且线程安全,适合定义成 static final 全局共享;存库排序用 Instant,只在展示层 atZone 转时区。

三、精确数值与 Optional

BigDecimal 表达十进制精确数,两个要点决定它能否用对:必须从字符串构造,除法要指定精度。

java4 行
var a = new BigDecimal("0.1");
var b = new BigDecimal("0.2");
a.add(b);                               // 0.3
a.divide(b, 2, RoundingMode.HALF_UP);    // 0.50

它同样不可变,add/multiply 都返回新对象,也不支持 + 运算符。判等有个细节:

java2 行
new BigDecimal("1.0").compareTo(new BigDecimal("1.00")); // 0,相等
new BigDecimal("1.0").equals(new BigDecimal("1.00"));    // false,scale 不同

equals 连小数位数一起比,所以比例、金额比较一律用 compareTo(...) == 0。

Optional 把“值可能不存在”写进方法签名,用法见集合篇:ofNullable 包裹、ifPresent 消费、orElseGet 提供惰性默认值、orElseThrow 把缺失当异常。

java3 行
var user = Optional.ofNullable(findUser());
user.ifPresent(u -> log.info("用户: {}", u));
String name = user.orElseGet(() -> loadDefaultName());

四、正则表达式

匹配先记元字符:\d 数字、\w 字母数字下划线、. 任意单字符(换行除外)、* 零次或多次、+ 一次或多次、? 零次或一次、() 分组、{n,m} 重复 n 到 m 次。

java2 行
"10012345678".matches("\\d{11}");  // true
"abc_123".matches("\\w+");         // true

四条使用规则:

  • String.matches() 要求整个字符串匹配,适合校验;找子串要用 Pattern + Matcher.find()。
  • 分组从 1 开始编号,0 号是整体;具名分组 (?<year>\d{4}) 比数字下标好读,用 group("year") 取。
  • 默认贪婪匹配,.* 会一路吃到最后一个结尾;逐个取出时写成 .*? 并配合 while (find())。
  • 切分用 split("\\s*,\\s*"),替换用 replaceAll,定位用 start()/end()。
java3 行
Pattern p = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = p.matcher("今天 2026-09-24 晴");
if (m.find()) System.out.println(m.group("year")); // 2026

注意:Java 字符串里正则的 \d 要写成 "\\d",那一层反斜杠来自字符串转义,不是正则多写了一杠。

五、文件与 NIO.2

三个角色分工明确:Path 描述一个位置,Path.of 是创建入口,Files 真正执行读取、写入、查询与遍历。

java4 行
Path dir = Path.of("/tmp", "app", "seed");
Files.createDirectories(dir);              // 先建父目录,否则写失败
Path file = dir.resolve("data.txt");
Files.write(file, List.of("apple", "bat", "cat"));

目录遍历有三种,覆盖不同需求:

方法 深度 谓词拿到什么 适用
Files.list 不递归,只一层 Path 看当前目录内容
Files.walk 递归,需给最大深度 Path 扫子树
Files.find 递归,需给最大深度 Path + BasicFileAttributes 按大小、修改时间、是否文件过滤
java4 行
try (var stream = Files.find(dir, 3, (path, attrs) ->
        attrs.isRegularFile() && path.toString().endsWith(".txt"))) {
    stream.forEach(System.out::println);
}

Files.find 比 walk 强的地方是谓词能拿到文件属性,不用对每个文件额外调一次 Files.readAttributes。

读整文件有两个快捷方法,默认使用 UTF-8:

java2 行
String text = Files.readString(file);
Files.writeString(target, text);

按大小选读写方式:

方法 适用 说明
readString / writeString KB 级文本 一次进出
readAllLines 行处理、数据量可装下内存 全部进内存
Files.lines 大文件 懒流,必须关闭
java3 行
try (var lines = Files.lines(file)) {   // 持有句柄,必须 try-with-resources
    long count = lines.filter(l -> l.contains("error")).count();
}

警告:readAllLines 遇到百万行文件会把全部内容装进内存;Files.list/walk/find 返回的流持有目录句柄,不关闭会一直占用。

六、网络编程:回环先跑通

方式 连接方式 写法 适用
TCP 长连接,一问一答 ServerSocket + Socket 自定义协议
UDP 无连接发包 DatagramSocket + DatagramPacket 丢包无妨的通知
HTTP 短连接请求响应 HttpClient + HttpRequest 调接口、拉数据

本地验证一律先在回环上跑通,端口写 0 让系统自动分配,省掉“端口被占”的干扰:

java4 行
try (var server = new ServerSocket(0)) {   // 0 = 系统分配
    int port = server.getLocalPort();
    try (var socket = new Socket("127.0.0.1", port)) { ... }
}

两个真实项目里最容易出问题的点:TCP 是字节流没有消息边界,两条消息可能粘成一条,读端要按行或按长度自己定帧;套接字与流一定要 try-with-resources 收口,否则连接越跑越少。

java2 行
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
resp.statusCode();  // 200

七、XML 与 JSON

XML 用 JDK 内置的 DOM 处理小文件(整树解析、按标签取值),大文件只读或内存吃紧时用 SAX 流式。JSON 没有内置 API,需要引入 Jackson。

java11 行
// XML 读:内置 javax.xml,无需额外依赖
var doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
        .parse(new InputSource(new StringReader(xml)));
var name = doc.getElementsByTagName("name").item(0).getTextContent(); // alan

// XML 写:建 Document → appendChild 挂元素 → setAttribute 设属性 → Transformer 输出

// JSON:需要 com.fasterxml.jackson.core:jackson-databind
var mapper = new ObjectMapper();
var user = mapper.readValue("{\"id\":1,\"name\":\"alan\"}", User.class);
String json = mapper.writeValueAsString(user);
方式 归属 适用
DOM JDK 内置 小 XML 读写
SAX JDK 内置 大 XML 只读
Jackson 外部依赖 JSON 与对象互转

警告:ObjectMapper 来自 com.fasterxml.jackson.core:jackson-databind,没声明依赖就编译不过;读写统一显式指定 UTF-8,中文乱码先查编码是否两端一致。

八、序列化与对象持久化

Java 原生序列化把对象状态转成字节流,用于存文件、缓存或跨进程传递。

java6 行
class Rectangle implements Serializable {
    private static final long serialVersionUID = 1L;
    int length;
    int breadth;
    transient int area;     // 不参与序列化
}
java6 行
try (var out = new ObjectOutputStream(new FileOutputStream("rect.ser"))) {
    out.writeObject(rectangle);
}
try (var in = new ObjectInputStream(new FileInputStream("rect.ser"))) {
    Rectangle rectangle = (Rectangle) in.readObject();
}

四个必须知道的点:

  • transient 字段不写进流,反序列化后是默认值。派生字段(area)、敏感数据(密码、密钥)、本身不可序列化的字段都该标记。
  • serialVersionUID 是兼容性标识。新增非 transient 字段通常可以不改;删除字段或改变字段类型属于破坏性变更,需要更新它。不显式声明时结构一变可能导致旧数据无法反序列化。
  • 反序列化不调用构造器,而是直接恢复先前状态;Serializable 与 Externalizable 在这一点上行为不同。
  • Externalizable 让你完全控制写什么、怎么写:
java9 行
public void writeExternal(ObjectOutput out) throws IOException {
    out.writeInt(length);
    out.writeInt(breadth);
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    length = in.readInt();
    breadth = in.readInt();
    area = length * breadth;   // 读回来后重算派生字段
}

警告:反序列化本身是危险的入口,不要对不可信数据直接反序列化。跨服务传输数据更常用的选择是 JSON、Protobuf 这类显式格式,而不是原生序列化。finalize() 已在 Java 9 弃用,不要用它管理资源。

九、内置类速览

几个到处都会遇到的内置类,重点是各自的使用方式与陷阱:

类 常用入口 注意事项
Object toString equals hashCode getClass 所有类的根类;wait/notify 必须在同步上下文中调用
System getProperty getenv nanoTime exit exit 直接结束 JVM,不能当普通返回用
Runtime getRuntime availableProcessors freeMemory exec gc() 只是请求;exec 是异步启动进程
UUID randomUUID() 适合主键、事务 ID、会话 ID
Locale Locale.CHINA、getDisplayLanguage 影响格式化结果,进程内是全局设置
Timer schedule(task, delay, period) 延迟执行 + 固定周期重复
ThreadLocal withInitial set get 每个线程一份副本,用完记得清理

几点展开:

java5 行
System.getProperty("java.version");  // JVM 系统属性
System.getenv("PATH");                // 操作系统环境变量,来源不同
long start = System.nanoTime();       // 测耗时就用单调递增的纳秒时钟
Runtime.getRuntime().availableProcessors();
UUID.randomUUID();                    // 名字里是 UUID,不是 UID

计时用 nanoTime 而不是 currentTimeMillis:后者返回的是墙上时钟,可能被系统时间调整影响。

金额展示需要注意 Locale:

java4 行
NumberFormat us = NumberFormat.getCurrencyInstance(Locale.US);
NumberFormat uk = NumberFormat.getCurrencyInstance(Locale.UK);
System.out.println(us.format(new BigDecimal("12.34")));
System.out.println(uk.format(new BigDecimal("12.34")));  // 金额显示会随地区变化

注意:ThreadLocal 在线程池里最容易被忽略的问题是没有清理——线程复用时残留数据会带到下一个任务,用完应调用 remove()。

十、易错点总表

现象 原因 处理
1 + 2 + "3" 得 "33" 加号从左向右、遇 String 即拼接 需要先算就加括号
金额算错或抛异常 double 构造 BigDecimal / 除法没给精度 字符串构造 + 指定精度与舍入模式
equals 判金额不等 equals 比较 scale 比例值用 compareTo 判 0
时区对不上 LocalDateTime 不带时区 跨时区用 ZonedDateTime + withZoneSameInstant
正则编译不过 少写一层反斜杠 \d 在字符串里写 "\\d"
匹配吃到最后一个结尾 贪婪匹配 改 .*? 配合 while (find())
大文件内存溢出 readAllLines 全量进内存 换 Files.lines 流式处理
目录句柄泄漏 list/walk/find 的流未关 try-with-resources
端口被占 / 消息粘连 固定端口 / TCP 无消息边界 本地用端口 0;按行或长度定帧
ObjectMapper 找不到 缺 Jackson 依赖 声明 jackson-databind 坐标
反序列化旧数据失败 字段结构变了但 serialVersionUID 未变 破坏性变更时更新 UID
金额显示格式随环境变 进程级 Locale 影响格式化 显式传 Locale 给 NumberFormat
线程池里数据串号 ThreadLocal 未清理 用完调用 remove()

字符串看不可变与常量池,时间看不可变与时区,金额看 BigDecimal 与 Locale,文件看大小决定整读还是流式,网络看字节流定帧。这几条固定下来,剩下的都是查表。

← 返回文章