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

目录

新特性:switch的箭头语法

新特性:switch中的case null

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

在枚举中的实际应用

新特性:模式匹配

智能转型

模式匹配

违反里式替换原则

守卫

支配

sealed的使用


本笔记参考自: 《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时已经成为正式特性,见JEP 441),因此若使用的是JDK 17,需要在编译时添加以下选项:

 --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。


模式匹配

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

违反里式替换原则

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

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

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

        该例子在JDK 17时还处于预览模式,但JDK 21时已经成为正式特性。

import java.util.stream.Stream;// 符合里式替换原则:子类和接口拥有的方法完全一致
// 定义一个接口:生命具有的行为
interface LifeForm {String move();String react();
}// 子类:蠕虫
class Worm implements LifeForm {@Overridepublic String move() {return "Worm::move()";}@Overridepublic String react() {return "Worm::react()";}
}// 子类:长颈鹿
class Giraffe implements LifeForm {@Overridepublic String move() {return "Giraffe::move()";}@Overridepublic 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) { // 选择器表达式pcase 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 p中的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 {@Overridepublic void feed() {}void walk() {}
}final class Fish implements Pet {@Overridepublic 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));}
}

        由于模式匹配并不受到继承结构的束缚,因此我们可以做到这种事:

【例子:传入不同的类型】

import java.util.List;// 使用record关键字生成类:
record XX() {
}public class ObjectMatch {// 使用Object作为参数static String match(Object o) {return switch (o) {case Dog d -> "是只狗";case Fish f -> "是条鱼";case Pet sp -> "既不是狗,也不是鱼";case String s -> "String:" + s;case Integer i -> "Integer:" + i;case String[] sa -> String.join(",", sa);case XX xx -> "XX:" + xx;case null, default -> "剩下的东西";};}public static void main(String[] args) {List.of(new Dog(), new Fish(), new Pet(),"句子", Integer.valueOf(3),Double.valueOf("1.145"),new String[]{"黑", "化", "肥"},new XX()).forEach(s -> System.out.println(match(s)));}
}

        程序执行的结果是:

        不过,如果使用Object作为switch的选择器表达式,编译器会强制要求我们使用default覆盖所有可能值。

守卫

        守卫用于细化匹配条件,使得switch语句不再只是匹配类型这么简单。要完成一次模式匹配,首先需要满足case对于类型的限制,然后再满足守卫条件。守卫可以是任何布尔表达式,它通过when关键字和case的类型判断相连接:

【例子:使用守卫进行精细的匹配】

        同样的,这个特性也是JDK 17中的预览特性,在JDK 21中实现时有了细微的调整(原本使用&&连接守卫条件,但现在改成了when)。

import java.util.List;sealed interface Shape {double area();
}record Circle(double radius) implements Shape {@Overridepublic double area() {return Math.PI * radius * radius;}
}record Rectangle(double side1, double side2)implements Shape {@Overridepublic double area() {return side1 * side2;}
}public class Shapes {static void classify(Shape s) {System.out.println(switch (s) {case Circle c when c.area() < 100.0 -> "小的圆形:" + c;case Circle c -> "大的圆形:" + c;case Rectangle r when r.side1() == r.side2() -> "正方形:" + r;case Rectangle r -> "长方形:" + r;});}public static void main(String[] args) {List.of(new Circle(5.0),new Circle(25.0),new Rectangle(12.0, 12.0),new Rectangle(12.0, 15.0)).forEach(t -> classify(t));}
}

        程序执行的结果是:

        我们还可以编写更为复杂的守卫条件:

case Circle c when c.area() > 20.0 &&c.area() < 100.0 -> c;

在这个特性还是预览状态的时候,我们需要使用圆括号将复杂的守卫条件括起来。不过在实装的时候,这些都不需要了。

支配

        在switch中,case语句的顺序很重要。若基类的case出现在更前面,子类的case就不会有机会被执行。此时基类支配了子类:

        特别是在使用守卫的时候,顺序会变得更加敏感。尽管编译器能够对类型的支配性做出反映,但它无法判断出现在守卫条件中的逻辑错误。

    若在同一个模式上有多个守卫,那么更具体的模式必须出现在更加泛化的模式之前。

        下面的例子展示了当守卫条件中存在逻辑错误时会发生什么:

【例子:有问题的守卫条件】

