第9章 JDK 11 新特性:var 与便捷新 API
本系列是《Java 高级用法》篇,承接《Java 基础入门》21 章内容。前两章我们讲了 JVM 调优和 JDK 8 的 Lambda/Stream,本章聚焦 JDK 11 的招牌特性:var 局部变量类型推断,以及 String、文件读写、HttpClient 等一大批"让日常开发更省事"的新 API。本章面向小白读者,所有术语第一次出现都会解释,请放心阅读。

背景介绍
JDK 11 于 2018 年发布,是自 JDK 8 之后最重要的 LTS(Long Term Support,长期支持)版本------官方承诺长期维护,因此大量企业从 JDK 8 迁移到 JDK 11。它没有 JDK 8 那种"改天换地"的大动作,却带来了一批"用起来真香"的细节改进:
var关键字让局部变量声明从"写两遍类型"变成"写一遍",代码更短;- String 新增 isBlank、strip、repeat 等高频方法,处理字符串少写很多工具类;
- Files.readString / writeString 让读写小文件从"十几行"变成"一行";
- HttpClient 正式转正,用现代 API 发起 HTTP 请求;
java Hello.java让"写完就能跑"成为现实,学习阶段再也不用先 javac 再 java;- 配合第 8 章学过的 Stream/Optional,还补上了 isEmpty、Predicate.not、不可变集合等"让流式代码更顺手的零件"。
本章就把这些特性一个一个讲透:怎么用、什么时候用、有什么限制,以及最重要的------JDK 8 没有这些写法,在 JDK 8 项目里使用会直接编译失败。
一、var:局部变量类型推断

