Java进阶 1-3 枚举(switch的新特性)

news2024/11/18 19:44:25

本笔记参考自: 《On Java 中文版》


        在Java中,“模式匹配”经历过好几个版本的功能扩充。这些扩充和switch关键字密切相关。如果对Java最新的特性感兴趣,可以查看Java增强建议(JEP)。

新特性:switch的箭头语法

        switch的箭头语法在JDK 14加入到了Java之中。现在,我们可以通过这种新的语法来使用switch

【例子:新的switch语句】

import static java.util.stream.IntStream.range;

public class ArrowInSwitch {
    static void arrows(int i) {
        switch (i) {
            case 1 -> System.out.println("......"); // 可以是一条语句
            case 2 -> { // 也可以是语句块
                // ...
            }
            default -> System.out.println();
        }
    }

    public static void main(String[] args) {
        range(0, 4).forEach(i -> arrows(i));
    }
}

        这种语法与传统的switch语法的一个区别在于,它舍弃了原本需要写在每个case后面的break。这使得句式整体看上去更加简洁了。

        注意:不能在同一个switch中同时使用冒号和箭头。

新特性:switch中的case null

        JDK 17的一项预览功能,允许我们在switch语句中使用case null(这原本是非法的)。

import java.util.function.Consumer;

public class CaseNull {
    // 原本只能先进行非空判断:
    static void old(String s) {
        if (s == null) {
            System.out.println("null");
            return;
        }

        switch (s) {
            case "X" -> System.out.println("X");
            default -> System.out.println("default");
        }
    }

    // 预览功能允许我们在switch语句中为null新增一条分支语句
    static void checkNull(String s) {
        switch (s) {
            case "X" -> System.out.println("X");
            case null -> System.out.println("null");
            default -> System.out.println("default");
        }

        // 冒号语法也可以使用null了:
        switch (s) {
            case "X":
                System.out.println("X");
                break;
            case null:
                System.out.println("null");
                break;
            default:
                System.out.println("default");
        }
    }

    // 用于测试:
    static void test(Consumer<String> cs) {
        cs.accept("X");
        cs.accept("Y");
        try {
            cs.accept(null);
        } catch (NullPointerException e) {
            System.out.println(e.getMessage());
        }
    }

    public static void main(String[] args) {
        test(CaseNull::old);
        test(CaseNull::checkNull);
    }
}

        程序执行的结果是:

        由于在JDK 17时该功能还是预览状态(JDK 21时已经成为正式特性),因此我们需要将下面的语句和javac配合使用,才能成功编译:

 --enable-preview -source 17

        值得一提的是,default并不会覆盖null这一情况,因此下面的方法在使用时可能引发报错:

static void defaultOnly(String s) {
    switch (s) {
        case "X" -> System.out.println("X");
        default -> System.out.println("default");
    }
}

        可以使用例子中的test()方法对其进行测试,会得到如下结果:

    switch无法如其设计时预计的那样覆盖所有的可能值,这是Java为了向后兼容做出的让步。

        还有一件事,现在的switch语句支持通过逗号合并多种模式。这意味着我们可以做到合并nulldefault,这样就不会出现上面的这种报错了:

case null, default -> System.out.println("null|default");

        不过最新的JDK 21还是做出了一些细微的改动,比如下面的语句在JDK 21中是不合法的(或许是处于安全性的考虑):

新特性:将switch作为表达式使用

        JDK 14添加的这项新功能,允许我们将switch作为表达式使用。现在,switch语句也可以返回值了:

【例子:有返回值的switch语句】

public class SwitchExpression {
    static int colon(String s) {
        var result = switch (s) {
            case "i":
                yield 1;
            case "j":
                yield 2;
            case "k":
                yield 3;
            default:
                yield 0;
        };

        return result;
    }

    static int arrow(String s) {
        var result = switch (s) {
            case "i" -> 1;
            case "j" -> 2;
            case "k" -> 3;
            default -> 0;
        };

        return result;
    }

