惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
爱范儿
爱范儿
Y
Y Combinator Blog
T
Tor Project blog
V
Visual Studio Blog
U
Unit 42
B
Blog RSS Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
G
Google Developers Blog
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
Recorded Future
Recorded Future
博客园_首页
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
P
Proofpoint News Feed
Jina AI
Jina AI
博客园 - 【当耐特】
S
Security @ Cisco Blogs
I
Intezer
MyScale Blog
MyScale Blog
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
腾讯CDC
T
Tenable Blog
A
Arctic Wolf
T
Threat Research - Cisco Blogs
S
Securelist
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
C
Check Point Blog
NISL@THU
NISL@THU
Microsoft Security Blog
Microsoft Security Blog
V
Vulnerabilities – Threatpost

博客园 - WinChance

反向代理"冷连接惩罚" 通用元 Prompt:新技术「做中学指挥手册」生成器 Timefold Solver 快速入门 Open Code 快捷键 基于现有项目提交到SVN远程仓 .NETer的Java 21低痛苦开发指南 AI+未来世界蓝图预测资料 资深工程师思维和技巧 语文启蒙 数学启蒙 英语启蒙 3大1小(4岁)重庆春节5天4晚·金佛山西坡最终版 中文提示词最佳实践开源库推荐 AI-Native 开发工作流手册 AI-Native 技术决策者速查手册 《Hello 算法》+ 尚硅谷韩顺平 组合拳食用指南 广东前20名亲子景点(按对4岁孩子吸引力排序) Gitea CICD Win11WSL2 安装Ubuntu-22.04 钣金件化学池视觉跟踪系统方案 面向初中生的电子制作 VMware虚拟机安装Ubuntu22.04服务器版图文教程(超详细) 在Ubuntu 24上优雅地安装VMware Tools的终极指南
.NETer 的 Java 17 低痛苦开发指南
WinChance · 2026-04-01 · via 博客园 - WinChance

《.NETer 的 Java 17 低痛苦开发指南》
从 C# 到 Java 的平滑着陆:保留 LINQ 手感、async/await 思维与 AutoMapper 体验


写在前面

如果你是一位习惯了 C# 的 .NETer,被迫(或主动)切换到 Java 17 技术栈,你一定经历过这些文化冲击

  • 想写 list.Where().Select().ToList(),却发现 Java 是 stream().filter().map().collect(Collectors.toList())——Collectors 是什么鬼?
  • 习惯了 user?.Address?.City 的一键防崩,Java 里却要写 if (user != null && user.getAddress() != null)——俄罗斯套娃式判空
  • 想念 async/await 的同步写法,面对 CompletableFuture.thenCompose().thenApply()——回调地狱再现
  • 习惯了 mapper.Map<OrderVO>(entity),Java 里却要手写 20 行的 Converter.setXxx()——样板代码写到吐

好消息是:在 Java 17 时代(LTS 版本,支持到 2029 年),通过合理的工具链组合,你可以恢复 90% 的 C# 开发体验,同时保持 Java 生态的稳定性。

本指南基于 Java 17 LTS + Spring Boot 3.x 环境,提供可直接落地的非侵入式方案(不绑定任何特定微服务框架)。


一、技术栈选型(Java 17 生态位对应表)

C# 生态 Java 17 对应方案 版本要求 解决痛点
LINQ StreamEx (one.util) 0.8.3+ 消除 Collectors 样板代码
AutoMapper MapStruct + Lombok 1.5.x / 1.18.x 编译期生成映射代码,零反射
async/await CompletableFuture + 优化模板 JDK 内置 结构化并发(非虚拟线程版)
?. 空传播 Optional 链式 + Objects 工具 JDK 内置 链式判空
decimal BigDecimal (String 构造) JDK 内置 金融精度保障
DateTime LocalDateTime / ZonedDateTime JDK 内置 线程安全,时区明确
属性语法 Lombok @Data / Record (JDK 16+) 1.18.x 消除 Getter/Setter 样板
异常处理 @SneakyThrows (Lombok) 1.18.x 消除 Checked Exception 污染