import java.util.List;record Person(String name, int age) {
}public class People {static String categorize(Person person) {return switch (person) {case Person p when p.age() > 40-> p + "处于中年";case Person p whenp.name().contains("D") || p.age() == 14-> p + "名字中有D,或者年龄是14";case Person p when p.age() >= 100 // 注意这个守卫条件,它被p.age() > 40支配了-> p + "年龄已经超过了一个世纪";case Person p-> p + "是其他的人";};}public static void main(String[] args) {List.of(new Person("A", 15),new Person("D", 42),new Person("BC", 14),new Person("E", 1),new Person("F", 101)).forEach(p -> System.out.println(categorize(p)));}
}

        程序执行的结果是:

        很显然,下面的语句并不可达:

case Person p when p.age() >= 100

这条语句已经被第一个case的守卫条件支配了。

sealed的使用

        在之前的例子中,sealed关键字的出现十分频繁。对于模式匹配而言,sealed是一个很好的辅助,因为其自动生成的代码就是final的。正如下面的这个例子一样:

【例子:sealed的使用】

import java.util.List;sealed interface Transport {
};record Bicycle(String id) implements Transport {
};record Glider(int size) implements Transport {
};record Surfboard(double weight) implements Transport {
};// 编译器会检测类型,因此若有类型未被包含在模式匹配中,编译器就会报错
// record Skis(int length) implements Transport {
// };public class SealedPatternMatch {static String exhaustive(Transport t) {return switch (t) {case Bicycle b -> "Bicycle " + b.id();case Glider g -> "Glider" + g.size();case Surfboard s -> "Surfboard" + s.weight();};}
}

         然而,编译器不会检测null。这是Java为了兼容旧的switch代码做出的调整。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

强化学习求解TSP(五):Qlearning求解旅行商问题TSP(提供Python代码)

一、Qlearning简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于奖励的决策问题。它是一种无模型的学习方法&#xff0c;通过与环境的交互来学习最优策略。Q-learning的核心思想是通过学习一个Q值函数来指导决策&#xff0c;该函数表示在给定状态下采取某个动作所获…

element表格数据,表头上(下)角标,html字符串渲染

1. 问题描述 在动态渲染的element表格中&#xff0c;表头和表中数据是一个含有html的字符串&#xff0c;需要渲染 2. 效果 3. 代码 const columns ref([{ text: 差值<sub>-3</sub> / 10<sup>-6</sup>℃<sup>-1</sup>, value: aallowEr…

简易实现 MyBatis 底层机制

MyBatis 大家好呀&#xff01;我是小笙&#xff0c;我中间有1年没有更新文章了&#xff0c;主要忙于毕业和就业相关事情&#xff0c;接下来&#xff0c;我会恢复更新&#xff01;我们一起努力吧&#xff01; 概述 MyBatis 是一个持久层的框架&#xff08;前身是 ibatis&#x…

5G阅信短信群发助力汽车行业营销拓客!

5G阅信短信群发在汽车行业营销中具有重要作用&#xff0c;可以帮助汽车企业快速触达目标客户群体&#xff0c;提高品牌知名度和销售业绩。以下是一些应用5G阅信短信群发助力汽车行业营销拓客的策略&#xff1a; 1.精准定位目标客户&#xff1a;通过分析客户 数据和行为&#xf…

计算机体系结构----计分板(scoreboard)算法

计分板算法简介 计分板记录着所有必要的信息&#xff0c;用来控制以下事情&#xff1a; 每条指令何时可以读取操作数并投入运行&#xff08;对应着RAW冲突的检测&#xff09;每条指令何时可以写入结果&#xff08;对应着WAR冲突的检测&#xff09;在计分板中&#xff0c;WAW冲…

Redis性能大挑战:深入剖析缓存抖动现象及有效应对的战术指南

在实际应用中&#xff0c;你是否遇到过这样的情况&#xff0c;本来Redis运行的好好的&#xff0c;响应也挺正常&#xff0c;但突然就变慢了&#xff0c;响应时间增加了&#xff0c;这不仅会影响用户体验&#xff0c;还会牵连其他系统。 那如何排查Redis变慢的情况呢&#xff1f…

springboot私人健身与教练预约管理系统源码和论文

随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&#xf…

[SAP] 解决程序提示被某用户使用或锁定的问题

在最近使用ABAP编辑器对Z437_TEST_2024程序进行修改操作时&#xff0c;出现程序提示"使用者 ABAP5110 当前编辑 Z437_TEST_2024"的情况&#xff0c;如下图所示 解决上述被某用户使用或锁定的方法如下 ① 在事务码输入框中输入事务码SM12&#xff0c;并输入用户名(使…

宋仕强论道之华强北精神和文化(二十一)

华强北的精神会内化再提炼和升华成为华强北文化&#xff0c;在外部会流传下去和传播开来。在事实上的行动层面&#xff0c;就是华强北人的思维方式和行为习惯&#xff0c;即见到机会就奋不顾身敢闯敢赌&#xff0c;在看似没有机会的时候拼出机会&#xff0c;和经济学家哈耶克企…

【Python学习】Python学习12-字典

目录 【Python学习】Python学习12-字典 前言创建语法访问列表中的值修改与新增字典删除字典元素Python字典内置函数&方法参考 文章所属专区 Python学习 前言 本章节主要说明Python的字典&#xff0c;是可变的容器&#xff0c;每个字典由键值对组成用冒号隔开&#xff0c;…

css深度选择器 /deep/

一、/deep/的含义和使用 /deep/ 是一种 CSS 深度选择器&#xff0c;也被称为深度组合器或者阴影穿透组合器&#xff0c;主要用在 Web 组件样式封装中。 在 Vue.js 或者 Angular 中&#xff0c;使用了样式封装技术使得组件的样式不会影响到全局&#xff0c;也就是说组件内部的…

激活函数整理

sigmoid函数 import torch from d2l import torch as d2l %matplotlib inline ​ xtorch.arange(-10,10,0.1,requires_gradTrue) sigmoidtorch.nn.Sigmoid() ysigmoid(x) ​ d2l.plot(x.detach(),y.detach(),x,sigmoid(x),figsize(5,2.5)) sigmoid函数连续、光滑、单调递增&am…