    public static void main(String[] args) {
        for (var s : new String[]{"i", "j", "k", "z"})
            System.out.format(
                    "%s %d %d%n", s, colon(s), arrow(s));
    }
}

        程序执行的结果是:

        如例子所示,现在我们可以通过yield关键字从switch语句中返回一个值。需要注意一点,break不能和yield并用。这是合理的,因为可以假定,当我们使用yield的时候,一定存在一个需要返回值的变量。

        不过yield不能这样用:

很显然,yield必须存在于case语句中。

        一个case想要包含多条语句,就需要使用大括号。在此之上,若需要返回值,那么即使使用箭头语法,我们也需要在case语句中添加yield。像这样:

在枚举中的实际应用

        之前也曾举过“信号灯”的例子(笔记 1-1),现在我们可以使用表达式的形式重写一遍之前的代码:

【例子:重写“信号灯”变化】

public class EnumSwitch {
    enum Signal {
        GREEN,
        YELLOW,
        RED
    }

    static Signal color = Signal.RED;

    public static void change() {
        color = switch (color) {
            case RED -> Signal.GREEN;
            case GREEN -> Signal.YELLOW;
            case YELLOW -> Signal.RED;
        };
    }

    public static void main(String[] args) {
        for (int i = 0; i < 7; i++) {
            change();
            System.out.println(color);
        }
    }
}

        程序执行的结果是:

        编译器会对使用了枚举的switch进行检测。因此若我们没有考虑某条路径,编译器就会报错:

因此,使用枚举可以使我们的switch语句更加安全。

新特性:模式匹配

        JEP 394对instanceof的功能进行了增强。虽说官方的名称是“instanceof的模式匹配”,但更准确的说法应该是“模式匹配辅助”。

智能转型

        下面的例子将要展示的,是其中用于支持模式匹配的一个特性,它在其他的一些语言中被称为“智能转型”。

【例子:用于支持模式匹配的特性】

public class SmartCasting {
    // 旧的写法:
    static void dumb(Object x) {
        if (x instanceof String) {
            String s = (String) x;
            if (s.length() > 0) {
                System.out.format(
                        "%d %s%n", s.length(), s.toUpperCase());
            }
        }
    }

    // 新的写法(省略了类型转换):
    static void smart(Object x) {
        if (x instanceof String s && s.length() > 0) {
            System.out.format(
                    "%d %s%n", s.length(), s.toUpperCase());
        }
    }

    // 错误的写法:
    static void wrong(Object x) {
        // 在这里,“或”这一逻辑并不成立
        // 编译器不允许这一写法,因此s会被视作无法解析的符号
        // if (x instanceof String s || s.length() > 0) {
        // }
    }

    public static void main(String[] args) {
        dumb("dumb");
        smart("smart");
    }
}

        程序执行的结果是:

        smart()展示了这一特性的实际应用:一旦确定了类型,我们就可以跳过转型的步骤。在这里,x instanceof String s会自动为我们创建一个新的变量ss的正式名称应该是【模式变量】)

    这个特性可以被作为模式匹配的构建块使用。

        还需注意一点,模式变量的作用域与一般而言的作用域并不完全重合。JEP 394中说明,模式变量的作用域设计来自于流的概念,只有当编译器确定该变量能够被安全赋值时,模式变量才会成立(这也解释了为什么在上述例子中无法使用【||】,因为编译器无法保证程序能够运行到那里)

        这个特性会导致一些看起来十分费解的极端情况:

【例子:奇怪的作用域范围】

public class OddScoping {
    static void f(Object o) {
        if (!(o instanceof String s)){
            System.out.println("这个变量不是String类型");
            throw new RuntimeException();
        }

        // s在(看起来)超出作用域的地方发挥了它的作用
        System.out.println(s.toUpperCase());
    }

    public static void main(String[] args) {
        f("Ab aB");
        f(null);
    }
}

        程序执行的结果是:

        如果不抛出异常,就会引发报错:

        若不抛出异常,就意味着当if语句内部处理完类型不匹配的情况后,程序将会调用类型未知的变量s。编译器无法保证变量s的类型,因此此处的s实际上无法被使用。

    因此,也可能会因为这个特性的使用而造成一些费解的Bug。


模式匹配

        正如之前所述,“智能替换”是为模式匹配服务的。与继承的多态类似,模式匹配也能够实现基于类型的行为。但与继承不同的是,模式匹配不会要求所有的类型具有相同的接口,或是处于相同的继承结构中。