二、痛点详解与解决方案

2.1 集合操作:重建 LINQ 体验(StreamEx)

C# 写法(参考)

var result = orders
    .Where(o => o.Status != "Done" && o.Amount > 100)
    .OrderByDescending(o => o.CreateTime)
    .Select(o => new { o.OrderNo, o.Amount })
    .Take(10)
    .ToDictionary(o => o.OrderNo, o => o.Amount);

Java 17 原生(痛苦)

// 冗长、类型噪音、Comparator 样板
Map<String, BigDecimal> map = orders.stream()
    .filter(o -> !"Done".equals(o.getStatus()) && o.getAmount().compareTo(new BigDecimal(100)) > 0)
    .sorted(Comparator.comparing(Order::getCreateTime).reversed())
    .limit(10)
    .collect(Collectors.toMap(
        Order::getOrderNo, 
        Order::getAmount, 
        (v1, v2) -> v1 // 必须处理冲突,否则抛异常
    ));

Java 17 低痛苦方案(StreamEx)

添加依赖:

<dependency>
    <groupId>one.util</groupId>
    <artifactId>streamex</artifactId>
    <version>0.8.3</version>
</dependency>

代码:

import one.util.streamex.StreamEx;

Map<String, BigDecimal> result = StreamEx.of(orders)
    .filter(o -> !"Done".equals(o.getStatus()))
    .filter(o -> o.getAmount().compareTo(BigDecimal.valueOf(100)) > 0)
    .sortedBy(Order::getCreateTime)           // 正序
    .reverseSorted()                          // 倒序(无需 Comparator)
    .map(o -> new AbstractMap.SimpleEntry<>(o.getOrderNo(), o.getAmount()))
    .toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue); // 无 mergeFunction 噪音

// 或更简单的 toMap 重载(直接取字段)
Map<String, BigDecimal> result = StreamEx.of(orders)
    .filter(o -> !"Done".equals(o.getStatus()))
    .sortedByDescending(Order::getCreateTime)  // 直接倒序
    .limit(10)
    .toMap(Order::getOrderNo, Order::getAmount);

关键优势

  • toMap() 无需处理 mergeFunction(Key 冲突时默认后覆盖,不抛异常)
  • sortedBy() 直接传方法引用,无需 Comparator.comparing()
  • groupingBy() 返回标准 Map,支持下游收集器简化

2.2 空指针防护:重建 ?. 操作符(Optional 链式)

C# 写法

var city = order?.Customer?.Address?.City ?? "Unknown";

Java 17 方案(Optional 链式)

import java.util.Optional;
import java.util.Objects;

// 方案 1:严格链式(JDK 8+ 标准)
String city = Optional.ofNullable(order)
    .map(o -> o.getCustomer())
    .map(c -> c.getAddress())
    .map(a -> a.getCity())
    .filter(Objects::nonNull)  // 过滤空字符串也可在此加 .filter(s -> !s.isEmpty())
    .orElse("Unknown");

// 方案 2:单层快速默认值(Java 9+)
String city = Objects.requireNonNullElse(order.getCity(), "Unknown");

// 方案 3:多层但快速失败(Java 9+ Objects 工具)
String city = Objects.requireNonNullElse(
    Objects.requireNonNullElse(
        order.getCustomer(), 
        new Customer()
    ).getAddress(), 
    new Address()
).getCity(); // 不推荐,不如 Optional 清晰

注意事项

  • Optional 适合读操作链,不要在里面写 map(o -> { o.setXxx(); return o; }),违反函数式原则
  • 如果只是为了读取展示,且对象层级很深,考虑使用 JsonPathcom.jayway.jsonpath)直接取值,避免 NPE:
    String city = JsonPath.read(orderJson, "$.customer.address.city"); // 路径不存在返回 null,不抛异常
    

2.3 DTO 映射:重建 AutoMapper(MapStruct)

C# 写法

var dto = mapper.Map<OrderDto>(entity);

