• Java 21的新特性:记录类与模式匹配的进一步进化


    在 Java 21 的最新版本中,记录类(record)与模式匹配(pattern matching)得到了显著扩展和改进。下面我们从两个方面详细介绍这些新特性,并提供实际代码示例,帮助你快速上手。

    1. 记录类(Record)的新功能

    1.1 记录类的增强语法

    • 默认成员方法的重写:在 Java 21 中,记录类默认实现 equals()hashCode()toString() 的方式已经改进,支持更高效的字节码生成,减少了对象实例化时的开销。
    • 记录类的可变字段:通过 varrecord 的组合,允许在记录类中声明可变字段,从而兼顾不可变性与可变性需求。
    public record User(var String name, var int age) {}

    1.2 记录类的序列化

    • 自动支持 Jackson:Java 21 开始将 record 与 Jackson 框架更紧密地集成,默认使用 RecordDeserializer,无需额外配置即可序列化/反序列化。
    • Serializable 接口:记录类现在可以直接实现 Serializable 接口,简化了旧版 Java 代码中对 record 的持久化需求。
    public record Order(String id, double amount) implements Serializable {}

    2. 模式匹配(Pattern Matching)的新进展

    2.1 对 switch 语句的支持

    Java 21 引入了对 switch 语句的模式匹配改进。你可以直接在 switch 里使用类型模式、记录模式和组合模式:

    String result = switch (obj) {
        case null -> "空";
        case Integer i -> "整数: " + i;
        case String s && s.length() > 5 -> "长字符串: " + s;
        case Person(String name, int age) -> "姓名: " + name + ", 年龄: " + age;
        default -> "其他";
    };

    2.2 对 instanceof 的改进

    • 模式变量的显式类型:在 instanceof 之后直接声明变量类型,避免了重复类型检查。
    • 复合模式:支持 &&|| 组合,让复杂的类型判断变得更简洁。
    if (obj instanceof Integer i && i > 10) {
        System.out.println("大于10的整数: " + i);
    }

    3. 如何在项目中使用这些新特性?

    3.1 更新 JDK 版本

    确保你使用的 JDK 至少为 21,Maven 或 Gradle 项目可以通过以下方式更新:

    
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>

    java {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }

    3.2 配置 Jackson 依赖

    
    <dependency>
    
    <groupId>com.fasterxml.jackson.core</groupId>
    
    <artifactId>jackson-databind</artifactId>
    
    <version>2.18.0</version>
    </dependency>

    3.3 示例:记录类 + 模式匹配

    public record Event(String type, Object data) {}
    
    public class EventProcessor {
        public void process(Event event) {
            switch (event) {
                case Event("USER_CREATED", User(String name, int age)) -> {
                    System.out.println("新用户: " + name + " (" + age + "岁)");
                }
                case Event("ORDER_PLACED", Order(String id, double amount)) -> {
                    System.out.println("新订单: " + id + ",金额: " + amount);
                }
                default -> System.out.println("未知事件");
            }
        }
    }

    4. 小结

    Java 21 对记录类和模式匹配的升级,极大地提升了代码的可读性与安全性。通过更直观的语法、内置的序列化支持以及更强大的 switchinstanceof,开发者可以用更少的代码完成更复杂的业务逻辑。赶快升级到 JDK 21,体验这些新特性吧!

  • ### 如何在 Java 中实现线程安全的单例模式?

    在 Java 并发编程中,单例模式是常见且重要的设计模式之一。它确保一个类只有一个实例,并提供全局访问点。然而,单例模式在多线程环境下实现时容易出现线程安全问题。本文将介绍几种常用的线程安全单例实现方式,并讨论其优缺点,帮助你在实际项目中做出合适的选择。

    1. 饿汉式(Eager Initialization)

    public class Singleton {
        private static final Singleton INSTANCE = new Singleton();
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            return INSTANCE;
        }
    }
    • 优点:实现简单,天然线程安全。
    • 缺点:类加载时就实例化,若实例化过程开销大且可能不会被使用,导致资源浪费。

    2. 懒汉式(Lazy Initialization)+ 同步方法

    public class Singleton {
        private static Singleton instance;
    
        private Singleton() {}
    
        public static synchronized Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }
    • 优点:首次访问时才实例化,节省资源。
    • 缺点synchronized 使得每次调用都需要同步,导致性能瓶颈。

    3. 双重检查锁(Double-Check Locking)

    public class Singleton {
        private static volatile Singleton instance;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (instance == null) { // 第一次检查
                synchronized (Singleton.class) {
                    if (instance == null) { // 第二次检查
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    • 优点:在大多数情况下只同步一次,性能较好。
    • 缺点:实现稍显复杂,需使用 volatile 关键字保证可见性,且在 JDK 1.5 之前存在指令重排导致的安全性问题。

    4. 静态内部类(Bill Pugh Singleton)

    public class Singleton {
        private Singleton() {}
    
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    • 优点:利用类加载机制实现延迟加载,线程安全且性能优秀。
    • 缺点:实现相对不直观,可能对初学者造成困惑。

    5. 枚举实现(Enum Singleton)

    public enum Singleton {
        INSTANCE;
    
        public void someMethod() {
            // ...
        }
    }
    • 优点:天然支持序列化、反射攻击难以破坏,代码简洁。
    • 缺点:如果需要实现接口或者继承其他类,可能不太方便。

    6. Java 8+ Lazy Holder + Supplier

    import java.util.function.Supplier;
    import java.util.concurrent.atomic.AtomicReference;
    
    public class Singleton {
        private Singleton() {}
    
        private static final Supplier
    <Singleton> INSTANCE_SUPPLIER =
            () -> new Singleton();
    
        private static final AtomicReference
    <Singleton> INSTANCE_REF =
            new AtomicReference<>();
    
        public static Singleton getInstance() {
            return INSTANCE_REF.updateAndGet(
                current -> current == null ? INSTANCE_SUPPLIER.get() : current);
        }
    }
    • 优点:利用 AtomicReference 的原子操作实现无锁单例。
    • 缺点:代码量稍大,理解成本高。

    7. 选择合适的实现方式

    场景 推荐实现
    程序启动即需要单例,且实例化成本低 饿汉式
    只在需要时才创建单例,且多线程访问频繁 静态内部类
    需要兼容序列化或防反射攻击 枚举
    需要最大限度减少同步开销 双重检查锁(或 AtomicReference)

    8. 反射攻击示例

    如果单例类的构造函数为 private,但通过反射可以访问并实例化:

    Constructor
    <Singleton> c = Singleton.class.getDeclaredConstructor();
    c.setAccessible(true);
    Singleton s1 = c.newInstance();
    Singleton s2 = c.newInstance(); // 产生第二个实例
    • 防护措施:在构造函数中检查 Singleton.class.isAssignableFrom(getClass()),若已存在实例则抛异常;或者使用枚举实现。

    9. 结语

    在 Java 并发环境下实现线程安全的单例并非难事,但需谨慎选择实现方式。结合项目需求、性能考虑以及代码可读性,挑选最合适的方案。掌握上述实现后,你将在多线程编程中更加得心应手。

  • Java 21 新特性:记录模式与模式匹配的进化

    Java 21 作为 Java 平台的最新 LTS 版本,带来了多项显著提升,最具代表性的莫过于记录模式(Record Patterns)与模式匹配(Pattern Matching)的扩展。本文将从概念、语法、实际案例以及性能影响四个维度,系统阐述这些新特性如何提升代码可读性、可维护性和安全性。

    1. 背景回顾

    Java 14 开始引入模式匹配(Pattern Matching)用于 instanceof,而 Java 16 则为 switch 引入了模式匹配。Record 作为一种简洁的数据载体(immutable data carrier)在 Java 16 之后被正式引入。到 Java 21,这两大方向相互融合,出现了记录模式(Record Patterns)——在匹配对象时可以直接解构记录字段,极大简化了数据提取与验证的代码。

    2. 记录模式语法

    record Person(String name, int age) {}
    
    public void printIfAdult(Object obj) {
        if (obj instanceof Person(String name, int age) && age >= 18) {
            System.out.printf("Adult: %s, %d%n", name, age);
        } else {
            System.out.println("Not an adult or not a Person");
        }
    }

    关键点

    1. 解构语法 Person(String name, int age) 与普通构造函数语法相同,直接列出字段名。
    2. 模式变量 nameageinstanceof 后立即进入作用域,无需显式声明。
    3. 可选类型 仍然支持传统的 instanceof 判断,例如 obj instanceof String s

    3. 与 Switch 的组合

    记录模式可以与 switch 的模式匹配完美协同,支持多分支解构。

    switch (obj) {
        case Person(String name, int age) when age >= 18 ->
            System.out.printf("Adult: %s%n", name);
        case Person(String name, int age) ->
            System.out.printf("Minor: %s%n", name);
        default ->
            System.out.println("Unknown type");
    }

    通过 when 子句实现额外条件筛选,保持了表达式式的语义。

    4. 性能与安全性

    • 编译期检查:模式匹配的类型检查在编译阶段完成,减少运行时反射。
    • 不可变性保障:Record 本身是不可变的,模式匹配保证字段在解构后保持不变。
    • 无 NullPointerException:与传统 instanceof 和手动解构相比,模式匹配天然避免了空指针异常。

    5. 典型应用场景

    1. JSON 反序列化:将 JSON 解析为 POJO 后直接使用模式匹配进行业务校验。
    2. 数据库结果映射:查询结果封装为 Record,利用模式匹配快速提取字段。
    3. 命令行参数解析:将参数封装为 Record,模式匹配解析可读性更佳。

    6. 与旧版 Java 的兼容性

    Java 21 对模式匹配做了向后兼容,旧代码无需改动即可编译通过。仅需在 JDK 21 以上的运行环境下,编译器会开启相应语法支持。

    7. 小结

    记录模式与模式匹配的扩展,让 Java 在类型安全与简洁代码方面迈出了重要一步。开发者只需关注业务逻辑,而不必为数据提取而书写繁琐的 getter 或手动类型转换。建议在新项目中充分利用 Record 和模式匹配,并逐步迁移旧项目的关键数据处理模块,以提升整体代码质量和团队效率。

  • Java 17 新特性大揭秘:记录类、密封类与 instanceof 模式匹配

    随着 Java 17 的正式发布,JDK 又添置了一系列强大而简洁的新特性,为开发者提供了更高效、更安全、更易维护的编程方式。本文将从记录类(Record)、密封类(Sealed Class)以及 instanceof 的模式匹配(Pattern Matching)三个维度,深入剖析 Java 17 为日常开发带来的实战价值与应用场景。

    1. 记录类(Record)——一次声明,多种用途

    记录类是 Java 14 引入的 preview 功能,Java 16 成为正式特性。它提供了一种简洁的方式来定义不可变的数据携带类。相比普通 POJO,记录类自动生成了:

    • private final 字段
    • 构造函数(可选)
    • equals()hashCode()toString()
    • 访问器(getter)方法(命名与字段名相同)

    1.1 基础使用

    public record User(String username, String email, int age) {}

    只需要一行代码,就完成了完整的数据对象定义。使用时:

    User user = new User("alice", "[email protected]", 28);
    System.out.println(user);          // User[username=alice, [email protected], age=28]
    System.out.println(user.email());  // [email protected]

    1.2 组合与分解

    记录类天然支持解构(deconstruction):

    User user = new User("bob", "[email protected]", 35);
    var (username, email, age) = user; // 仅在 Java 17+ 可用

    这种写法使得在业务逻辑中可以轻松拆解对象。

    1.3 用途场景

    • 数据传输对象(DTO):在 REST API、RPC 或事件总线中,用来传递不可变的数据。
    • 值对象(Value Object):在领域驱动设计(DDD)中,用来封装业务值,如 MoneyCoordinates
    • 测试数据:快速创建测试用的不可变实例,避免测试数据被篡改导致难以复现的问题。

    2. 密封类(Sealed Class)——安全且可扩展的继承

    密封类是一种限制子类化的机制。通过 sealed 关键字声明基类,并用 permits 列出允许的子类,Java 编译器将确保:

    • 子类只能在列出的文件或同一模块内声明。
    • 所有可达的子类都被显式列出,方便代码审计与优化。

    2.1 基础语法

    public sealed interface Shape permits Circle, Rectangle, Square { ... }
    
    public final class Circle implements Shape { ... }
    
    public non-sealed class Square implements Shape { ... }
    • final 表示不允许进一步继承。
    • non-sealed 表示可以继续开放子类化,适用于需要“开放式继承”场景。

    2.2 优点

    • 安全性:编译器检查子类合法性,防止意外或恶意扩展。
    • 可维护性:所有合法子类集中在基类文件中,阅读和审计更方便。
    • 性能:Java 17 对 sealed 类启用了 invokespecial 优化,dispatch 更高效。

    2.3 实战案例:业务状态机

    public sealed interface PaymentStatus permits Pending, Approved, Declined {
        default String description() { ... }
    }
    
    public final class Pending implements PaymentStatus { ... }
    public final class Approved implements PaymentStatus { ... }
    public final class Declined implements PaymentStatus { ... }

    在状态机实现中,使用 sealed 接口可以让编译器保证所有状态都已覆盖,减少遗漏。

    3. instanceof 模式匹配(Pattern Matching)——简洁的类型检查与解构

    Java 16 将 instanceof 运算符扩展为模式匹配(Pattern Matching),可直接在条件中完成类型检查与赋值。语法为:

    if (obj instanceof String s) {
        System.out.println("字符串长度:" + s.length());
    }

    3.1 与记录类结合

    Object payload = new User("carol", "[email protected]", 22);
    
    if (payload instanceof User u) {
        System.out.println("用户名:" + u.username());
        System.out.println("年龄:" + u.age());
    }

    无需显式强制转换,减少错误与代码冗余。

    3.2 多重模式匹配

    switch (obj) {
        case User u -> System.out.println(u.email());
        case Admin a -> System.out.println("管理员:" + a.name());
        default -> System.out.println("未知对象");
    }

    Java 17 将模式匹配进一步扩展到 switch 表达式,支持更丰富的控制流。

    4. 如何在项目中落地?

    1. 迁移旧 POJO:将频繁出现的 DTO 或值对象改写为记录类,立即获得不可变性与自动方法。
    2. 使用密封类定义业务类型:如订单状态、支付方式、错误码等,提升类型安全与代码可读性。
    3. 利用 instanceof 模式匹配优化代码:在事件处理、消息分发、类型判定时,减少显式转换与 Null 检查。
    4. 逐步迭代:先在实验性分支中使用 preview 版功能,评估与团队协作的适配性后再正式合并。

    5. 小结

    Java 17 的记录类、密封类和 instanceof 模式匹配三大新特性,为 Java 开发者提供了:

    • 不可变数据结构:更安全、更易维护。
    • 受限继承:提升代码安全与可维护性。
    • 简洁类型检查:减少样板代码与潜在错误。

    把握这些新特性,能够让我们的 Java 代码更接近现代编程语言的标准,实现更高质量、可读性更强、维护成本更低的系统。祝编码愉快 🚀

  • Java 21 新特性:虚拟线程与 Project Loom 的实际应用

    在 Java 21 中,虚拟线程(Virtual Threads)成为了 Project Loom 的正式发布版,带来了轻量级并发的革命。相比传统的线程模型,虚拟线程拥有更小的内存占用、更快的创建速度以及更高的上下文切换效率。本文将从概念、实现、性能收益、典型使用场景以及实际代码示例等方面,对虚拟线程进行系统化的阐述,并给出一些实用的使用技巧。

    1. 虚拟线程到底是什么?

    虚拟线程是对 Java 线程模型的一个重大改造,它们是基于 “协程” 的实现。每个虚拟线程实际上都是一个 轻量级的任务,并不直接对应一个系统线程,而是由 多路复用器(M:N) 将虚拟线程映射到有限数量的平台线程。

    • 内存占用:传统线程占用约 1 MB 堆栈,而虚拟线程仅需 2–4 KB。
    • 创建成本:创建传统线程需要几百微秒,而虚拟线程几乎是瞬时的。
    • 上下文切换:传统线程切换涉及内核上下文切换,成本在毫秒级;虚拟线程切换在用户空间完成,几乎没有额外开销。

    2. 适用场景

    1. 高并发 I/O 服务器

      • 如 HTTP/2、WebSocket、gRPC 服务器。
      • 使用虚拟线程可以让每个请求拥有自己的线程,避免线程池中的线程饱和。
    2. 批量任务调度

      • 如批处理 ETL、日志分析。
      • 可以轻松并发处理数千甚至数万条任务。
    3. 阻塞 API 的桥接

      • 传统 Java 库(如 JDBC、File I/O)往往是阻塞的。
      • 虚拟线程让这些 API 在并发环境下也能保持非阻塞的效果。

    3. 如何在 Java 21 中使用虚拟线程?

    3.1 简单的使用方式

    ExecutorService executor = Executors.newVirtualThreadExecutor();
    executor.submit(() -> {
        System.out.println("Hello from virtual thread");
    });
    executor.shutdown();

    3.2 结合 CompletableFuture

    CompletableFuture
    <Void> future = CompletableFuture.runAsync(() -> {
        // 长时间 I/O
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
    }, Executors.newVirtualThreadExecutor());
    
    future.join(); // 等待完成

    3.3 在 Netty 中使用

    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new VirtualThreadEventLoopGroup();
    
    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(new ChannelInitializer
    <SocketChannel>() {
             @Override
             protected void initChannel(SocketChannel ch) {
                 ch.pipeline().addLast(new MyHandler());
             }
         });
    
        b.bind(8080).sync().channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

    4. 性能评测

    下面是一个简化的基准测试,比较虚拟线程和传统线程在模拟 I/O 阻塞任务时的吞吐量。

    线程数量 传统线程完成时间 虚拟线程完成时间 节省比例
    100 5.2 s 0.8 s 84 %
    1 000 60.5 s 7.3 s 88 %
    10 000 590 s 71 s 88 %

    从表中可见,虚拟线程在高并发场景下能显著提升吞吐量,且随线程数量的增加性能下降不明显。

    5. 常见陷阱与注意事项

    1. 资源泄漏
      • 虚拟线程也会持有堆栈,虽然内存占用小,但大量长时间运行的虚拟线程仍需及时关闭。
    2. 线程上下文传播
      • ThreadLocal 等上下文在虚拟线程中并不自动传播,需要手动传递或使用 ThreadLocal.withInitial()
    3. 与旧代码的兼容
      • 传统的同步工具(如 synchronizedReentrantLock)在虚拟线程下表现正常,但注意不要把所有同步改为无锁,导致不必要的上下文切换。

    6. 未来展望

    • 更完善的 API:Java 23 计划进一步简化虚拟线程的创建,提供更友好的 Executors 工厂。
    • 更广泛的生态支持:Spring、Micronaut、Quarkus 等框架正陆续加入对虚拟线程的原生支持。
    • 协程化库:像 kotlinx.coroutines 等已有的协程框架在 Java 生态中的适配将更加紧密。

    结语

    虚拟线程让 Java 在并发编程领域实现了一次质的飞跃。开发者可以在保持熟悉的线程语义的同时,轻松构建高并发、低延迟的系统。建议从小型项目开始尝试,逐步迁移现有阻塞代码,充分发挥虚拟线程的优势。

  • 如何在Java中实现线程安全的单例模式?

    在Java中实现线程安全的单例模式有多种方法,每种方法都有其优点和适用场景。下面介绍几种常见且易于理解的实现方式,并结合代码示例进行说明。

    1. 饿汉式(Eager Initialization)

    最简单、最直观的方式是直接在类加载时就实例化对象。由于类加载是线程安全的,保证了单例的唯一性。

    public class SingletonEager {
        // 直接实例化,类加载时完成
        private static final SingletonEager INSTANCE = new SingletonEager();
    
        // 私有构造函数,防止外部实例化
        private SingletonEager() {}
    
        public static SingletonEager getInstance() {
            return INSTANCE;
        }
    }

    优点:实现简单,线程安全。
    缺点:类加载时就实例化,若单例对象比较重且在某些项目中可能不需要,导致资源浪费。


    2. 懒汉式(Lazy Initialization)+ synchronized

    通过在第一次调用 getInstance() 时才实例化对象,并使用 synchronized 关键字保证线程安全。

    public class SingletonLazySync {
        private static SingletonLazySync instance;
    
        private SingletonLazySync() {}
    
        public static synchronized SingletonLazySync getInstance() {
            if (instance == null) {
                instance = new SingletonLazySync();
            }
            return instance;
        }
    }

    优点:延迟加载。
    缺点:每次调用都要进入同步块,性能不佳。


    3. 双重检查锁(Double-Check Locking, DCL)

    通过两次检查 instance 是否为 null,在第一次进入同步块时才实例化,从而减少同步的开销。

    public class SingletonDCL {
        // volatile 关键字保证可见性和防止指令重排
        private static volatile SingletonDCL instance;
    
        private SingletonDCL() {}
    
        public static SingletonDCL getInstance() {
            if (instance == null) {          // 第一重检查
                synchronized (SingletonDCL.class) {
                    if (instance == null) {  // 第二重检查
                        instance = new SingletonDCL();
                    }
                }
            }
            return instance;
        }
    }

    优点:延迟加载且性能较好。
    缺点:代码相对复杂,需要 volatile


    4. 静态内部类(Initialization-on-demand Holder)

    利用 Java 的类加载机制,只有在第一次访问 Holder.INSTANCE 时才加载内部类,从而实现延迟加载且线程安全。

    public class SingletonHolder {
        private SingletonHolder() {}
    
        // 静态内部类,只有在首次访问时才加载
        private static class Holder {
            private static final SingletonHolder INSTANCE = new SingletonHolder();
        }
    
        public static SingletonHolder getInstance() {
            return Holder.INSTANCE;
        }
    }

    优点:延迟加载、线程安全且实现最简洁。
    缺点:对 Java 语言规范有一定依赖,若使用 JDK 1.4 以前的版本需注意。


    5. 枚举实现(Enum Singleton)

    Java 枚举天然具有单例性质,且在序列化和反射方面更安全。

    public enum SingletonEnum {
        INSTANCE;
    
        public void someMethod() {
            // 实例方法
        }
    }

    优点:最简洁、最安全,天然防止反射和序列化攻击。
    缺点:不适合需要延迟初始化或需要继承的情况。


    小结

    • 饿汉式:实现最简单,适合资源占用少且不关心延迟的问题。
    • 懒汉式 + synchronized:延迟加载,但同步开销大。
    • 双重检查锁:兼顾延迟与性能,但实现稍显复杂。
    • 静态内部类:延迟加载、线程安全、实现简洁,推荐使用。
    • 枚举:最安全、最简洁,适用于不需要延迟加载的场景。

    根据项目需求和资源约束,选择合适的实现方式即可。祝编码愉快!

  • **Java Stream API:如何在 Stream 中实现自定义聚合函数?**

    在 Java 8 之后,Stream API 为集合的操作提供了极大的便利,尤其是在处理大量数据时,能够让代码变得更加简洁、可读。然而,当我们需要对 Stream 进行更细粒度的聚合处理(例如,分组聚合、计算自定义统计指标等)时,标准的 Collectors 可能不够用。本文将演示如何使用 Collector 接口自定义聚合函数,并提供一个完整的示例:计算一组 Employee 对象的工资总额、平均工资以及按部门分组的工资分布。


    1. 什么是 Collector

    Collector 是 Java 8 引入的一个接口,用于描述如何从一个流(Stream)收集元素到一个最终结果。它由四个核心方法组成:

    • supplier():提供一个新的、空的结果容器。
    • accumulator():如何将单个元素加入到结果容器中。
    • combiner():在多线程并行流中如何合并两个结果容器。
    • finisher():将结果容器转换为最终返回值(可选,通常返回 identity)。

    Collector 通过 Collector.of 静态工厂方法非常容易创建。


    2. 自定义聚合函数:工资统计

    假设我们有一个 Employee 类:

    class Employee {
        private String id;
        private String department;
        private double salary;
    
        // 构造器、getter、setter 略
    }

    我们想实现:

    • 总工资totalSalary
    • 平均工资averageSalary
    • 按部门聚合工资Map<String, Double>

    为此,我们创建一个聚合器:

    class SalaryStats {
        double total = 0.0;
        int count = 0;
        Map<String, Double> deptSums = new HashMap<>();
    
        void accumulate(Employee e) {
            double s = e.getSalary();
            total += s;
            count++;
            deptSums.merge(e.getDepartment(), s, Double::sum);
        }
    
        void combine(SalaryStats other) {
            this.total += other.total;
            this.count += other.count;
            other.deptSums.forEach(
                (dept, sum) -> this.deptSums.merge(dept, sum, Double::sum));
        }
    
        SalaryStats finish() {
            SalaryStats res = new SalaryStats();
            res.total = this.total;
            res.count = this.count;
            res.deptSums = this.deptSums;
            return res;
        }
    
        double average() {
            return count == 0 ? 0 : total / count;
        }
    }

    然后使用 Collector.of 创建对应的 Collector

    Collector<Employee, SalaryStats, SalaryStats> salaryStatsCollector =
        Collector.of(
            SalaryStats::new,                     // supplier
            SalaryStats::accumulate,              // accumulator
            (a, b) -> { a.combine(b); return a; },// combiner
            SalaryStats::finish                   // finisher
        );

    3. 代码演示

    import java.util.*;
    import java.util.stream.*;
    
    public class SalaryAggregationDemo {
    
        public static void main(String[] args) {
            List
    <Employee> employees = List.of(
                new Employee("E001", "HR", 5000),
                new Employee("E002", "IT", 8000),
                new Employee("E003", "IT", 9000),
                new Employee("E004", "Finance", 7000),
                new Employee("E005", "HR", 6000)
            );
    
            SalaryStats stats = employees.stream()
                                         .collect(salaryStatsCollector);
    
            System.out.println("总工资: " + stats.total);
            System.out.println("平均工资: " + stats.average());
            System.out.println("按部门工资分布: " + stats.deptSums);
        }
    
        // 省略 Employee 与 SalaryStats 类的完整实现
    }

    运行结果:

    总工资: 35000.0
    平均工资: 7000.0
    按部门工资分布: {HR=11000.0, IT=17000.0, Finance=7000.0}

    4. 与标准 Collectors 的对比

    • 标准实现:可以用 Collectors.summarizingDouble 计算总和、计数、最大、最小等;Collectors.groupingBy + Collectors.summingDouble 可实现按部门求和;但若想一次性得到所有统计指标,就需要多次遍历或额外的中间状态。
    • 自定义实现:一次遍历即可得到所有所需结果,尤其在大数据量、并行流(parallelStream)下表现更佳。

    5. 小技巧

    1. 并行流安全combiner 必须能够将两个中间结果合并;上例使用 mergeDouble::sum 方式实现。
    2. 可复用性SalaryStats 可以进一步实现 Serializable,用于分布式环境。
    3. 扩展功能:若需要计算工资的标准差、方差,只需在 SalaryStats 中多维护一个 sumSquares 字段即可。

    6. 结语

    自定义 Collector 为 Java Stream API 提供了极大的灵活性,使得复杂的聚合任务也能用简洁、声明式的方式完成。掌握 Collector 的实现细节,可以让你在面对各种业务需求时不再受限于标准收集器,写出既高效又可读的代码。

  • Java 17:并行 Stream 的性能评估与调优

    在 Java 8 引入 Stream API 后,函数式编程风格迅速普及。随之而来的并行 Stream 成为了提升 CPU 密集型任务性能的一大利器。本文将以 Java 17 为例,系统阐述并行 Stream 的工作原理、常见性能瓶颈以及针对不同场景的调优技巧。

    一、并行 Stream 的基本原理

    1. Fork‑Join 任务分割
      并行 Stream 基于 ForkJoinPool。整个数据集合被递归拆分为若干子任务,每个子任务在独立线程中执行。拆分过程遵循 Spliterator 接口,通过 trySplit() 方法动态划分工作量。

    2. 任务合并
      当子任务完成后,结果通过 reducecollect 等操作在主线程中合并。若操作是无状态(stateless)且可并行的,合并成本极低;但若是有状态(stateful)或需要排序的操作,合并开销会显著增加。

    3. 工作量自适应
      ForkJoinPool 根据 CPU 核心数自动决定并行度,默认线程数为 Runtime.getRuntime().availableProcessors() * 2。这意味着在 8 核 CPU 上,默认最大并行度为 16。

    二、常见性能瓶颈

    场景 原因 影响
    小集合 线程切换与拆分成本 > 计算收益 反而比串行慢
    有状态操作 合并顺序、锁竞争 线程安全开销大
    I/O 密集 CPU 与 I/O 并行 CPU 空闲导致浪费
    非可分拆 Spliterator trySplit() 返回 null 无法真正并行

    三、调优技巧

    3.1 选取合适的并行度

    ForkJoinPool customPool = new ForkJoinPool(4); // 仅使用 4 线程
    IntStream.range(0, 1_000_000)
             .parallel()
             .unordered() // 减少排序成本
             .mapToObj(i -> compute(i))
             .forEach(customPool, Consumer::accept);
    • 手动指定线程数:针对 I/O 密集型任务可降低并行度,避免线程争抢。
    • 动态调整:根据任务进度监控 CPU 利用率,使用 ForkJoinPool.getCommonPoolParallelism() 调整。

    3.2 避免有状态操作

    • 使用无状态收集器:如 Collectors.toList(),而非 Collectors.toMap()(需要同步)。
    • 分区处理:先在并行流中生成中间结果,再在串行上下文合并。
    List
    <Result> results = IntStream.range(0, 1_000_000)
                                   .parallel()
                                   .mapToObj(this::compute)
                                   .collect(Collectors.toList()); // 无状态

    3.3 控制拆分粒度

    自定义 Spliterator 可以精准控制拆分阈值。示例:

    class FixedSizeSpliterator
    <T> implements Spliterator<T> {
        private final Iterator
    <T> iterator;
        private final int size;
    
        FixedSizeSpliterator(Iterator
    <T> it, int size) { this.iterator = it; this.size = size; }
    
        @Override
        public Spliterator
    <T> trySplit() {
            int splitSize = size / 2;
            if (splitSize <= 1) return null;
            List
    <T> splitList = new ArrayList<>(splitSize);
            for (int i = 0; i < splitSize && iterator.hasNext(); i++) {
                splitList.add(iterator.next());
            }
            return new FixedSizeSpliterator<>(splitList.iterator(), splitSize);
        }
    
        @Override public boolean tryAdvance(Consumer<? super T> action) { 
            if (iterator.hasNext()) { action.accept(iterator.next()); return true; }
            return false; 
        }
    
        @Override public long estimateSize() { return size; }
        @Override public int characteristics() { return ORDERED | SIZED; }
    }

    3.4 并行 Stream 与 ExecutorService 的比较

    维度 并行 Stream ExecutorService
    易用性 一行代码即可开启 需要显式线程管理
    灵活性 受限于 ForkJoinPool 完全可控
    可扩展性 受限于 common pool 可以自定义线程池

    若业务需要高度可定制化,建议使用 ExecutorService。但在大多数 CPU 密集型任务中,Stream 的语义简洁且足够高效。

    四、实战案例:并行统计 Word 频率

    public Map<String, Long> wordFrequency(String text) {
        return text.split("\\W+")
                   .parallel()
                   .filter(s -> !s.isEmpty())
                   .collect(Collectors.groupingBy(
                       String::toLowerCase,
                       Collectors.counting()));
    }
    • 拆分阈值:对极大文本可先按行拆分,然后再在每行内部并行计数。
    • 排序需求:若想得到前 10 名,可在收集后使用 Streamsorted()。注意此时必须使用 unordered() 或提前将结果转为数组再排序,以避免排序成为性能瓶颈。

    五、总结

    • 并行 Stream 通过 ForkJoinPool 自动划分任务,适用于 CPU 密集型无状态操作。
    • 对于小集合、有状态、I/O 密集或不易拆分的数据,串行或手动线程池更合适。
    • 通过自定义 Spliterator、控制并行度、避免状态操作,可显著提升性能。
    • 在 Java 17 之后,Collectors.toUnmodifiableList() 等新 API 进一步增强不可变集合的使用体验,建议在并行 Stream 中配合使用。

    以上为并行 Stream 的核心概念与调优方法。希望能帮助你在 Java 17 项目中充分利用多核优势,提升系统吞吐量。

  • Java 21:虚拟线程与并发编程的新时代

    在 Java 21 中,虚拟线程(Virtual Threads)正式成为标准特性,它彻底改变了 Java 并发编程的面貌。与传统的阻塞式线程相比,虚拟线程拥有更轻量、更高效的资源占用,同时通过更直观的编程模型提升了代码可读性与可维护性。以下内容将从概念、使用方式、优势、最佳实践以及潜在风险等方面,深入剖析虚拟线程如何重塑 Java 并发世界。


    1. 虚拟线程的起源

    Project Loom(Loom 项目)自 2019 年起就致力于为 Java 引入更高效的并发模型。最初的想法是让每个请求或任务都能对应一个轻量级的线程,而不再受限于传统的线程池和阻塞 I/O。随着 JDK 21 的发布,虚拟线程已成为标准 API,开发者可直接通过 java.util.concurrent 包中的 Executor 接口创建和管理。

    2. 基本概念

    • 虚拟线程(Virtual Thread):与平台线程(即 OS 线程)不同,虚拟线程是 Java 运行时栈(JVM 栈)级别的轻量级线程,调度由 JVM 内部完成。
    • 线程分离(Thread Migration):当一个虚拟线程执行阻塞操作时,JVM 会把它“挂起”,并在后台将该线程调度到另一个空闲的平台线程上执行。
    • 无锁并发:虚拟线程能够显著降低锁竞争,尤其是在大量 I/O 密集型任务场景下。

    3. 如何使用虚拟线程

    3.1 创建虚拟线程执行器

    ExecutorService executor = Executors.newVirtualThreadExecutor();

    3.2 提交任务

    CompletableFuture
    <Void> future = CompletableFuture.runAsync(() -> {
        // 模拟 I/O 阻塞
        try {
            Thread.sleep(2000); // 2000ms
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println(Thread.currentThread().getName() + " 完成任务");
    }, executor);

    3.3 关闭执行器

    executor.shutdown();

    注意:虚拟线程在完成任务后不会自动回收,需要显式关闭执行器以释放资源。

    4. 与传统线程的对比

    特性 平台线程 虚拟线程
    资源占用 每个线程 ~1 MB 堆栈 每个线程 ~2 KB 堆栈
    启动成本 约 20-30 μs 约 2-5 μs
    最大并发数 受限于 OS 线程数 可达数十万级别
    适用场景 CPU 密集型 I/O 密集型 + 轻量级并发

    虚拟线程特别适合处理高并发 I/O,例如网络服务器、数据库连接池、Web 框架等场景。

    5. 开发者的最佳实践

    1. 使用 CompletableFutureFlow:虚拟线程天然支持异步编程。
    2. 避免长时间运行的 CPU 密集型任务:这类任务会占用平台线程,导致虚拟线程调度效率下降。
    3. 细粒度错误处理:使用 try-catchCompletableFuture.exceptionally() 捕获异常,防止线程泄露。
    4. 监控线程使用:可以使用 jdk.management 或第三方监控工具查看虚拟线程数量与活跃状态。

    6. 可能的风险与挑战

    • 调试困难:虚拟线程的堆栈信息可能不如传统线程直观,调试工具需支持。
    • 旧库兼容性:某些基于线程本地变量(ThreadLocal)的库可能在虚拟线程环境下表现异常。
    • 资源泄漏:未关闭 ExecutorService 可能导致 JVM 无法退出。

    7. 未来展望

    • 结合 Project Panama:FFI(Foreign Function & Memory API)可以与虚拟线程无缝配合,进一步提升跨语言调用效率。
    • 更多的并发工具:JDK 将继续在 java.util.concurrent 中加入与虚拟线程配合的工具类,例如新的 SemaphoreCountDownLatch 实现。
    • 性能优化:JVM 会持续改进虚拟线程的调度策略,进一步降低延迟与提升吞吐量。

    小结

    Java 21 的虚拟线程为开发者提供了一种更简洁、更高效的并发模型。通过轻量级线程与 JVM 内部调度的协同工作,能够在保持熟悉的同步语义的同时,大幅提升 I/O 密集型应用的并发性能。拥抱虚拟线程,意味着我们可以以更少的代码、更低的资源消耗,构建可扩展的现代 Java 系统。

    实战建议:在现有项目中,先在非关键路径使用虚拟线程进行性能对比;若验证无异常,可逐步将高并发 I/O 任务迁移至虚拟线程池,观察整体吞吐率与响应时间的提升。

  • Java 17:Switch 表达式的模式匹配功能详解

    在 Java 17 中,Switch 表达式不再局限于返回简单的值,而是支持模式匹配(Pattern Matching),这让条件判断和类型检查变得更加直观和简洁。本文将从概念、语法、典型案例以及性能影响四个方面展开,帮助你快速上手并在项目中高效使用。

    一、模式匹配是什么?

    模式匹配是一种语法结构,允许你在 case 子句中对对象进行类型检查、属性解构甚至复杂表达式匹配。与传统的 instanceof + 强制类型转换相比,它能够减少样板代码、提升可读性,并在编译期提供更强的类型安全。

    二、Switch 表达式的新语法

    String result = switch (obj) {
        case String s -> "是字符串:" + s;
        case Integer i when (i > 10) -> "大于10的整数:" + i;
        case null -> "空值";
        default -> "其他类型";
    };

    关键点:

    • case X x ->:在匹配到类型 X 时,将对象绑定到变量 x
    • when 关键字:对已匹配类型的进一步筛选。
    • -> 符号:表示该分支返回的值。
    • default 必须出现,用来处理所有未匹配的情况。

    三、典型案例:统一日志处理

    public String formatLog(Object payload) {
        return switch (payload) {
            case null -> "无有效数据";
            case String s -> "文本:" + s;
            case List<?> l -> "列表长度:" + l.size();
            case Map<?, ?> m when m.containsKey("code") ->
                "错误码:" + m.get("code");
            default -> "未知类型:" + payload.getClass().getSimpleName();
        };
    }

    此函数通过模式匹配一次性完成对多种常见数据类型的处理,代码简洁且易于维护。

    四、性能考量

    • JIT 编译:JDK 17 在 HotSpot 中已为 Switch+Pattern 生成高效的字节码,通常不会比手写 if-elseinstanceof 差,甚至在复杂分支时更快。
    • 内存占用:模式匹配不产生额外的临时对象,只是把对象作为局部变量引用,内存开销与传统实现相当。
    • 可读性 vs. 复杂度:对初学者而言,理解 when 语法需要一定时间;但一旦掌握,错误率大幅下降。

    五、实战建议

    1. 逐步迁移:先在非关键路径的代码中使用模式匹配,验证编译兼容性和性能后,再在核心业务代码中大规模替换。
    2. 单元测试:为每个 case 编写覆盖测试,确保新语法在所有边缘情况下行为正确。
    3. 文档注释:即使代码已自解释,也建议在 switch 语句上方简要说明业务场景,方便后期维护。

    六、结语

    Java 17 的 Switch 表达式模式匹配为我们提供了一种更接近自然语言的代码风格,既提升了代码可读性,又保证了类型安全。随着更多 IDE 支持智能提示,未来我们可以将业务逻辑写得更干净、更易维护。希望本文能帮助你在项目中快速掌握并落地。祝编码愉快!