现在所有的基础模块都已准备好,我们可以以更全局的视角来看看模式匹配了。不过要记住,Java 的模式匹配仍在开发中,和成熟的模式匹配系统(如 Kotlin、Scala,以及 Python 中的那些)相比,功能还相当有限。
Java 团队还打算加入更多的特性。可能要到好几年后,才会看到完整形式的 Java 模式匹配。
基于继承的多态性实现了基于类型的行为,但是要求这些类型都在同一个继承层次结构中。而模式匹配也实现了基于类型的行为,却并不要求类型全都具有相同的接口,或都处于相同的继承层次结构中。
这是一种不同的反射使用方式。你仍然需要在运行时确定类型,但该方式比反射更加 正式、更加结构化。
如果感兴趣的类型全都具有共同的基类,并且只会使用公共基类中定义的方法,那么就遵循了里氏替换原则(Liskov Substitution Principle,LSP )。在这种情况下,模式匹配就不是必需的了——只需使用普通的继承多态性即可。
NormalLiskov.java
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(lf -> System.out.println(
lf.move() + " " + lf.react()));
}
}
运行结果如下:
所有的方法都完全定义在 LifeForm 接口中,实现类中也并没有增加任何新的方法。
但如果需要增加难以放在基类中的方法呢?比如某些蠕虫身体被切断后可以再生,而长颈鹿肯定做不到。长颈鹿可以踢人,但是很难想象怎样可以在基类中表现出该行为,同时又不会使 Worm (蠕虫)错误地实现(该行为)。Java集合库遇到了这种问题,并试图以一种笨拙的方式解决:在基类中增加“可选”方法,某些子类可以实现该方法,而另一些子类则不实现。这种方法遵循了里氏替换原则,但造成了混乱的设计。
Java 的基本灵感来自一种叫作 SmallTalk 的动态语言,它会利用已有的类并增加方法,以此实现代码复用。以 Smallalk 的设计方式来实现一套“pet"(宠物)继承层次结构,可能最终看起来是这样的:
Pet.java
public class Pet {
void feed() {
}
}
class Dog extends Pet {
void walk() {
}
}
class Fish extends Pet {
void changeWater() {
}
}
我们利用了基本 Pet 的功能性,并通过增加我们需要的方法扩展了该类。这不同于本书中一贯的主张(正如 NormalLiskov.java 所示)应该仔细地设计基类,以包含所有继承层次结构中可能需要的方法,因此遵循了里氏替换原则。虽然这是个很有抱负的目标,但也有些不切实际。试图将基于动态类型的 SmallTalk 模型强制引入基于静态类型的 Java 系统,这必然会造成妥协。在某些情况下,这些妥协可能是行不通的。模式匹配允许使用 SmallTalk 的方式向子类添加新方法,同时仍然保持里氏替换原则的大部分形式。基本上,模式匹配允许违反里氏替换原则,而不产生不可控的代码。
有了模式匹配,就可以通过为每种可能的类型进行检查并编写不同的代码,来处理 Pet 继承层次结构的非里氏替换原则的性质:
PetPatternMatch.java JDK 17
import java.util.List;
public class PetPatternMatch {
static void careFor(Pet p) {
switch (p) {
case Dog d -> d.walk();
case Fish f -> f.changeWater();
case Pet sp -> sp.feed();
}
}
static void petCare() {
List.of(new Dog(), new Fish())
.forEach(p -> careFor(p));
}
}
switch§ 中的 p 称为选择器表达式(selector expression)。在模式匹配诞生之前,选择器表达式只能是完整的基本类型(char、byte、short 或 int)、对应的包装类形式 (Character、Byte、Short 或 Integer)、String 或 enum 类型。有了模式匹配,选择器表达式扩展到可以支持任何引用类型。此处的选择器表达式可以是 Dog、Fish 或 Pet。
注意,这和继承层次结构中的动态绑定类似,但是并没有将不同类型的代码放在重写的方法中,而是将其放在了不同的 case 表达式中。
编译器强制增加了 case Pet,因为该类可以在不是 Dog 或 Fish 的情况下仍然合法地存在。如果没有增加 case Pet,switch 就无法覆盖所有可能的输入值。为基类使用接口可以消除该约束,但会增加另一个约束。下面这个示例被放置在自己的 package 中,以防止命名冲突:
PetPatternMatch2.java JDK 17
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) {
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));
}
}
如果 Pet 没有用 sealed (密封,Java 15 引入的关键字,用于限制类或方法的随意继承扩充)修饰,编译器会再次发出警告“switch 语句没有覆盖所有可能的输入值”。本例中的具体原因则是 interface Pet 可以被其他任何文件中的任何类实现,所以破坏了 switch 语句覆盖的完整性。通过将 Pet 修饰为sealed 的,编译器就能确保 switch 覆盖到了所有可能的Pet类型。
模式匹配不会像继承多态性那样将你约束在单一继承层次结构中——你可以匹配任意类型。想要这样做,就需要将 Object 传入 switch:
ObjectMatch.java JDK 17
import java.util.List;
record XX() {
}
public class ObjectMatch {
static String match(Object o) {
return switch (o) {
case Dog d -> "Walk the dog";
case Fish f -> "Change the fish water";
case Pet sp -> "Not dog or fish";
case String s -> "String " + s;
case Integer i -> "Integer " + i;
case String[] sa -> String.join(", ", sa);
case null, XX xx -> "null or XX: " + xx;
default -> "Something else";
};
}
public static void main(String[] args) {
List.of(new Dog(), new Fish(), new Pet(),
"Oscar", Integer.valueOf(12),
Double.valueOf("47.74"),
new String[]{"to", "the", "point"},
new XX()
).forEach(
p -> System.out.println(match(p))
);
}
}
运行结果如下:
将 Object 参数传入 switch 的时候,编译器会要求有 default 存在,也是为了搜盖所有可能的输入值(不过 null 除外,编译器并不要求存在为 null 的 case ,尽管这种情况也可能发生)。
可以将为 null 的 case 和另一种模式合并到一起,如case null,XX xx。这样是可以的,因为对象引用也可以是 null。