Java 17 方案(MapStruct + Lombok)

依赖配置(关键:处理器顺序 Lombok 在前):

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>1.5.5.Final</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.36</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>1.18.36</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.5.5.Final</version>
                    </path>
                    <!-- 关键绑定:让 MapStruct 能看到 Lombok 生成的 getter -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

代码实现:

// Entity(数据库实体)
@Data  // Lombok 自动生成 getter/setter
public class OrderEntity {
    private Long id;
    private String orderNumber;  // 注意字段名不同
    private BigDecimal totalAmount;
    private LocalDateTime createTime;
    private Integer status;      // 0=Pending, 1=Done
}

// DTO(返回给前端)
@Data
public class OrderDto {
    private String orderNo;     // 对应 orderNumber
    private String amount;      // 需要格式化 + 单位
    private String createTime;  // 需要格式化字符串
    private String statusDesc;  // 需要转换 0->"待处理"
}

// Converter 接口(编译期自动生成实现类)
@Mapper(componentModel = "spring")
public interface OrderConverter {
    
    // 字段名不同、类型转换、表达式计算
    @Mapping(target = "orderNo", source = "orderNumber")
    @Mapping(target = "amount", expression = "java(entity.getTotalAmount().toPlainString() + \" 元\")")
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm")
    @Mapping(target = "statusDesc", expression = "java(convertStatus(entity.getStatus()))")
    OrderDto toDto(OrderEntity entity);
    
    // 批量转换(自动生成 for-each,无需手写)
    List<OrderDto> toDtoList(List<OrderEntity> entities);
    
    // 默认方法:自定义转换逻辑
    default String convertStatus(Integer status) {
        return status == 0 ? "待处理" : (status == 1 ? "已完成" : "未知");
    }
}

// 使用(Controller 或 Service 中注入)
@Service
public class OrderService {
    @Autowired
    private OrderConverter converter;
    
    public List<OrderDto> getList() {
        List<OrderEntity> entities = orderMapper.selectAll();
        return converter.toDtoList(entities);  // 一行转换,零样板代码
    }
}

编译后生成的实现类(位于 target/generated-sources/annotations):

@Component
public class OrderConverterImpl implements OrderConverter {
    @Override
    public OrderDto toDto(OrderEntity entity) {
        if (entity == null) return null;
        OrderDto dto = new OrderDto();
        dto.setOrderNo(entity.getOrderNumber());
        dto.setAmount(entity.getTotalAmount().toPlainString() + " 元");
        // ... 自动处理
        return dto;
    }
}

2.4 异步编程:Java 17 下的最佳实践(CompletableFuture 优化)

现实检查:Java 17 没有 Virtual Threads(JDK 21+),也没有 async/await 语法糖。但我们可以通过结构化并发模式CompletableFuture 的合理组合,减少回调地狱。

C# 参考

public async Task<OrderStatus> GetStatusAsync(string id) {
    var user = await userSvc.GetAsync(id);
    var order = await orderSvc.GetAsync(user.OrderId);
    var stock = await stockSvc.CheckAsync(order.Sku);
    return Merge(user, order, stock);
}

