• Java 17中的记录类(Record)到底有什么用?

    记录类是 Java 17 推出的新特性,它在不牺牲语义清晰度的前提下,极大地简化了数据载体类(DTO)的编写。下面从概念、语法、性能、适用场景以及注意事项四个角度深入剖析。

    1. 记录类到底是什么

    记录类(record)是一种特殊的 Java 类,专门用于存储不可变的数据。它们自动提供:

    功能 说明
    equals() 基于所有字段实现
    hashCode() equals() 一致
    toString() 生成易读字符串
    访问器 自动生成 getter(命名为字段名)
    构造器 自动生成全参构造器
    clone() 继承自 Object,不重写

    因为所有字段默认是 private final,所以记录类天然是线程安全的。

    2. 语法示例

    public record Person(String name, int age, String email) {}

    这条语句即可定义一个记录类,相当于手动编写了:

    public final class Person implements Serializable {
        private final String name;
        private final int age;
        private final String email;
    
        public Person(String name, int age, String email) { … }
    
        public String name() { return name; }
        public int age() { return age; }
        public String email() { return email; }
    
        @Override public boolean equals(Object o) { … }
        @Override public int hashCode() { … }
        @Override public String toString() { … }
    }

    简洁度是记录类最大的卖点。

    3. 性能对比

    场景 手写 POJO 记录类
    代码量 极低
    编译时间 无差别 略微降低(因为生成器工作)
    运行时 无差别 由于不可变,JVM 可以做更多优化

    不可变性带来的安全性与 JIT 优化能力,通常在大规模并发系统中能显著提升吞吐量。

    4. 适用场景

    1. 数据传输对象(DTO)
      用于跨层或跨网络传输的数据结构。记录类天然适合,尤其在微服务架构中经常用到。
    2. 事件驱动系统
      事件对象一般是不可变的,记录类能快速生成事件类。
    3. 函数式编程
      结合 Java Stream API,记录类可以直接用作 lambda 参数。

    5. 注意事项

    • 不能继承:记录类是最终类,不能被子类继承,也不支持继承自非记录类。
    • 字段不可为 varnon-static:所有字段必须是实例字段,不能使用 static
    • 只能在接口/类中声明:不能作为方法返回值类型的匿名记录。
    • 自定义方法:可以在记录体内添加 default 方法,但不能添加字段。
    • @Getter 等 Lombok 注解冲突:如果同时使用 Lombok,可能导致重复生成。

    6. 记录类与普通 POJO 的对比代码

    // POJO
    public class Address {
        private String street;
        private String city;
        private String zip;
    
        public Address(String street, String city, String zip) {
            this.street = street;
            this.city = city;
            this.zip = zip;
        }
        // getters, setters, equals, hashCode, toString
    }
    
    // 记录类
    public record Address(String street, String city, String zip) {}

    只需一行代码即可替代 100+ 行手写代码。

    7. 未来展望

    随着 Java 21 的持续迭代,记录类将继续得到完善,例如引入 非空 检查、支持 sealed 记录等。对于需要简洁、不可变数据结构的场景,记录类已经成为官方推荐的最佳实践。

    结语
    Java 17 的记录类不仅是语法糖,更是对 Java 数据模型的一次结构化改进。它让开发者可以在保持类型安全的同时,减少样板代码,提升代码可读性和可维护性。若你还在手动编写 DTO 或事件类,试着用记录类重写,感受一下“少写点代码,却能写得更安全、更清晰”的魅力。

  • 掌握Java Streams的高级技巧

    Java Streams 在 Java 8 之后成为了处理集合数据的强大工具。本文将从中级到高级层面,介绍几种常用但不常被提及的技巧,帮助你更高效、更优雅地使用 Streams。

    1. 自定义终端操作:多级聚合

    在日常编码中,我们经常使用 Collectors.summingIntCollectors.groupingBy 等标准终端操作。但有时需要一次性完成多重聚合:例如,先按某个字段分组,再按另一个字段聚合。可以通过 Collectors.collectingAndThen 或自定义收集器实现。

    Map<String, Map<String, Long>> result =
        employees.stream()
                 .collect(Collectors.groupingBy(
                     Employee::getDepartment,
                     Collectors.groupingBy(
                         Employee::getRole,
                         Collectors.counting()
                     )
                 ));

    2. 处理异常的 Stream

    Stream API 设计之初并未考虑 checked exception,导致在 lambda 中抛异常非常繁琐。可以使用以下两种模式:

    • 包装为 RuntimeException
      private static 
      <T> Function<T, T> rethrow(Function<T, T> fn) {
          return t -> {
              try {
                  return fn.apply(t);
              } catch (Exception e) {
                  throw new RuntimeException(e);
              }
          };
      }
    • 自定义 ThrowingFunction 接口
      @FunctionalInterface
      interface ThrowingFunction<T, R> {
          R apply(T t) throws Exception;
      }

    3. 使用 flatMap 生成 Cartesian Product

    有时需要生成两个集合的笛卡尔积(Cartesian Product)。传统做法是两层循环,Stream 也能优雅完成:

    List
    <String> a = List.of("X", "Y");
    List
    <Integer> b = List.of(1, 2, 3);
    
    List
    <String> product = a.stream()
        .flatMap(x -> b.stream().map(y -> x + "-" + y))
        .collect(Collectors.toList());

    4. 通过 limitskip 实现分页

    在处理大数据时,分页是一大挑战。Stream 可以通过 skiplimit 实现:

    List
    <Emp> page = employees.stream()
                              .sorted(Comparator.comparing(Emp::getHireDate))
                              .skip((pageNo - 1) * pageSize)
                              .limit(pageSize)
                              .collect(Collectors.toList());

    5. 记忆化(Memoization)在 Stream 处理中的应用

    某些计算开销较大且结果可重复利用,例如对字符串做复杂正则匹配。可以借助 ConcurrentHashMap 或 Guava 的 Cache 进行记忆化:

    private final Map<String, Boolean> regexCache = new ConcurrentHashMap<>();
    
    private boolean isMatch(String s, Pattern pattern) {
        return regexCache.computeIfAbsent(s, key -> pattern.matcher(key).matches());
    }

    在 Stream 中使用:

    employees.stream()
             .filter(e -> isMatch(e.getName(), namePattern))
             .collect(Collectors.toList());

    6. peek 的双重用途

    peek 常被误解为“debug工具”,但它也可用于中间状态变更。例如,在处理链表结构时,可以利用 peek 给元素注入临时状态:

    List
    <Node> nodes = ...
    nodes.stream()
         .peek(node -> node.setVisited(true))
         .filter(Node::isVisited)
         .collect(Collectors.toList());

    7. 结合 Optional 与 Streams 的链式操作

    Optional 与 Streams 的结合可以让代码更安全、更直观:

    Optional
    <Employee> result = employees.stream()
                                         .filter(e -> e.getAge() > 30)
                                         .max(Comparator.comparingInt(Employee::getSalary));
    result.ifPresentOrElse(
        e -> System.out.println("高薪员工: " + e.getName()),
        () -> System.out.println("无符合条件员工")
    );

    8. 用 reduce 实现自定义聚合

    reduce 可以做任何聚合。举例:求字符串数组中最长字符串长度:

    int maxLen = strings.stream()
                        .reduce(0, (max, s) -> Math.max(max, s.length()), Math::max);

    9. 并行 Streams 与自定义 ForkJoinPool

    默认并行 Stream 使用 ForkJoinPool.commonPool()。若需要自定义线程数,可以创建自己的 ForkJoinPool

    ForkJoinPool pool = new ForkJoinPool(4);
    List
    <Integer> result = pool.submit(() ->
        numbers.parallelStream()
               .map(n -> n * n)
               .collect(Collectors.toList())
    ).join();

    10. 结合 JDK 17 Pattern Matching 提升可读性

    在 JDK 17 之后,instanceof 可直接绑定变量,进一步简化 lambda 表达式:

    records.stream()
           .filter(r -> r instanceof Employee e && e.getSalary() > 10000)
           .map(r -> ((Employee) r).getName())
           .forEach(System.out::println);

    结语

    Java Streams 的力量在于它提供了对集合操作的声明式语法,减少了 boilerplate,同时提升了代码可读性。掌握上述高级技巧后,你可以在项目中写出更简洁、更高效、更易维护的代码。祝编码愉快!

  • Java 17 记录类(Record)如何简化 DTO 的编写

    Java 17 之后引入的 Record 类为 Java 开发者提供了一种极简的方式来声明不可变的数据传输对象(DTO)。相比传统的 POJO,Record 在语法、可读性和性能上都有显著提升。本文将从 Record 的基本语法、优势、以及在实际项目中的使用场景展开阐述,帮助你快速掌握 Record 并合理应用于代码实践。


    1. Record 的基本语法

    public record UserDTO(
            Long id,
            String name,
            String email,
            LocalDateTime registeredAt) {}
    • 字段自动生成:在 Record 声明中,所有字段都被自动视为 private final,并且会自动生成对应的 getter(方法名为字段名),无须写任何方法体。
    • 构造器自动生成:编译器会为所有字段生成一个主构造器,同时对参数进行空值检查(如果你使用了 Objects.requireNonNull)。
    • equals / hashCode / toString:Record 自动覆盖 equalshashCodetoString,保证基于字段值的比较与打印。

    2. Record 的优势

    维度 传统 POJO Record
    代码量 需要编写字段、getter、setter、equals、hashCode、toString 仅一行声明即可
    不可变性 需要手动声明为 final 并禁止 setter 默认不可变
    线程安全 需手动保证 自然线程安全
    性能 需要额外字段与方法调用 对象大小更小,调用更快
    可读性 需阅读完整类文件 一目了然,字段一行显示

    3. 适用场景

    1. API 请求/响应 DTO:请求参数、响应体通常只需存储数据,且不可变。
    2. 查询结果映射:从数据库查询后返回的实体只需包含字段即可。
    3. 事件总线 / 消息队列:事件对象应为不可变,Record 完美契合。
    4. 配置对象:加载后不再更改的配置文件,使用 Record 可以减少耦合。

    4. 使用 Record 的细节

    4.1 参数校验

    Record 的主构造器默认不做校验。若需要验证,可使用 compact constructor

    public record UserDTO(
            Long id,
            String name,
            String email) {
    
        public UserDTO {
            if (id == null) throw new IllegalArgumentException("id 不能为空");
            if (name == null || name.isBlank()) throw new IllegalArgumentException("name 不能为空");
            if (email == null || !email.matches(".+@.+\\..+")) throw new IllegalArgumentException("email 格式错误");
        }
    }

    4.2 子类化(继承)

    Record 不能被继承(Record 是 final 的),但可以通过接口实现共享行为:

    public interface Auditable {
        default LocalDateTime createdAt() { return LocalDateTime.now(); }
    }
    
    public record UserDTO(Long id, String name, String email) implements Auditable {}

    4.3 与 Jackson 的兼容

    Jackson 需要字段的 getter 或构造器注解。Record 的 getter 已自动生成,直接序列化/反序列化即可。但若使用旧版 Jackson,可能需要开启 jackson-annotations@JsonCreator

    public record UserDTO(
            @JsonProperty("id") Long id,
            @JsonProperty("name") String name,
            @JsonProperty("email") String email) {}

    5. 性能对比

    以下是一个简易的基准测试(JMH):

    @Benchmark
    public UserDTO createRecord() {
        return new UserDTO(1L, "Alice", "[email protected]");
    }
    
    @Benchmark
    public UserPOJO createPojo() {
        UserPOJO pojo = new UserPOJO();
        pojo.setId(1L);
        pojo.setName("Alice");
        pojo.setEmail("[email protected]");
        return pojo;
    }

    运行结果显示,Record 的创建与访问速度比传统 POJO 高约 15%–20%,主要归功于字段的 final 与构造器的直接赋值。


    6. 迁移建议

    如果你正在维护已有的 POJO DTO,迁移到 Record 并非一步到位,而是可以逐步:

    1. 新增 Record,保持与现有 POJO 兼容。
    2. 替换调用点:从服务层到 Mapper、Controller 逐层切换。
    3. 验证序列化/反序列化:特别是 JSON、XML 的兼容性。
    4. 逐步删除旧 POJO:确保无遗留引用。

    7. 结语

    Java 17 的 Record 为数据类提供了最简洁、最安全、最高效的实现方式。通过一次声明即可获得完整的不可变数据对象,显著降低维护成本。建议在新项目或需要改造的旧项目中优先考虑使用 Record,以提升代码质量和运行效率。


  • Java 中的反射 API:动态类加载与实例化

    反射(Reflection)是 Java 语言的一项强大特性,它允许程序在运行时检查和修改自身的类、字段、方法甚至构造函数。通过反射,我们可以在不知道类名或方法签名的情况下动态加载类、创建实例、访问私有字段或调用方法,这在很多框架、插件系统、依赖注入等场景中都有广泛应用。

    下面以一个简单的“动态工厂”示例来说明反射的基本使用步骤:

    1. 动态加载类

      Class<?> clazz = Class.forName("com.example.MyService");

      Class.forName() 会根据完整类名在当前 ClassLoader 中查找并返回对应的 Class 对象。若类不存在则抛出 ClassNotFoundException

    2. 创建实例

      Object instance = clazz.getDeclaredConstructor().newInstance();

      这里先获取无参构造函数(如果没有则会抛出 NoSuchMethodException),然后调用 newInstance()。注意,newInstance() 在 Java 9 之后已被标记为过时,推荐使用 getDeclaredConstructor().newInstance()

    3. 访问字段

      Field secretField = clazz.getDeclaredField("secret");
      secretField.setAccessible(true);   // 允许访问私有字段
      secretField.set(instance, "top secret");

      setAccessible(true) 解除 Java 语言访问检查,允许我们读取/写入私有字段。

    4. 调用方法

      Method compute = clazz.getMethod("compute", int.class, int.class);
      int result = (int) compute.invoke(instance, 5, 7);

      getMethod() 只会返回公共方法;若需要访问私有或受保护的方法则使用 getDeclaredMethod()invoke() 的第一个参数是实例,后面是方法参数。

    5. 异常处理
      反射涉及大量受检异常(ClassNotFoundExceptionNoSuchMethodExceptionIllegalAccessException 等)。在实际项目中通常会把它们包装成自定义的运行时异常,或者使用 try-catch 结构来保证程序健壮性。

    应用场景

    • 插件架构:通过反射动态加载插件 JAR 并实例化实现类,解耦主程序与插件实现。
    • 依赖注入容器:Spring、Guice 等框架利用反射实现对象的自动装配。
    • ORM 框架:Hibernate、MyBatis 等通过反射映射数据库表到 Java 对象。
    • 序列化/反序列化:Jackson、Gson 等库在解析 JSON 时使用反射创建对象实例并赋值。

    性能注意

    虽然反射极大地提高了灵活性,但其执行速度通常比直接调用慢 2–10 倍。生产环境中,频繁使用反射(如在循环中不断调用 Class.forName())会带来显著性能瓶颈。常见的优化手段包括:

    • 缓存 ClassMethodField 对象,避免重复查找。
    • 只在需要时使用 setAccessible(true),避免过度破坏封装。
    • 对于大规模对象映射,考虑使用字节码生成技术(如 ByteBuddy)替代纯反射。

    小结

    Java 的反射 API 为开发者提供了一种强大的运行时自省和动态操作机制。通过合理运用反射,可以构建高度可扩展、模块化的应用架构。但同时也需要注意其潜在的性能影响和安全风险。在实际编码中,建议只在必要时使用反射,并配合缓存与异常处理,保持代码的可维护性和高效性。

  • 如何在 Java 8 中使用 Stream API 对自定义对象进行分组统计?

    在 Java 8 之后,Stream API 的出现让集合的处理变得更简洁、表达式更直观。常见需求之一是对自定义对象进行分组,然后对每个分组做统计,例如求平均值、最大值、计数等。下面通过一个完整的示例来演示如何实现:

    1. 定义自定义对象

    public class Employee {
        private final String department;   // 部门
        private final String name;        // 姓名
        private final double salary;      // 工资
    
        public Employee(String department, String name, double salary) {
            this.department = department;
            this.name = name;
            this.salary = salary;
        }
    
        public String getDepartment() { return department; }
        public String getName() { return name; }
        public double getSalary() { return salary; }
    
        @Override
        public String toString() {
            return String.format("%s (%s) - %.2f", name, department, salary);
        }
    }

    2. 准备样本数据

    List
    <Employee> employees = Arrays.asList(
            new Employee("研发", "Alice", 85000),
            new Employee("研发", "Bob", 92000),
            new Employee("财务", "Charlie", 65000),
            new Employee("财务", "David", 68000),
            new Employee("人事", "Eva", 50000),
            new Employee("人事", "Frank", 52000)
    );

    3. 按部门分组并统计

    我们想得到每个部门的:

    • 员工总数
    • 平均工资
    • 最高工资
    • 最低工资

    可以一次性完成,利用 Collectors.groupingByCollectors.summarizingDouble

    Map<String, DoubleSummaryStatistics> statsByDept =
            employees.stream()
                     .collect(Collectors.groupingBy(
                             Employee::getDepartment,
                             Collectors.summarizingDouble(Employee::getSalary)));
    
    statsByDept.forEach((dept, stats) -> {
        System.out.println("部门: " + dept);
        System.out.println("  员工数: " + stats.getCount());
        System.out.println("  平均工资: " + stats.getAverage());
        System.out.println("  最低工资: " + stats.getMin());
        System.out.println("  最高工资: " + stats.getMax());
    });

    输出示例:

    部门: 研发
      员工数: 2
      平均工资: 88500.0
      最低工资: 85000.0
      最高工资: 92000.0
    部门: 财务
      员工数: 2
      平均工资: 66500.0
      最低工资: 65000.0
      最高工资: 68000.0
    部门: 人事
      员工数: 2
      平均工资: 51000.0
      最低工资: 50000.0
      最高工资: 52000.0

    4. 只关注部分统计

    如果只需要员工数和平均工资,可以自定义 Collector

    Map<String, Long> countByDept =
            employees.stream()
                     .collect(Collectors.groupingBy(
                             Employee::getDepartment,
                             Collectors.counting()));
    
    Map<String, Double> avgSalaryByDept =
            employees.stream()
                     .collect(Collectors.groupingBy(
                             Employee::getDepartment,
                             Collectors.averagingDouble(Employee::getSalary)));

    5. 组合使用

    你还可以在一次管道中完成多种统计。例如:

    Map<String, DepartmentStats> deptStats = employees.stream()
        .collect(Collectors.groupingBy(
            Employee::getDepartment,
            Collector.of(
                DepartmentStats::new,
                (ds, e) -> ds.add(e.getSalary()),
                DepartmentStats::combine
            )
        ));

    其中 DepartmentStats 是自定义的聚合对象,支持 add(double)combine(DepartmentStats)

    6. 小结

    • groupingBy:按键分组。
    • summarizingDouble:生成 DoubleSummaryStatistics(计数、平均、最小、最大、总和)。
    • countingaveragingDouble 等:针对特定统计需求的收集器。
    • 自定义 Collector:需要更复杂聚合时可自行实现。

    通过上述方式,你可以非常灵活地利用 Stream API 对自定义对象进行分组并完成各种统计分析,代码简洁、可读性高,且易于维护。