第9章 JDK 11 新特性:var 与便捷新 API

第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/writeStringPath.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 时代的 readAllLinesnewBufferedReader 依然可用,各有适用场景:

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.ofSet.ofMap.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
// 复制结果:要复制的数据

易错点

  1. ❌ 在 JDK 8 项目里写 varisBlank()Files.readString,编译直接报错。
    ✅ 这些是 JDK 11/17 的语法/API(需 JDK 11/17 才能运行),JDK 8 只能用传统写法;迁移项目时先确认编译环境。
  2. ❌ 把 var 当成"动态类型",以为 var x = 1; x = "abc" 可行。
    ✅ var 的类型在编译期就固定为 int,再赋字符串会编译报错;它只是"省写类型",不是动态语言里的 var。
  3. var x; 不赋初值就使用。
    ✅ var 必须有初始值,否则无法推断类型,编译报错:cannot infer type for local variable。
  4. ❌ 把 var 用在成员变量、方法参数、返回值上。
    ✅ var 只能用于局部变量(含 for 循环、try-with-resources),其余位置编译报错。
  5. ❌ 滥用 var 导致可读性下降,如 var r = service.getResult(); 看不出返回类型。
    ✅ 当右边表达式能"一眼看出类型"(如 var s = "hi"var list = new ArrayList<String>())时才用 var;类型不明显时显式写类型。
  6. ❌ 以为 " abc ".trim()strip() 完全一样。
    ✅ strip 能去掉 Unicode 空白(含全角空格),trim 只去 ASCII 空白;需要严格处理全角空格时用 strip。
  7. ❌ 用 readString 读超大文件,直接把内存打爆。
    ✅ readString 适合小文件;大文件应使用 Files.newBufferedReader 流式读取。
  8. ❌ 往 List.of/Set.of 里塞 null 或重复元素:List.of 放 null 抛 NullPointerException,Set.of 放重复元素抛 IllegalArgumentException。
    ✅ 不可变集合对数据有严格要求,不确定数据时用 Arrays.asList 或先做去重/判空。
  9. ❌ 写 var x = null; 想当然以为能推断出类型。
    ✅ null 没有类型信息,var 无法推断,编译失败;必须显式写类型 String x = null;
  10. ❌ 以为 java Hello.java 能跑多文件、带包的项目。
    ✅ 源码启动模式只支持单个无 package 的源文件;多文件项目仍要 javac(或 Maven/Gradle)编译。

JDK 8 / 11 / 17 版本调用方式

  1. 本章所有特性均为 JDK 11 引入,JDK 8 没有对应写法 :JDK 8 写 varisBlank()Files.readStringHttpClientjava 单文件运行 均无法编译/运行,必须使用传统替代写法(显式类型、trim 加正则、BufferedReader、HttpURLConnection、先 javac 再 java)。
  2. JDK 17 完全支持本章全部特性,用法与 JDK 11 一致,无需任何修改。
  3. 版本间的小差异: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())
  4. 关于 var:var 是随 JDK 11 这个 LTS 版本普及开来的写法,JDK 11/17 用法完全一致;JDK 8 完全无法使用。
  5. 本章示例中的 Path.ofjava.net.http 包、String.lines() 等 API 在 JDK 11/17 中一致,JDK 8 均无对应实现。
  6. JDK 11 专属的小工具:Optional.isEmpty()Predicate.notInputStream.transferToString.stripLeading/stripTrailing 都需 JDK 11/17 才能运行,JDK 8 无替代 API,只能手写。
  7. 多行字符串文本块(三引号)是 JDK 17 的特性(需 JDK 17 才能运行),JDK 11 只能用 \n 拼接或 String.join;把项目从 JDK 11 升到 JDK 17 时才能享受该语法。

小结

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

下一篇:第10章 JDK 17 新特性:语法糖与新能力

相关推荐
hoho_1233 分钟前
麒麟V10升级nginx最新版本到1.31.4
java·服务器·nginx
冬夜戏雪33 分钟前
笔试的Scanner in Scanner out
java·开发语言
南城以南溫暖如初14738 分钟前
社交裂变商城系统设计与实战开发指南
java·spring boot·mysql·vue·mybatis
Zane19941241 分钟前
HashSet、TreeSet、LinkedHashSet:底层都是“套壳“
java·开发语言
xiaoqiMikko43 分钟前
Spring 官方给 5.3 线开了三个修复版,Maven Central 上一个都没有
java·spring boot
南城以南溫暖如初1471 小时前
分享返现商城系统开发实战:技术架构与运营经验指南
java·spring boot·mysql·架构·vue·mybatis
vipxieliang1 小时前
Java切面编程(AOP)详解:从核心概念到实战应用
java·spring boot
.Hypocritical.2 小时前
SpringBoot 各版本 Profile 多环境配置完整对比(2.x/ 2.4+ / 3.x/4.x
java·spring boot·后端
露天赏雪2 小时前
掘金小册样章
java·开发语言·人工智能·ai编程