Java 17 低痛苦方案(CompletableFuture 顺序 + 并行组合)

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class OrderAsyncService {
    
    // 自定义线程池(避免使用默认 ForkJoinPool.commonPool(),防止阻塞)
    private final ExecutorService executor = Executors.newFixedThreadPool(10);
    
    // 场景 1:顺序执行(链式依赖)
    public CompletableFuture<OrderStatus> getStatusSequential(String userId) {
        return CompletableFuture.supplyAsync(() -> userService.get(userId), executor)
            .thenCompose(user -> 
                CompletableFuture.supplyAsync(() -> orderService.get(user.getOrderId()), executor)
            )
            .thenCompose(order -> 
                CompletableFuture.supplyAsync(() -> stockService.check(order.getSku()), executor)
            )
            .thenApply(stock -> merge(...)); // 最终合并
    }
    
    // 场景 2:并行执行(无依赖,同时发起)
    public CompletableFuture<OrderStatus> getStatusParallel(String userId) {
        CompletableFuture<User> userCf = CompletableFuture.supplyAsync(() -> userService.get(userId), executor);
        CompletableFuture<Config> configCf = CompletableFuture.supplyAsync(() -> configService.get(), executor);
        CompletableFuture<Log> logCf = CompletableFuture.supplyAsync(() -> logService.getRecent(), executor);
        
        // 全部完成后合并(类似 C# Task.WhenAll + 解构)
        return CompletableFuture.allOf(userCf, configCf, logCf)
            .thenApply(v -> {
                // 注意:join() 在 thenApply 内部不会阻塞外部线程,因为此时已完成
                User user = userCf.join();
                Config config = configCf.join();
                Log log = logCf.join();
                return merge(user, config, log);
            });
    }
    
    // 场景 3:并行 + 异常处理(Java 12+ 的 exceptionalCompletion 改进)
    public CompletableFuture<OrderStatus> getStatusWithFallback(String userId) {
        return CompletableFuture.supplyAsync(() -> userService.get(userId), executor)
            .exceptionally(ex -> {
                log.error("获取用户失败", ex);
                return User.defaultUser(); // 降级
            })
            .thenCompose(user -> orderService.getAsync(user.getId()));
    }
}

降低痛苦的技巧

  1. 始终传递自定义 Executor:防止默认线程池饱和(Java 默认 ForkJoinPool 线程数 = CPU 核心数 - 1,IO 密集型应用会阻塞)
  2. 封装工具类:将 supplyAsync(() -> x, executor) 封装为 async(() -> x) 方法,减少样板
  3. 避免 get()/join() 在主线程调用:只在 thenApply 回调内部使用 join()(此时数据已就绪,不会真阻塞)

ThreadLocal 在异步下的传递(多租户/追踪 ID 场景):
Java 17 的 CompletableFuture 默认不会传递 ThreadLocal,需使用 TransmittableThreadLocal(TTL)(阿里开源):

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.5</version>
</dependency>
// 定义
public class Context {
    public static final TransmittableThreadLocal<String> TENANT_ID = new TransmittableThreadLocal<>();
}

// 在异步链中手动传递(或使用 TtlRunnable/TtlCallable 包装)
CompletableFuture.supplyAsync(
    TtlCallable.get(() -> {
        // 此内部能获取到父线程的 TENANT_ID
        return service.get();
    }), 
    executor
);

2.5 日期时间:LocalDateTime 全家桶(替代 Date/Calendar)

C# 参考

var now = DateTime.Now;
var utc = DateTime.UtcNow;
var formatted = now.ToString("yyyy-MM-dd HH:mm");
var parsed = DateTime.Parse("2024-01-01");

Java 17 方案

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;
import java.time.LocalTime;

// 当前时间(时区敏感)
LocalDateTime now = LocalDateTime.now(); // 本地系统时区
ZonedDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC"));

// 格式化(线程安全,可复用实例)
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = now.format(fmt);

// 解析
LocalDateTime parsed = LocalDateTime.parse("2024-01-01 12:00:00", fmt);

// 日期计算(链式,非破坏性,返回新实例)
LocalDateTime tomorrow = now.plusDays(1).withHour(9).withMinute(0);

// 时间戳转换(与旧 Date 互操作)
long epochMilli = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
Date oldDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant()); // 必要时转旧 Date

绝对禁止(Java 17 中仍存在的遗留类,但不应使用):

  • new Date() / Calendar.getInstance():线程不安全,时区混乱
  • SimpleDateFormat:线程不安全(绝对不要作为静态变量),已被 DateTimeFormatter 取代

2.6 金额计算:BigDecimal 正确姿势(替代 decimal)

C# 参考

decimal total = price * quantity; // 精确计算
decimal rounded = Math.Round(total, 2); // 保留 2 位

Java 17 方案

import java.math.BigDecimal;
import java.math.RoundingMode;

