Java 21 switch模式匹配实战:干掉业务代码里那坨if-else

2026-08-31 0 960

最近把公司的一个老模块升级到JDK21,虽然只是升个级,但我顺手把里面一个又臭又长的if-else链给重构了。原来那个方法大概有一百多行,里面全是`instanceof`判断,还夹着各种类型转换,每次改动都提心吊胆。改用Java 21的switch模式匹配之后,代码砍掉了六十行,而且逻辑清晰得多了。这篇文章就是记录一下这个过程,顺便聊聊模式匹配到底咋用。

以前处理不同类型得这么写

业务场景挺简单的:用户提交一个动作请求,后端要根据请求里的`payload`类型走不同的处理逻辑。比如有`TextMessage`、`ImageMessage`、`VoiceMessage`,每种消息的数据结构不一样。老代码是这样的:

public void handleMessage(Message msg) {
    if (msg instanceof TextMessage text) {
        // 处理文本消息,需要拿到text.content()
        System.out.println("处理文本: " + text.content());
        //... 后面可能还有几十行
    } else if (msg instanceof ImageMessage image) {
        // 处理图片消息,需要拿到image.url()和image.size()
        System.out.println("处理图片: " + image.url() + " 大小 " + image.size());
    } else if (msg instanceof VoiceMessage voice) {
        // 处理语音消息
        System.out.println("处理语音: " + voice.duration() + "秒");
    } else if (msg instanceof VideoMessage video) {
        // 处理视频消息
        System.out.println("处理视频: " + video.videoUrl());
    } else {
        throw new IllegalArgumentException("未知类型: " + msg);
    }
}

看着不算乱,但是如果每个分支里再加一些逻辑,比如校验权限、保存日志、查数据库,这个`else if`就慢慢变成屎山了。更要命的是,以后如果增加一种`FileMessage`类型,你还得复制粘贴一个`else if`块,很容易漏掉某个地方,或者类型转换出错。

Java 21的switch模式匹配能干啥

Switch表达式以前只能匹配整数、枚举、字符,Java 17开始支持类型模式,Java 21完善了很多。现在你可以直接用`case`后面跟类型来匹配,不用再写`instanceof`了。而且Java 21的switch可以自动对类型进行模式绑定,用`when`子句还能加上守卫条件。听起来很玄乎,直接看上面那个例子改成switch之后的样子:

public void handleMessage(Message msg) {
    if (msg == null) {
        throw new IllegalArgumentException("消息不能为空");
    }

    switch (msg) {
        case TextMessage text -> {
            System.out.println("处理文本: " + text.content());
            // 区块里可以有更多语句
        }
        case ImageMessage image -> System.out.println("处理图片: " + image.url() + " 大小 " + image.size());
        case VoiceMessage voice -> System.out.println("处理语音: " + voice.duration() + "秒");
        case VideoMessage video -> System.out.println("处理视频: " + video.videoUrl());
        default -> throw new IllegalArgumentException("未知类型: " + msg);
    }
}

注意看,每个`case`后面直接跟类型名,后面跟一个变量名,这样不用再单独写`instanceof`和变量声明。比如`case TextMessage text`就相当于原来的`msg instanceof TextMessage text`,而且是Java 21严格模式下的类型判断。这种写法更精准,`switch`的目标类型是`Message`,所以那些未知的子类都会被`default`接住。

如果分支里只有一行,不用花括号也可以。我习惯统一加花括号,防止以后往里面加代码的时候忘了括号。

更复杂的场景:带上守卫条件

很多时候光匹配类型是不够的,比如文本消息有敏感词的时候要特殊处理,图片消息超过2MB要压缩。以前这么写:

if (msg instanceof TextMessage text && text.content().contains("敏感")) {
    // 拦截敏感内容
} else if (msg instanceof TextMessage text && text.content().length() > 100) {
    // 长文本处理
} else if (msg instanceof ImageMessage image && image.size() > 2048) {
    // 超大的图片处理
}

换成switch模式匹配,可以用`when`关键字加条件。注意必须写在冒号或箭头前面:

switch (msg) {
    case TextMessage text when text.content().contains("敏感") -> System.out.println("拦截敏感文本");
    case TextMessage text when text.content().length() > 100 -> System.out.println("处理长文本");
    case ImageMessage image when image.size() > 2048 -> System.out.println("压缩图片");
    // 兜底逻辑
    default -> System.out.println("正常处理");
}

这里有个细节要注意,`when`子句里的条件如果抛异常,整个switch会直接抛出该异常,不会跳到下一个case。所以写条件的时候尽量避免可能抛异常的操作,或者提前try-catch。

再配合记录模式一起用

要是你的消息类型是一个record,比如`TextMessage`定义成了`record TextMessage(String content) {}`,那switch模式匹配可以直接解构出组件值,这叫做“记录模式”。这么一来,甚至不用调用`content()`方法了,直接拿到变量。看例子:

record TextMessage(String content) {}
record ImageMessage(String url, int size) {}
record VoiceMessage(int duration) {}