违反里式替换原则

||| 里式替换原则:所有引用基类的地方必须能透明地使用其子类的对象。

        在里式替换原则中,子类全部具有相同相同的基类,并且只会使用公共基类中定义的方法。例如:

【例子:遵守里式替换原则的情况】

import java.util.stream.Stream;

// 符合里式替换原则:子类和接口拥有的方法完全一致
// 定义一个接口:生命具有的行为
interface LifeForm {
    String move();

    String react();
}

// 子类:蠕虫
class Worm implements LifeForm {
    @Override
    public String move() {
        return "Worm::move()";
    }

    @Override
    public String react() {
        return "Worm::react()";
    }
}

// 子类:长颈鹿
class Giraffe implements LifeForm {
    @Override
    public String move() {
        return "Giraffe::move()";
    }

    @Override
    public String react() {
        return "Giraffe::react()";
    }
}

public class NormalLiskov {
    public static void main(String[] args) {
        Stream.of(new Worm(), new Giraffe())
                .forEach(lifeForm -> System.out.println(
                        lifeForm.move() + " " + lifeForm.react()));
    }
}

        程序执行的结果是:

        这种做法存在一个漏洞:“蠕虫”和“长颈鹿”终究是不同的生物。它们独特的行为(像蠕虫的爬行或长颈鹿的奔跑)难以在基类中进行描述。

    Java的集合库就遇到了这样的问题,他们的解决方式是添加一些“可选”的方法,让子类自行决定是否实现(然而,最终得到的成果并不尽人意)。

        当然,在实际使用Java中可以发现,Java也会允许我们写出如下的这种代码:

【例子:在继承结构中扩展子类】

public class Pet {
    void feed() {
    }
}

class Dog extends Pet {
    void walk() {
    }
}

class Fish extends Pet {
    void changeWater() {
    }
}

        这种程序的设计思路来源于SmallTalk:利用已有的类增加方法,实现代码的复用。然而,这种借鉴是存在局限性的,因为Java是一门基于静态类型的语言,而SmallTalk是动态的。

    SmallTalk具有动态类型检查,这意味着在进行类型相关的一些操作时,SmallTalk更不容易引发安全问题。

        模式匹配的存在允许Java编写出违反里式替换原则,并且更加安全的代码。下面的例子会为每一个可能的类型进行检测,并且进行不同的处理:

【例子:更加安全的模式匹配】

import java.util.List;

public class PetPatternMatch {
    static void careFor(Pet p) {
        switch (p) { // 选择器表达式p
            case Dog dog -> dog.walk();
            case Fish fish -> fish.changeWater();
            // case Pet必须存在,用于覆盖所有的可能值
            case Pet sp -> sp.feed();
        }
    }

    static void petCare() {
        List.of(new Dog(), new Fish())
                .forEach(p -> careFor(p));
    }
}

        在模式匹配出现之前,switch语句只接受基本类型和对应的包装类。换言之,模式匹配扩展了switch语句可以持有的类型范围。

    这种做法和动态绑定的不同之处在于,switch将对不同类型的操作交由case语句处理。

        事实上,在上面的例子中关于基类Petcase语句(case Pet)并不是必要的。为了安全性,编译器会要求我们使用Pet覆盖所有的可能值。如果要解释这种行为,是因为Pet也可能被其他的文件使用,甚至在其他文件中存在未知的子类。

        换言之,我们可以密封基类接口(使用sealed关键字做到这一点),来证明其的安全性:

【例子:通过密封接口优化程序】

import java.util.List;

// 密封的接口:
sealed interface Pet {
    void feed();
}

final class Dog implements Pet {
    @Override
    public void feed() {
    }

    void walk() {
    }
}

final class Fish implements Pet {
    @Override
    public void feed() {
    }

    void changeWater() {
    }
}

public class PetPatternMatch2 {
    static void careFor(Pet p) {
        // sealed关键字可以确保Pet接口只在本文件内使用
        // 因此可以保证不会出现未发现的Pet子类
        switch (p) {
            case Dog d -> d.walk();
            case Fish f -> f.changeWater();
        }
    }