// 构造(必须用 String,防止 double 精度污染)
BigDecimal price = new BigDecimal("19.99");      // ✅ 正确
BigDecimal wrong = new BigDecimal(19.99);       // ❌ 错误:19.99 实际是 19.989999999...

// 计算(不可变,返回新实例)
BigDecimal total = price.multiply(BigDecimal.valueOf(quantity));
BigDecimal sum = total.add(shippingFee);

// 除法(必须指定精度和舍入模式,否则可能抛 ArithmeticException)
BigDecimal avg = total.divide(count, 2, RoundingMode.HALF_UP); // 保留 2 位,四舍五入

// 比较(不要用 equals,因为会比较精度 scale)
if (total.compareTo(BigDecimal.ZERO) > 0) { ... } // >0 表示大于零

工具类封装建议

public class MoneyUtils {
    private static final int DEFAULT_SCALE = 2;
    private static final RoundingMode DEFAULT_MODE = RoundingMode.HALF_UP;
    
    public static BigDecimal of(double val) {
        return BigDecimal.valueOf(val); // 内部使用 Double.toString,安全
    }
    
    public static BigDecimal of(String val) {
        return new BigDecimal(val);
    }
    
    public static BigDecimal divide(BigDecimal a, BigDecimal b) {
        return a.divide(b, DEFAULT_SCALE, DEFAULT_MODE);
    }
}

2.7 异常处理:消灭 Checked Exception(@SneakyThrows)

痛点:Java 强迫你处理 IOExceptionSQLException,业务层代码被 try-catch 污染。

方案:Lombok 的 @SneakyThrows(编译期自动转为非受检异常)+ 全局异常处理。

// 业务层:不再写 throws,不再写 try-catch
@SneakyThrows  // 编译期将 IOException 包装为 RuntimeException
public void writeToFile(String content, String path) {
    Files.write(Path.of(path), content.getBytes()); // 原强制 throws IOException
}

// 或更细粒度控制
public void process() {
    try {
        riskyOperation();
    } catch (IOException e) {
        throw new BusinessException("文件操作失败", e); // 转为自定义运行时异常
    }
}