var 不是"动态类型",而是让编译器根据初始值推断 出变量的真实类型。var s = "hello" 编译后,s 依然是 String,类型在编译期就确定,运行期没有任何变化。
1. 基本用法
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class VarDemo {
public static void main(String[] args) {
// 传统写法:类型写两遍
String name = "张三";
// var 写法:类型只写一遍
var name2 = "张三";
System.out.println(name2);
// 集合类型推断
var list = new ArrayList<String>(); // 等价于 ArrayList<String> list = ...
list.add("java");
System.out.println(list);
// 复杂泛型也能推断:Map<String, List<Integer>>
var map = new HashMap<String, List<Integer>>();
map.put("score", List.of(90, 95)); // List.of 需 JDK 11/17 才能运行
System.out.println(map);
}
}
// 输出:
// 张三
// [java]
// {score=[90, 95]}
2. 各种类型的推断结果
java
public class VarInfer {
public static void main(String[] args) {
// 基本类型:整数默认 int,想当 long 用要写 L
var i = 10; // int
var l = 10L; // long
var d = 3.14; // double
System.out.println("i 的类型:" + ((Object) i).getClass().getSimpleName());
System.out.println("l 的类型:" + ((Object) l).getClass().getSimpleName());
System.out.println("d 的类型:" + ((Object) d).getClass().getSimpleName());
// 数组:推断结果是 int[],不是 Integer[]
var arr = new int[]{1, 2, 3};
System.out.println("arr 的类型:" + arr.getClass().getSimpleName() + ",长度 " + arr.length);
// 三目运算:int 和 long 取公共类型 long
var x = true ? 1 : 2L;
System.out.println("三目结果类型:" + ((Object) x).getClass().getSimpleName());
}
}
// 输出:
// i 的类型:Integer
// l 的类型:Long
// d 的类型:Double
// arr 的类型:int[]
// 三目结果类型:Long
java
// var 与 Lambda 配合:变量类型一目了然
import java.util.function.Function;
public class VarLambda {
public static void main(String[] args) {
// 传统写法
Function<String, Integer> f1 = s -> s.length();
// var 写法:Lambda 没有自带类型,必须显式转型后才能推断
var f2 = (Function<String, Integer>) s -> s.length();
System.out.println(f1.apply("abcd"));
System.out.println(f2.apply("abcd"));
}
}
// 输出:
// 4
// 4
注意第二段代码:Lambda 本身没有类型,所以 var f2 = s -> ... 会编译失败,必须显式加 (Function<String, Integer>) 转型才能用 var。这说明 var 只是"帮你写类型",不是"替你猜类型"。
3. var 的限制
var 只能用于局部变量 (方法内部、for 循环内、try-with-resources 中),并且必须有初始值,因为类型要从初始值推断。
java
public class VarLimit {
public static void main(String[] args) {
// ✅ 正确:有初始值
var a = 10;
// ❌ 错误:没有初始值,无法推断
// var b;
// b = 10;
// ✅ 正确:普通 for 循环里可以用
for (var i = 0; i < 3; i++) {
System.out.print(i + " ");
}
System.out.println();
// ✅ 正确:for-each 增强循环里可以用
for (var s : new String[]{"x", "y"}) {
System.out.print(s + " ");
}
System.out.println();
}
}
// 输出:
// 0 1 2
// x y
java
public class VarError {
// ❌ 错误:var 不能用于成员变量
// var field = 1;
// ❌ 错误:var 不能用于方法参数
// void test(var x) { }
// ❌ 错误:var 不能用于方法返回值
// var test() { return 1; }
public static void main(String[] args) {
System.out.println("以上三处若取消注释,均会编译报错");
}
}
// 输出:
// 以上三处若取消注释,均会编译报错
java
// var 的更多合法与非法场景
import java.io.BufferedReader;
import java.io.StringReader;
public class VarMore {
public static void main(String[] args) throws Exception {
// ✅ var 用于 try-with-resources
try (var br = new BufferedReader(new StringReader("hello"))) {
System.out.println("读到:" + br.readLine());
}
// ✅ var 用于 for-each
for (var c : "abc".toCharArray()) {
System.out.print(c + " ");
}
System.out.println();
// ❌ 错误:var 不能推断 null 字面量(不知道是哪种类型)
// var n = null;
// 报错:cannot infer type for local variable n
// ✅ 正确写法:先显式声明类型,再赋 null
String s = null;
System.out.println("s = " + s);
}
}
// 输出:
// 读到:hello
// a b c
// s = null
为什么 var 只能用于局部变量?因为局部变量的类型"一眼就能从等号右边看出来",省掉左边类型不影响可读性;而成员变量、方法签名是类的"公共接口",写清楚类型才能让别人(和 IDE)看懂,所以语言设计上直接禁止。为什么不能推断 null?因为 null 没有类型信息,编译器不知道是 String、Integer 还是别的什么。
二、String 新方法:字符串处理的"瑞士军刀"
JDK 11 给 String 增加了一批实用方法,最常用的四个:isBlank、strip、repeat、lines。
java
public class StringNewMethods {
public static void main(String[] args) {
// isBlank:字符串是否"空白"(空串、空格、换行都算)
System.out.println("\"\".isBlank() = " + "".isBlank());
System.out.println("\" \".isBlank() = " + " ".isBlank());
System.out.println("\"\\n\".isBlank() = " + "\n".isBlank());
System.out.println("\"abc\".isBlank() = " + "abc".isBlank());
// strip:去掉首尾空白(比 trim 更强,能处理全角空格等 Unicode 空白)
System.out.println("\" hi \".strip() = [" + " hi ".strip() + "]");
// repeat:重复 n 次
System.out.println("\"ha\".repeat(3) = " + "ha".repeat(3));
// lines:按行拆分,返回 Stream
"第一行\n第二行\n第三行".lines().forEach(System.out::println);
}
}
// 输出:
// "".isBlank() = true
// " ".isBlank() = true
// "\n".isBlank() = true
// "abc".isBlank() = false
// " hi ".strip() = [hi]
// "ha".repeat(3) = hahaha
// 第一行
// 第二行
// 第三行
strip vs trim:全角空格见分晓
trim() 只能去掉 ASCII 空白(空格、制表符等),strip() 能去掉所有 Unicode 空白(含全角空格),所以新代码优先用 strip。
java
public class StripVsTrim {
public static void main(String[] args) {
String fullWidth = "\u3000你好\u3000"; // \u3000 是全角空格,属于 Unicode 空白
// trim 只认 ASCII 空白,全角空格去不掉
System.out.println("trim 后:[" + fullWidth.trim() + "]");
// strip 认识所有 Unicode 空白
System.out.println("strip 后:[" + fullWidth.strip() + "]");
// 只去一边:stripLeading / stripTrailing
String s = " hi ";
System.out.println("stripLeading:[" + s.stripLeading() + "]");
System.out.println("stripTrailing:[" + s.stripTrailing() + "]");
}
}
// 输出:
// trim 后:[ 你好 ]
// strip 后:[你好]
// stripLeading:[hi ]
// stripTrailing:[ hi]
repeat 的边界情况:0 次、1 次、负数
java
public class RepeatEdge {
public static void main(String[] args) {
System.out.println("\"ab\".repeat(0) = [" + "ab".repeat(0) + "]"); // 空串
System.out.println("\"ab\".repeat(1) = " + "ab".repeat(1));
System.out.println("\"ab\".repeat(3) = " + "ab".repeat(3));
try {
"ab".repeat(-1); // 负数次数直接抛异常
} catch (IllegalArgumentException e) {
System.out.println("负数抛异常:" + e.getMessage());
}
}
}
// 输出:
// "ab".repeat(0) = []
// "ab".repeat(1) = ab
// "ab".repeat(3) = ababab
// 负数抛异常:count is negative: -1
lines 的边界情况:空串、结尾换行、连续换行
java
public class LinesEdge {
public static void main(String[] args) {
// 空字符串:0 行
System.out.println("\"\".lines() 行数:" + "".lines().count());
// 结尾带换行:不会多出一行空行
System.out.println("\"a\\n\".lines() 行数:" + "a\n".lines().count());
// 连续换行:中间的空行会保留
System.out.println("\"a\\n\\nb\".lines() 行数:" + "a\n\nb".lines().count());
// 兼容 \r\n 换行
"a\r\nb".lines().forEach(System.out::println);
}
}
// 输出:
// "".lines() 行数:0
// "a\n".lines() 行数:1
// "a\n\nb".lines() 行数:3
// a
// b
java
// 实战:解析多行文本,过滤空行
public class StringLinesCase {
public static void main(String[] args) {
String config = "host=localhost\n\nport=8080\n";
// lines() 拆行 → filter 去掉空白行 → forEach 打印
config.lines()
.filter(line -> !line.isBlank())
.forEach(System.out::println);
}
}
// 输出:
// host=localhost
// port=8080
这些方法都需要 JDK 11/17 才能运行,JDK 8 里写 "".isBlank() 会直接编译报错。JDK 8 的替代方案:判空白用 s.trim().isEmpty(),去空白用 s.trim() 加正则 replaceAll("^\\s+|\\s+$", ""),重复用 for 循环拼接,拆行用 s.split("\\n")。
三、Files.readString / writeString:一行读写小文件
JDK 11 之前,读一个小文件要开流、建缓冲、逐行读、关流,十几行样板代码;JDK 11 一行搞定。
java
import java.nio.file.Files;
import java.nio.file.Path;
public class FileReadWrite {
public static void main(String[] args) throws Exception {
// writeString:把字符串直接写入文件
Files.writeString(Path.of("note.txt"), "你好,JDK 11!");
// readString:把文件内容直接读成字符串
String content = Files.readString(Path.of("note.txt"));
System.out.println("读到的内容:" + content);
}
}
// 输出:
// 读到的内容:你好,JDK 11!
java
// 更多用法:指定字符集、追加模式、读字节、读不存在的文件
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class FileMore {
public static void main(String[] args) throws Exception {
Path p = Path.of("log.txt");
// 指定字符集读写(默认 UTF-8)
Files.writeString(p, "你好", StandardCharsets.UTF_8);
String s = Files.readString(p, StandardCharsets.UTF_8);
System.out.println("读取:" + s);
// 追加模式:不覆盖原内容
Files.writeString(p, "\n追加的一行", StandardCharsets.UTF_8, StandardOpenOption.APPEND);
System.out.println("追加后:" + Files.readString(p));
// readAllBytes:读成字节数组(适合二进制文件)
byte[] bytes = Files.readAllBytes(p);
System.out.println("字节数:" + bytes.length);
// 边界:读不存在的文件抛 NoSuchFileException
try {
Files.readString(Path.of("not-exist.txt"));
} catch (java.nio.file.NoSuchFileException e) {
System.out.println("捕获 NoSuchFileException:" + e.getFile());
}
Files.deleteIfExists(p);
}
}
// 输出:
// 读取:你好
// 追加后:你好
// 追加的一行
// 字节数:22
// 捕获 NoSuchFileException:not-exist.txt
java
// 对比:JDK 8 时代读文件的经典写法(JDK 11 依然兼容)
import java.io.BufferedReader;
import java.io.FileReader;
public class OldReadFile {
public static void main(String[] args) throws Exception {
StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new FileReader("note.txt"))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
}
System.out.println("JDK 8 写法读到的内容:" + sb);
}
}
// 输出:
// JDK 8 写法读到的内容:你好,JDK 11!
注意:Files.readString/writeString、Path.of 都需 JDK 11/17 才能运行,JDK 8 必须用 BufferedReader 或 Files.readAllLines(JDK 8 已有)等旧 API。小文件用新 API,大文件(几百 MB 以上)仍建议流式处理,避免一次性载入内存。
四、HttpClient:现代 HTTP 请求 API
HttpClient 在 JDK 11 正式转正(需 JDK 11/17 才能运行)。它支持 HTTP/1.1 和 HTTP/2、同步/异步两种调用方式,写法比 JDK 8 时代的 HttpURLConnection 现代得多。
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
// 创建客户端
HttpClient client = HttpClient.newHttpClient();
// 构造 GET 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com"))
.GET()
.build();
// 同步发送,body 按字符串接收
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码:" + response.statusCode());
System.out.println("响应长度:" + response.body().length());
}
}
// 输出(需要联网,状态码视目标网站而定):
// 状态码:200
// 响应长度:2381
POST 请求、超时、重定向
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientPost {
public static void main(String[] args) throws Exception {
// 自定义客户端:连接超时 5 秒、跟随重定向
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// POST 请求:带 JSON 请求体、自定义头、整体超时
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"张三\"}"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("状态码:" + response.statusCode());
System.out.println("响应体长度:" + response.body().length());
}
}
// 输出(需要联网,状态码视目标网站而定):
// 状态码:200
// 响应体长度:529
边界情况:404 不算异常,连不上才是异常
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class HttpClientEdge {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
// 情况一:访问不存在的路径,返回状态码 404------这是"正常响应",不抛异常
HttpRequest r404 = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/status/404"))
.GET().build();
HttpResponse<String> resp404 = client.send(r404, HttpResponse.BodyHandlers.ofString());
System.out.println("404 请求的状态码:" + resp404.statusCode());
// 情况二:目标地址根本连不上,抛 ConnectException
try {
HttpRequest bad = HttpRequest.newBuilder()
.uri(URI.create("http://127.0.0.1:9/")) // 本机 9 端口没有服务
.timeout(Duration.ofSeconds(3))
.GET().build();
client.send(bad, HttpResponse.BodyHandlers.ofString());
} catch (java.net.ConnectException e) {
System.out.println("捕获连接异常:" + e.getMessage());
}
}
}
// 输出:
// 404 请求的状态码:404
// 捕获连接异常:Connection refused
异步请求:不阻塞主线程
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class HttpClientAsync {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com"))
.GET().build();
// sendAsync:立即返回 CompletableFuture,不阻塞当前线程
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
// 主线程可以继续干别的事
System.out.println("请求已发出,主线程继续干活");
// join():阻塞等待结果
HttpResponse<String> response = future.join();
System.out.println("异步状态码:" + response.statusCode());
}
}
// 输出(顺序不保证,"主线程继续干活"一般先打印):
// 请求已发出,主线程继续干活
// 异步状态码:200
java
// JDK 8 时代只能用 HttpURLConnection,写法繁琐
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class OldHttp {
public static void main(String[] args) throws Exception {
HttpURLConnection conn = (HttpURLConnection)
new URL("https://www.baidu.com").openConnection();
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
System.out.println("状态码:" + code);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
System.out.println("读取完成");
}
conn.disconnect();
}
}
// 输出:
// 状态码:200
// 读取完成
HttpClient 相比 HttpURLConnection 的优势:支持 HTTP/2、异步发送(sendAsync 返回 CompletableFuture)、链式构建请求、统一的超时设置。日常开发如果只是简单 GET,两者都能用;涉及连接池、异步、HTTP/2 等复杂场景,优先 HttpClient。
五、java Hello.java:单文件直接运行
JDK 11 起,可以直接 java 文件名.java 运行单个源码文件,省去 javac 编译这一步。这是学习阶段和写小工具的福音。
java
// Hello.java ------ 直接 java Hello.java 张三 即可运行(需 JDK 11/17)
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, JDK 11!");
// 把命令行参数原样打印出来
for (String arg : args) {
System.out.println("参数:" + arg);
}
}
}
// 输出:
// Hello, JDK 11!
// 参数:张三
bash
# JDK 11/17 直接运行(无需 javac),还可以带命令行参数
java Hello.java 张三
# 输出:
# Hello, JDK 11!
# 参数:张三
# JDK 8 必须分两步
javac Hello.java
java Hello 张三
# 输出:
# Hello, JDK 11!
# 参数:张三
注意事项:这种"源码启动模式"只适用于单个源文件,且要求文件名与公开类名一致、不能带 package 声明(带 package 会报错),也不能依赖其他未编译的类文件;大型项目仍然要用 javac + 构建工具(Maven/Gradle)编译。
六、toArray 函数式写法与 Lambda 参数 var
JDK 11 还带来两个"小确幸":给 Collection.toArray 增加了函数式重载,以及允许在 Lambda 参数上使用 var。
java
import java.util.Arrays;
import java.util.List;
public class ToArrayDemo {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "c"); // List.of 需 JDK 11/17 才能运行
// JDK 8 及以前:传入指定大小的数组
String[] arr1 = list.toArray(new String[0]);
System.out.println("旧写法:" + Arrays.toString(arr1));
// JDK 11:传入"生成数组"的函数(需 JDK 11/17 才能运行)
String[] arr2 = list.toArray(String[]::new);
System.out.println("新写法:" + Arrays.toString(arr2));
// 边界:空列表也能转,得到长度为 0 的数组
String[] empty = List.of().toArray(String[]::new);
System.out.println("空列表转数组长度:" + empty.length);
}
}
// 输出:
// 旧写法:[a, b, c]
// 新写法:[a, b, c]
// 空列表转数组长度:0
java
// Lambda 参数使用 var(需 JDK 11/17 才能运行)
import java.util.List;
public class VarInLambda {
public static void main(String[] args) {
List<String> words = List.of("java", "lambda"); // List.of 需 JDK 11/17 才能运行
// 给 Lambda 参数显式标注 var
words.forEach((var w) -> System.out.println("单词:" + w));
// 注意:var 是可选的,但一旦用就要全用
// ❌ 错误写法:words.forEach((var w, x) -> ...); x 没加 var 会报错
}
}
// 输出:
// 单词:java
// 单词:lambda
这两个特性的价值在于"统一风格":Lambda 参数加 var 后,可以像普通变量一样加注解(如 (@NotNull var x) -> ...),这在框架开发中很实用。
扩展知识
1. var 与泛型推断的坑
var list = new ArrayList<String>() 能推断出带泛型的完整类型;但如果等号右边是 new ArrayList<>()(钻石操作符,泛型留空),推断结果会是 ArrayList<Object>,可能和你想要的不同,注意显式写泛型。
java
import java.util.ArrayList;
public class VarGeneric {
public static void main(String[] args) {
// ✅ 显式泛型:list 是 ArrayList<String>,只能装字符串
var list = new ArrayList<String>();
list.add("ok");
// ⚠️ 钻石操作符 + var:推断结果是 ArrayList<Object>,什么都能装
var list2 = new ArrayList<>();
list2.add("hello"); // 能加字符串
list2.add(123); // 也能加整数------类型太宽,容易埋雷
System.out.println(list2);
// ❌ 想当成 String 列表用,编译直接报错
// String s = list2.get(0);
}
}
// 输出:
// [hello, 123]
2. 多行字符串的替代品:JDK 17 文本块
JDK 11 还没有文本块(Text Block,三引号多行字符串),它要到 JDK 17 才正式可用(需 JDK 17 才能运行)。JDK 11 里写多行字符串仍要用 \n 拼接或 String.join。
java
public class TextBlock {
public static void main(String[] args) {
// JDK 17 文本块(三引号,需 JDK 17 才能运行)
String json = """
{
"name": "张三",
"age": 18
}
""";
System.out.println(json);
// JDK 11 的替代写法:用 \n 拼接
String old = "{\n \"name\": \"张三\",\n \"age\": 18\n}\n";
System.out.println("JDK 11 拼接写法:" + old);
}
}
// 输出:
// {
// "name": "张三",
// "age": 18
// }
// JDK 11 拼接写法:{
// "name": "张三",
// "age": 18
// }
3. Files 的其他便捷方法
除了 readString/writeString,JDK 11 的 Files 还有 readAllBytes(读字节数组)、writeString(Path, CharSequence, OpenOption...)(支持追加模式)等,配合 Path.of 使用非常顺手。JDK 8 时代的 readAllLines、newBufferedReader 依然可用,各有适用场景:
java
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class FilesMore {
public static void main(String[] args) throws Exception {
Path p = Path.of("users.txt");
Files.writeString(p, "张三\n李四\n王五");
// readAllLines:一次性读出所有行(JDK 8 就有)
List<String> lines = Files.readAllLines(p);
System.out.println("readAllLines:" + lines);
// readAllBytes:读成字节数组(适合二进制文件)
byte[] bytes = Files.readAllBytes(p);
System.out.println("readAllBytes 字节数:" + bytes.length);
// newBufferedReader:流式逐行读(大文件首选,不会一次性载入内存)
try (var br = Files.newBufferedReader(p)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("流式读到:" + line);
}
}
Files.deleteIfExists(p);
}
}
// 输出:
// readAllLines:[张三, 李四, 王五]
// readAllBytes 字节数:20
// 流式读到:张三
// 流式读到:李四
// 流式读到:王五
4. HttpClient 的异步请求:并发发多个请求
client.sendAsync(request, handler) 返回 CompletableFuture(异步结果容器,后续章节会详细讲),不阻塞当前线程,适合高并发场景;用 .join() 或回调获取结果。把多个异步请求组合起来,就是"并发请求":
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class HttpAsyncMany {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
List<String> urls = Arrays.asList(
"https://www.baidu.com",
"https://www.qq.com",
"https://www.163.com");
// 并发发起 3 个请求,每个请求完成后取出状态码
List<CompletableFuture<Integer>> futures = urls.stream()
.map(url -> client.sendAsync(
HttpRequest.newBuilder().uri(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::statusCode))
.collect(Collectors.toList());
// 全部等待完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
futures.forEach(f -> System.out.println("状态码:" + f.join()));
}
}
// 输出(需要联网,状态码视目标网站而定):
// 状态码:200
// 状态码:200
// 状态码:200
5. Optional.isEmpty:JDK 11 补上的"另一半"
JDK 8 的 Optional 只有 isPresent,判断"为空"要写 !isPresent(),别扭且容易看错。JDK 11 补上了 isEmpty(需 JDK 11/17 才能运行):
java
import java.util.Optional;
public class OptionalIsEmpty {
public static void main(String[] args) {
Optional<String> present = Optional.of("有值");
Optional<String> empty = Optional.empty();
// isEmpty:JDK 11 新增(需 JDK 11/17 才能运行)
System.out.println("present.isEmpty() = " + present.isEmpty());
System.out.println("empty.isEmpty() = " + empty.isEmpty());
// JDK 8 只能这样写:
System.out.println("JDK 8 写法 !isPresent() = " + !present.isPresent());
}
}
// 输出:
// present.isEmpty() = false
// empty.isEmpty() = true
// JDK 8 写法 !isPresent() = false
6. 不可变集合:List.of / Set.of / Map.of
JDK 11 里用 List.of、Set.of、Map.of 几行就能建出不可变集合(需 JDK 11/17 才能运行;JDK 8 用 Arrays.asList 或手动 put)。不可变集合更安全:不怕被别处偷偷修改;代价是不能增删改。
java
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ImmutableCollections {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("x", "y");
Map<String, Integer> map = Map.of("k1", 1, "k2", 2);
System.out.println("list = " + list);
System.out.println("set = " + set);
System.out.println("map = " + map);
// 不可变:任何修改都抛 UnsupportedOperationException
try {
list.add("d");
} catch (UnsupportedOperationException e) {
System.out.println("List.of 不能 add:" + e);
}
// 不允许 null 元素
try {
List.of("a", null);
} catch (NullPointerException e) {
System.out.println("List.of 不允许 null");
}
// Set.of 不允许重复元素
try {
Set.of("a", "a");
} catch (IllegalArgumentException e) {
System.out.println("Set.of 不允许重复元素");
}
}
}
// 输出(Set/Map 的打印顺序不保证):
// list = [a, b, c]
// set = [x, y]
// map = {k2=2, k1=1}
// List.of 不能 add:java.lang.UnsupportedOperationException
// List.of 不允许 null
// Set.of 不允许重复元素
7. Predicate.not:流式代码的"取反神器"
配合第 8 章的 Stream,JDK 11 的 Predicate.not 让取反判断不用再写 !(需 JDK 11/17 才能运行,JDK 8 无此方法):
java
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateNot {
public static void main(String[] args) {
List<String> words = Arrays.asList("", "hello", " ", "world", "");
// 传统写法:! 取反
List<String> r1 = words.stream()
.filter(s -> !s.isBlank())
.collect(Collectors.toList());
System.out.println("取反写法:" + r1);
// JDK 11 的 Predicate.not:语义更清晰
Predicate<String> notBlank = Predicate.not(String::isBlank);
List<String> r2 = words.stream()
.filter(notBlank)
.collect(Collectors.toList());
System.out.println("Predicate.not 写法:" + r2);
}
}
// 输出:
// 取反写法:[hello, world]
// Predicate.not 写法:[hello, world]
8. InputStream.transferTo:一行复制流
复制输入流到输出流,JDK 8 要手写循环读缓冲;JDK 11 一行搞定(需 JDK 11/17 才能运行):
java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class TransferToDemo {
public static void main(String[] args) throws Exception {
String data = "要复制的数据";
ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes());
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 把输入流内容直接复制到输出流,返回复制的字节数
long copied = in.transferTo(out);
System.out.println("复制字节数:" + copied);
System.out.println("复制结果:" + out.toString());
}
}
// 输出:
// 复制字节数:18
// 复制结果:要复制的数据
易错点
- ❌ 在 JDK 8 项目里写
var、isBlank()、Files.readString,编译直接报错。
✅ 这些是 JDK 11/17 的语法/API(需 JDK 11/17 才能运行),JDK 8 只能用传统写法;迁移项目时先确认编译环境。 - ❌ 把 var 当成"动态类型",以为
var x = 1; x = "abc"可行。
✅ var 的类型在编译期就固定为 int,再赋字符串会编译报错;它只是"省写类型",不是动态语言里的 var。 - ❌
var x;不赋初值就使用。
✅ var 必须有初始值,否则无法推断类型,编译报错:cannot infer type for local variable。 - ❌ 把 var 用在成员变量、方法参数、返回值上。
✅ var 只能用于局部变量(含 for 循环、try-with-resources),其余位置编译报错。 - ❌ 滥用 var 导致可读性下降,如
var r = service.getResult();看不出返回类型。
✅ 当右边表达式能"一眼看出类型"(如var s = "hi"、var list = new ArrayList<String>())时才用 var;类型不明显时显式写类型。 - ❌ 以为
" abc ".trim()和strip()完全一样。
✅ strip 能去掉 Unicode 空白(含全角空格),trim 只去 ASCII 空白;需要严格处理全角空格时用 strip。 - ❌ 用
readString读超大文件,直接把内存打爆。
✅ readString 适合小文件;大文件应使用 Files.newBufferedReader 流式读取。 - ❌ 往
List.of/Set.of里塞 null 或重复元素:List.of 放 null 抛 NullPointerException,Set.of 放重复元素抛 IllegalArgumentException。
✅ 不可变集合对数据有严格要求,不确定数据时用 Arrays.asList 或先做去重/判空。 - ❌ 写
var x = null;想当然以为能推断出类型。
✅ null 没有类型信息,var 无法推断,编译失败;必须显式写类型String x = null;。 - ❌ 以为
java Hello.java能跑多文件、带包的项目。
✅ 源码启动模式只支持单个无 package 的源文件;多文件项目仍要 javac(或 Maven/Gradle)编译。
JDK 8 / 11 / 17 版本调用方式
- 本章所有特性均为 JDK 11 引入,JDK 8 没有对应写法 :JDK 8 写
var、isBlank()、Files.readString、HttpClient、java 单文件运行均无法编译/运行,必须使用传统替代写法(显式类型、trim 加正则、BufferedReader、HttpURLConnection、先 javac 再 java)。 - JDK 17 完全支持本章全部特性,用法与 JDK 11 一致,无需任何修改。
- 版本间的小差异:
List.of/Set.of/Map.of需 JDK 11/17 才能运行(JDK 8 用 Arrays.asList 或手动 put),且是不可变集合;Stream.toList()是 JDK 17 才有的(需 JDK 17 才能运行),JDK 11 仍要用collect(Collectors.toList())。 - 关于 var:var 是随 JDK 11 这个 LTS 版本普及开来的写法,JDK 11/17 用法完全一致;JDK 8 完全无法使用。
- 本章示例中的
Path.of、java.net.http包、String.lines()等 API 在 JDK 11/17 中一致,JDK 8 均无对应实现。 - JDK 11 专属的小工具:
Optional.isEmpty()、Predicate.not、InputStream.transferTo、String.stripLeading/stripTrailing都需 JDK 11/17 才能运行,JDK 8 无替代 API,只能手写。 - 多行字符串文本块(三引号)是 JDK 17 的特性(需 JDK 17 才能运行),JDK 11 只能用
\n拼接或 String.join;把项目从 JDK 11 升到 JDK 17 时才能享受该语法。
小结

- var 是局部变量类型推断:类型由初始值推断,编译期确定,运行期无变化。
- var 的限制:只能用于局部变量、必须有初始值、不能推断 null;成员变量、参数、返回值禁止使用。
- String 新方法:isBlank 判空白、strip 去 Unicode 空白、repeat 重复、lines 按行拆分,全部需 JDK 11/17 才能运行。
- Files.readString/writeString 一行读写小文件,支持字符集与追加模式,大文件仍用流式。
- HttpClient 是 JDK 11 正式转正的现代 HTTP 客户端,支持 HTTP/2、同步/异步、超时与重定向。
java Hello.java源码单文件直接运行,省去 javac;多文件、带包项目仍需编译。toArray(String[]::new)与 Lambda 参数 var 是 JDK 11 的细节增强。- 本章全部特性 JDK 8 均不支持,迁移或混用版本时务必确认编译环境。
- 不可变集合(List.of/Set.of/Map.of)与新小工具(isEmpty、Predicate.not、transferTo)让代码更安全更短,但要注意它们的"禁区"(null、重复、不可变)。
- 升级路径建议:JDK 8 → 11 先做语法/API 兼容性扫描;JDK 11 → 17 可享受文本块、Stream.toList 等新语法,但同样要先验证三方库兼容性。