public void handleMessage(Message msg) {
    switch (msg) {
        case TextMessage(String content) -> System.out.println("文本内容: " + content);
        case ImageMessage(String url, int size) -> System.out.println("图片地址: " + url + ", 大小: " + size + "KB");
        case VoiceMessage(int duration) -> System.out.println("语音时长: " + duration + "秒");
        default -> throw new IllegalArgumentException("未知消息");
    }
}

这个就非常爽了,连getter都不用调。而且如果record里还有嵌套record,也一样可以继续解构。比如`ReplyMessage`里面包含`TextMessage`和`UserInfo`,可以写成这样:

record UserInfo(String name) {}
record ReplyMessage(TextMessage text, UserInfo user) {}

case ReplyMessage(TextMessage(String content), UserInfo(String name)) -> {
    System.out.println(name + " 回复: " + content);
}

这种嵌套解构在以前得写一堆判空和转换,现在一行搞定。不过要注意,如果record的某个组件是null,模式匹配会失败,不会进入这个case。所以最好提前判断好。

重构一个真实的订单折扣模块

光说没用,来一个我在项目里实际重构的案例。原来是一个订单折扣计算,根据用户类型和支付方式打折。规则是这样的:普通用户打9折,会员打8折,企业用户打7折;使用支付满减、优惠券等情况还有额外折扣。我简化一下,只保留核心骨架,感受一下模式匹配的威力。

先定义一些record:

record User(String name, String type) {}
record Order(double amount) {}
record Coupon(String code, double discount) {}
record Payment(String channel) {}

老代码可能长这样:

public double calculate(Order order, User user, Payment payment, Coupon coupon) {
    double result = order.amount();

    if ("normal".equals(user.type())) {
        result = result * 0.9;
    } else if ("member".equals(user.type())) {
        result = result * 0.8;
    } else if ("enterprise".equals(user.type())) {
        result = result * 0.7;
    } else {
        throw new IllegalArgumentException("未知用户类型");
    }

    if ("coupon".equals(payment.channel()) && coupon != null) {
        result = result - coupon.discount();
    }

    return result;
}

这种代码很容易出现一个问题:如果用户类型和支付方式有各种组合,你就要写很多嵌套if。现在用switch模式匹配,把不同组合直接变成case。不过为了演示,我把支付方式单独抽成一个方法:

public double calculate(Order order, User user, Payment payment, Coupon coupon) {
    double base = applyUserDiscount(order.amount(), user);
    return applyPaymentDiscount(base, payment, coupon);
}

private double applyUserDiscount(double amount, User user) {
    return switch (user) {
        case User u when "normal".equals(u.type()) -> amount * 0.9;
        case User u when "member".equals(u.type()) -> amount * 0.8;
        case User u when "enterprise".equals(u.type()) -> amount * 0.7;
        default -> throw new IllegalArgumentException("未知用户类型");
    };
}

private double applyPaymentDiscount(double amount, Payment payment, Coupon coupon) {
    return switch (payment) {
        case Payment p when "coupon".equals(p.channel()) && coupon != null -> 
            amount - coupon.discount();
        case Payment p when "points".equals(p.channel()) -> 
            amount * 0.95;
        default -> amount;
    };
}

这样一看,每个方法只做一件事,而且switch把类型和条件组合得明明白白的。以后想加一种支付方式,只需要在对应switch里加一个case,不会影响到别的逻辑。

有哪些坑要注意

1. 别忘了case的穿透问题

只有用`->`箭头形式的switch才不会穿透。如果你还在用老的`case xxx:`写法,那得自己加`break`。Java 21里推荐用箭头,简洁而且安全。

2. switch表达式的穷举性

如果你把switch当表达式用(有返回值),比如`double result = switch(…) {…};`,那必须穷举所有可能,或者有`default`。否则编译都过不了。我一般都会加个`default`,这样万一有新的未知类型,还能给个兜底逻辑。

3. 类型模式匹配的顺序

如果多个case有继承关系,比如`case Dog dog`和`case Animal animal`,那子类要写在前面,否则后面的`Dog`分支永远不会执行,编译器也会报错。但由于我们通常用的都是record或者final类,这个坑碰到的概率不大。

4. null的处理

switch模式匹配默认不匹配null,会直接抛出`NullPointerException`。所以最好在最前面判断一下null,或者用`case null`(Java 21支持)来专门处理。比如`case null -> throw new IllegalArgumentException(“null”)`。

一些个人感受

升级到Java 21有一段时间了,最大的感受是写代码变得顺畅了。以前很多需要`if-else`叠加`instanceof`的胶水逻辑,现在用switch模式匹配就非常优雅。不过也别指望它能解决所有问题,如果你的业务里乱到连类型都分不清,还是先去把设计梳理好。

这次重构之后,代码的行数减少了很多,但可读性反而提高了。最明显的是测试用例不用再一个个分支去判断了,因为每个case自己就是一个完整的分支,出问题直接定位到那一段。希望这个案例能给你一些启发。

Java 21 switch模式匹配实战:干掉业务代码里那坨if-else
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java 21 switch模式匹配实战:干掉业务代码里那坨if-else https://www.taomawang.com/server/java/2675.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务