// 全局统一处理(Spring 环境)
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handle(BusinessException e) {
        return ResponseEntity.status(500).body(new ErrorResponse(e.getMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGeneric(Exception e) {
        log.error("系统异常", e);
        return ResponseEntity.status(500).body(new ErrorResponse("系统繁忙"));
    }
}

2.8 属性定义:Lombok @Data / Record(替代自动属性)

C# 参考

public class Order {
    public string OrderNo { get; set; }
    public decimal Amount { get; set; }
}

Java 17 方案

方案 A:Lombok @Data(可变,用于 Entity)

import lombok.Data;

@Data  // 自动生成 getter/setter/equals/hashCode/toString
public class Order {
    private String orderNo;
    private BigDecimal amount;
}
// 等效于手写 30+ 行代码

方案 B:Java Record(不可变,用于 DTO/VO,Java 16+ 标准特性)

public record Order(String orderNo, BigDecimal amount) {
    // 自动生成:private final 字段、全参构造、getter(orderNo() 而非 getOrderNo())、equals、hashCode、toString
    
    // 可添加额外方法或静态工厂
    public static Order empty() {
        return new Order("", BigDecimal.ZERO);
    }
}

// 使用
Order o = new Order("123", new BigDecimal("100"));
System.out.println(o.orderNo()); // 注意:getter 风格是字段名()

Record 注意事项

  • Record 是 final 类,不能被继承
  • 适合纯数据传输对象(DTO),不适合需要修改的 Entity(JPA 实体建议仍用 Lombok @Data)

三、完整的对比速查表

场景 C# 习惯 Java 17 低痛苦写法 避坑提示
空值传播 obj?.Prop Optional.ofNullable(obj).map(o -> o.getProp()).orElse(default) 不要在 Optional 内修改对象
集合筛选 list.Where(x => x.Age > 18) StreamEx.of(list).filter(x -> x.getAge() > 18).toList() 不要用 Collectors.toList()
集合映射 list.Select(x => x.Name) StreamEx.of(list).map(X::getName).toList() 方法引用 X::getNamex -> x.getName() 简洁
集合转Map ToDictionary(x => x.Id, x => x.Name) StreamEx.of(list).toMap(X::getId, X::getName) 原生 Stream 需处理 mergeFunction
排序 OrderByDescending(x => x.Date) StreamEx.of(list).sortedByDescending(X::getDate) 原生需 Comparator.comparing().reversed()
DTO映射 mapper.Map<Dto>(entity) converter.toDto(entity)(MapStruct 生成) 不要用 BeanUtils.copyProperties(反射慢)
异步串行 await A(); await B(); cf.thenCompose(a -> B(a)) Java 17 无 await 语法
异步并行 Task.WhenAll(A(), B()) CompletableFuture.allOf(cf1, cf2).thenApply(...) 注意传递自定义线程池
日期格式化 DateTime.Now.ToString("yyyy-MM-dd") LocalDateTime.now().format(DateTimeFormatter.ofPattern(...)) 不要用 SimpleDateFormat
金额计算 decimal total = price * qty price.multiply(qty).setScale(2, RoundingMode.HALF_UP) 必须用 String 构造 BigDecimal
异常处理 try-catch 或抛 Runtime @SneakyThrows + 全局异常处理 业务层不处理 Checked Exception
属性定义 { get; set; } @Data (Lombok) 或 record Record 适合 DTO,@Data 适合 Entity

四、开发环境检查清单(代码审查用)

提交代码前,确认以下 16 条

集合与流

  • stream().collect(Collectors.toList()) → 应为 StreamEx.of().toList()
  • Comparator.comparing() 冗长写法 → 应为 StreamEx.sortedBy()
  • Collectors.toMap() 的三参数重载(处理 merge)→ 应为 StreamEx 的 toMap

空指针

  • 无超过 2 层的 if (x != null) 嵌套 → 应为 Optional 链
  • obj.getX().getY().getZ() 裸调用 → 应有 Optional 防护或 JsonPath

数据转换

  • 无手写 Converter(setXxx 样板)→ 应有 MapStruct 接口
  • BeanUtils.copyProperties → 应为 MapStruct(性能差 10 倍以上)

异步

  • 无裸用 CompletableFuture.supplyAsync()(不传线程池)→ 应传自定义 Executor
  • 无在主线程调用 future.get()(阻塞)→ 应在回调链中处理

工程规范

  • new Date() / Calendar / SimpleDateFormat → 应为 LocalDateTime
  • double 计算金额 → 应为 BigDecimal(String)
  • new BigDecimal(double) → 应为 new BigDecimal("0.1")BigDecimal.valueOf()
  • 无业务方法声明 throws IOException/SQLException → 应为 @SneakyThrows 或包装为 RuntimeException
  • + 拼接日志字符串 → 应为 log.info("{}", var) 占位符
  • fos.close() 在 finally 外 → 应为 try-with-resources
  • 无返回 Map<String, Object> → 应为强类型 DTO(MapStruct 生成)
  • 无手动 Getter/Setter 样板 → 应为 Lombok @Data 或 Record

五、结语

从 C# 迁移到 Java 17,最大的痛苦不是语言本身,而是历史遗留的样板代码习惯。通过引入 StreamEx、MapStruct、Lombok、LocalDateTime 这四个关键工具,你可以在不升级到 JDK 21(保留 LTS 稳定性)的前提下,恢复 90% 的 .NET 开发体验。

关键心法:Java 生态的优势在于工具链的丰富性——不要试图用原生 API 硬写,而要像使用 LINQ/AutoMapper 一样,引入合适的库来填补语言层面的语法糖缺失

保持 C# 的函数式思维(流式操作、不可变对象、延迟执行),Java 17 也能写出同样优雅的代码。


文档版本:v1.0-for-java17
适用环境:Java 17 LTS + Spring Boot 3.x
建议配合:IntelliJ IDEA(对 Lombok、MapStruct 支持最好)