一、枚举类型——新特性(模式匹配-违反里氏替换原则)

文章探讨了Java中的模式匹配特性,指出其在不依赖同一继承层次结构的类型间实现行为的方式。模式匹配允许违反里氏替换原则,同时保持代码可控。文中通过示例展示了如何使用模式匹配处理Pet类层次结构,强调了它在处理非里氏替换场景下的优势,并提到了sealed接口在确保switch覆盖完整性的角色。此外,还展示了如何对Object进行模式匹配,以及处理null的情况。
摘要由CSDN通过智能技术生成

现在所有的基础模块都已准备好,我们可以以更全局的视角来看看模式匹配了。不过要记住,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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小熊猫呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值