    static void petCare() {
        List.of(new Dog(), new Fish())
                .forEach(p -> careFor(p));
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1371551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java爬虫获取省市区镇村5级行政区划

公司有个项目需要五级行政区划&#xff0c;没有现成的数据&#xff0c;写了一段代码&#xff0c;从gj统计j获取的数据。记录一下。 1.引入maven解析html <!-- jsoup --> <dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifa…

nextjs + ahooks 报错 Cannot use import statement outside a module

在 nextjs 中使用 ahooks 时&#xff0c;报错 SyntaxError: Cannot use import statement outside a module&#xff0c;如下图所示&#xff1a; 解决方案 transpilePackages 官网介绍 Next.js can automatically transpile and bundle dependencies from local packages (lik…

MT36291 2.5A 高效的1.2MHz电流模式升压转换器 DCDC管理芯片 航天民芯

描述 MT36291是一个恒定频率、6引脚SOT23电流模式升压转换器&#xff0c;旨在用于小型、低功耗的应用。MT36291的开关频率为1.2MHz&#xff0c;并允许使用2mm或更低高度的微小、低成本的电容器和电感器。内部软启动导致注入电流小&#xff0c;延长电池寿命。MT36291的特点是在光…

Linux 常用指令汇总

Linux 常用指令汇总 文章目录 Linux 常用指令汇总[toc]前言一、文件目录指令pwd 指令ls 指令cd 指令mkdir 指令rmdir 指令tree 指令cp 指令rm 指令mv 指令cat 指令more 指令less 指令head 指令tail 指令echo 指令> 指令>> 指令 二、时间日期指令date 指令cal 指令 三、…

20240110在ubuntu20.04下重启samba服务

20240110在ubuntu20.04下重启samba服务 百度搜索&#xff1a;samba restart https://www.python100.com/html/78028.html 重启samba命令详解 更新&#xff1a;2023-05-17 16:04 一、重启samba命令 重启samba可以使用以下命令&#xff1a; /etc/init.d/smb restart 或者 syste…

探索AI技术的奥秘:揭秘人工智能的核心原理

目录 前言 学习AI要看的第一本书 人工智能应当以人为本 史蒂芬卢奇&#xff08;Stephen Lucci&#xff09; 萨尔汗M穆萨&#xff08;Sarhan M . Musa&#xff09; 丹尼科佩克&#xff08;Danny Kopec&#xff09;&#xff08;已故&#xff09; 通晓六点&#xff0c;明白…

IPV6学习记录

IPV6的意义 从广义上来看IPV6协议包含的内容很多: IPV6地址的生成与分配 IPV6的报头的功能内容 IPV4网络兼容IPV6的方案 ICMPv6的功能(融合了arp和IGMP功能) IPV6的路由方式 ipv6的诞生除了由于ipv4的地址枯竭外&#xff0c;很大程度上也是因为ipv4多年的发展产生了很多…

网络的设置

一、网络设置 1.1查看linux基础的网络设置 网关 route -n ip地址ifconfigDNS服务器cat /etc/resolv.conf主机名hostname路由 route -n 网络连接状态ss 或者 netstat域名解析nslookup host 例题&#xff1a;除了ping&#xff0c;什么命令可以测试DNS服务器来解…

Python 与 PySpark数据分析实战指南:解锁数据洞见

&#x1f482; 个人网站:【 海拥】【神级代码资源网站】【办公神器】&#x1f91f; 基于Web端打造的&#xff1a;&#x1f449;轻量化工具创作平台&#x1f485; 想寻找共同学习交流的小伙伴&#xff0c;请点击【全栈技术交流群】 数据分析是当今信息时代中至关重要的技能之一。…

Blazor 错误笔记

1. 运行时问题 Microsoft.NETCore.App.Runtime.Mono.browser-wasm Microsoft.NETCore.App.Runtime.Mono.browser-wasm 是一个 .NET Core 运行时的包&#xff0c;用于在浏览器中运行 .NET Core 应用程序。它是针对 WebAssembly 架构的 .NET Core 运行时&#xff0c;可以在浏览…

自动修复vcruntime140.dll丢失的解决办法,快速解决dll文件问题

在使用电脑时也会有不少用户都遇到vcruntime140.dll丢失的情况&#xff0c;那么有什么办法可以解决vcruntime140.dll丢失呢&#xff1f;今天将给大家分享一些关于vcruntime140.dll丢失的解决办法&#xff0c;从自动修复和手动修复两个方向给大家分析希望能够帮助到大家。 一.vc…

Phoenix基本使用

1、Phoenix简介 1.1 Phoenix定义 Phoenix是HBase的开源SQL皮肤。可以使用标准JDBC API代替HBase客户端API来创建表&#xff0c;插入数据和查询HBase数据。 1.2 Phoenix特点 容易集成&#xff1a;如Spark&#xff0c;Hive&#xff0c;Pig&#xff0c;Flume和Map Reduce。性能…

51-6 Vision Transformer ,ViT 论文精读

李沐&#xff08;沐神&#xff09;、朱毅讲得真的好&#xff0c;干货蛮多&#xff0c;值得认真读很多遍&#xff0c;甚至可以当成多模态大模型基础课程学习。 论文原文: An image is worth 16x16 words: transformers for image recognition at scale。 ViT取代了CNN&#xf…

给大家带来三个图片格式转换工具以及步骤

在处理图片时&#xff0c;我们经常需要将图片格式转换为JPG格式。JPG格式是一种常见的图片格式&#xff0c;它具有较好的压缩效果和兼容性&#xff0c;适用于各种不同的应用场景。下面将介绍如何将图片格式转换为JPG格式。 方法一&#xff1a;水印云工具 1. 打开我们的软件。…

Q-BENCH: A BENCHMARK FOR GENERAL-PURPOSEFOUNDATION MODELS ON LOW-LEVEL VISION

继续分享Q系列文章&#xff0c;今天分享Q-BENCH。 简单来说&#xff0c;作者对MLLMs在lowlevel领域中的评价提出了一个测试基准&#xff0c;主要分三点进行讲述。 有点类似于综述&#xff0c;显然作者团队在MLLM的lowlevel领域属于开山之作了。 第一个基准叫感知Perception 简…

朴素贝叶斯进行垃圾邮件分类的python简单示例实现

朴素贝叶斯&#xff08;Naive Bayes&#xff09;是一种基于贝叶斯定理的分类算法&#xff0c;它的核心思想是利用特征之间的独立性来简化分类问题。具体来说&#xff0c;朴素贝叶斯假设每个特征与其他特征之间是相互独立的&#xff0c;即每个特征对于分类结果的影响是相互独立的…

网络安全B模块(笔记详解)- Web渗透测试

Web渗透测试 1.通过渗透机Kali1.0对服务器场景PYsystem20192进行Web渗透测试(使用工具w3af的对目标Web服务器进行审计),在w3af的命令行界面下,使用命令列出所有用于审计的插件,将该操作使用的命令作为Flag值提交; 进入kali命令控制台中使用命令w3af_console进入w3af命令…

Java IO学习和总结(超详细)

一、理解 I/O 是输入和输出的简写&#xff0c;指的是数据在计算机内部和外部设备之间的流动。简单来说&#xff0c;当你从键盘输入数据、从鼠标选择操作&#xff0c;或者在屏幕上看到图像&#xff0c;这些都是 I/O 操作。它就像是计算机与外部世界沟通的桥梁&#xff0c;没有 I…

Zookeeper设计理念与源码剖析

Zookeeper 架构理解 整体架构 Follower server 可以直接处理读请求&#xff0c;但不能直接处理写请求。写请求只能转发给 leader server 进行处理。最终所有的写请求在 leader server 端串行执行。&#xff08;因为分布式环境下永远无法精确地确认不同服务器不同事件发生的先后…

【Flink精讲】Flink数据延迟处理

面试题&#xff1a;Flink数据延迟怎么处理&#xff1f; 将迟到数据直接丢弃【默认方案】将迟到数据收集起来另外处理&#xff08;旁路输出&#xff09;重新激活已经关闭的窗口并重新计算以修正结果&#xff08;Lateness&#xff09; Flink数据延迟处理方案 用一个案例说明三…