从Java8到Java23值得期待的x个特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

概述

从 2017 年开始,Java 版本更新遵循每六个月发布一次的节奏,LTS版本则每两年发布一次,以快速验证新特性,推动 Java 的发展。

Java版本分布

数据来源2024 State of the Java Ecosystem

提升幸福感的特性

局部变量类型推断(Java 10)

// 以前的写法
Map<String, List<ProcSequenceFlow>> sourceMap = sequenceFlowList.stream()
        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));

// 现在的写法
var sourceMap2 = sequenceFlowList.stream()
        .collect(Collectors.groupingBy(ProcSequenceFlow::getSourceRef));

Switch表达式(Java 14)

我们以“判断是否工作日”的例子展示一下,在Java14之前:

@Test
void testSwitch() {
    final DayOfWeek day = DayOfWeek.from(LocalDate.now());
    String typeOfDay = "";
    switch (day) {
        case MONDAY:
        case TUESDAY:
        case WEDNESDAY:
        case THURSDAY:
        case FRIDAY:
            typeOfDay = "Working Day";
            break;
        case SATURDAY:
        case SUNDAY:
            typeOfDay = "Rest Day";
            break;
    }

    Assertions.assertFalse(typeOfDay.isEmpty());
}

在Java14中:

@Test
void testSwitchExpression() {
    final DayOfWeek day = DayOfWeek.SATURDAY;
    final String typeOfDay = switch (day) {
        case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
            System.out.println("Working Day: " + day);
            yield "Working Day";
        }
        case SATURDAY, SUNDAY -> "Day Off";
    };

    Assertions.assertEquals("Day Off", typeOfDay);
}

注意看,例子中我们使用了两种写法,一种是通过yield关键字表示返回结果,一种是在->之后直接返回。

文本块(Java 15)

直接上代码

@Test
void testTextBlock() {
    final String singleLine = "你好,我是看山,公众号「看山的小屋」。这行没有换行,而且我的后面多了一个空格 \n 这次换行了";
    final String textBlockSingleLine = """
            你好,我是看山,公众号「看山的小屋」。\
            这行没有换行,而且我的后面多了一个空格\s
            这次换行了""";

    Assertions.assertEquals(singleLine, textBlockSingleLine);
}

字符串模板(预览)

字符串模板通过将文本和嵌入式表达式结合在一起,使得Java程序能够以一种更加直观和安全的方式构建字符串。

与传统的字符串拼接(使用+操作符)、StringBuilderString.format 等方法相比,字符串模板提供了一种更加清晰和安全的字符串构建方式。

特别是当字符串需要从用户提供的值构建并传递给其他系统时(例如,构建数据库查询),使用字符串模板可以有效地验证和转换模板及其嵌入表达式的值,从而提高Java程序的安全性。

让我们通过代码看一下这个特性的魅力:

public static void main(String[] args) {
    // 拼装变量
    String name = "看山";
    String info = STR. "My name is \{ name }" ;
    assert info.equals("My name is 看山");

    // 拼装变量
    String firstName = "Howard";
    String lastName = "Liu";
    String fullName = STR. "\{ firstName } \{ lastName }" ;
    assert fullName.equals("Howard Liu");
    String sortName = STR. "\{ lastName }, \{ firstName }" ;
    assert sortName.equals("Liu, Howard");

    // 模板中调用方法
    String s2 = STR. "You have a \{ getOfferType() } waiting for you!" ;
    assert s2.equals("You have a gift waiting for you!");

    Request req = new Request("2017-07-19", "09:15", "https://www.howardliu.cn");
    // 模板中引用对象属性
    String s3 = STR. "Access at \{ req.date } \{ req.time } from \{ req.address }" ;
    assert s3.equals("Access at 2017-07-19 09:15 from https://www.howardliu.cn");

    LocalTime now = LocalTime.now();
    String markTime = DateTimeFormatter
            .ofPattern("HH:mm:ss")
            .format(now);
    // 模板中调用方法
    String time = STR. "The time is \{
            // The java.time.format package is very useful
            DateTimeFormatter
                    .ofPattern("HH:mm:ss")
                    .format(now)
            } right now" ;
    assert time.equals("The time is " + markTime + " right now");

    // 模板嵌套模板
    String[] fruit = {"apples", "oranges", "peaches"};
    String s4 = STR. "\{ fruit[0] }, \{
            STR. "\{ fruit[1] }, \{ fruit[2] }"
            }" ;
    assert s4.equals("apples, oranges, peaches");

    // 模板与文本块结合
    String title = "My Web Page";
    String text = "Hello, world";
    String html = STR. """
    <html>
      <head>
        <title>\{ title }</title>
      </head>
      <body>
        <p>\{ text }</p>
      </body>
    </html>
    """ ;
    assert html.equals("""
            <html>
              <head>
                <title>My Web Page</title>
              </head>
              <body>
                <p>Hello, world</p>
              </body>
            </html>
            """);

    // 带格式化的字符串模板
    record Rectangle(String name, double width, double height) {
        double area() {
            return width * height;
        }
    }
    Rectangle[] zone = new Rectangle[] {
            new Rectangle("Alfa", 17.8, 31.4),
            new Rectangle("Bravo", 9.6, 12.4),
            new Rectangle("Charlie", 7.1, 11.23),
    };
    String table = FMT. """
        Description     Width    Height     Area
        %-12s\{ zone[0].name }  %7.2f\{ zone[0].width }  %7.2f\{ zone[0].height }     %7.2f\{ zone[0].area() }
        %-12s\{ zone[1].name }  %7.2f\{ zone[1].width }  %7.2f\{ zone[1].height }     %7.2f\{ zone[1].area() }
        %-12s\{ zone[2].name }  %7.2f\{ zone[2].width }  %7.2f\{ zone[2].height }     %7.2f\{ zone[2].area() }
        \{ " ".repeat(28) } Total %7.2f\{ zone[0].area() + zone[1].area() + zone[2].area() }
        """;
    assert table.equals("""
            Description     Width    Height     Area
            Alfa            17.80    31.40      558.92
            Bravo            9.60    12.40      119.04
            Charlie          7.10    11.23       79.73
                                         Total  757.69
            """);
}

public static String getOfferType() {
    return "gift";
}

record Request(String date, String time, String address) {
}

这个功能当前是第一次预览,在Java22第二次预览,Java23的8.12版本中还没有展示字符串模板的第三次预览(JEP 465: String Templates),还不能确定什么时候可以正式用上。

模式匹配

instanceof模式匹配(Java 16)

instanceof主要用来检查对象类型,作为类型强转前的安全检查。

比如:

@Test
void test() {
    final Object obj1 = "Hello, World!";
    int result = 0;
    if (obj1 instanceof String) {
        String str = (String) obj1;
        result = str.length();
    } else if (obj1 instanceof Number) {
        Number num = (Number) obj1;
        result = num.intValue();
    }

    Assertions.assertEquals(13, result);
}

可以看到,我们每次判断类型之后,需要声明一个判断类型的变量,然后将判断参数强制转换类型,赋值给新声明的变量。

这种写法显得繁琐且多余。

instanceof改进后:

@Test
void test1() {
    final Object obj1 = "Hello, World!";
    int result = 0;
    if (obj1 instanceof String str) {
        result = str.length();
    } else if (obj1 instanceof Number num) {
        result = num.intValue();
    }

    Assertions.assertEquals(13, result);
}

不仅如此,instanceof模式匹配的作用域还可以扩展。在if条件判断中,我们都知道&&与判断是会执行所有的表达式,所以使用instanceof模式匹配定义的局部变量继续判断。

比如:

if (obj1 instanceof String str && str.length() > 20) {
    result = str.length();
}

与原来的写法对比,新的写法代码更加简洁、可读性更高,能够提出很多冗余繁琐的代码,非常实用的一个特性。

switch模式匹配(Java 21)

switch模式匹配是一个非常赞的功能,可以在选择器表达式使用基础判断和任意引用类型,包括instanceof操作符。这意味着可以更灵活地使用对象、数组、列表等复杂数据结构作为switch语句的基础,从而简化代码并提高可读性。

switch模式匹配允许在switch语句中使用模式来测试表达式,每个模式都有特定的动作,从而可以简洁、安全地表达复杂的数据导向查询。

通过代码看下switch模式匹配的魅力;

static String formatValue(Object obj) {
    return switch (obj) {
        case null -> "null";
        case Integer i -> String.format("int %d", i);
        case Long l -> String.format("long %d", l);
        case Double d -> String.format("double %f", d);
        case String s -> String.format("String %s", s);
        case Person(String name, String address) -> String.format("Person %s %s", name, address);
        default -> obj.toString();
    };
}

public record Person(String name, String address) {}

public static void main(String[] args) {
    System.out.println(formatValue(10));
    System.out.println(formatValue(20L));
    System.out.println(formatValue(3.14));
    System.out.println(formatValue("Hello"));
    System.out.println(formatValue(null));
    System.out.println(formatValue(new Person("Howard", "Beijing")));
}

// 运行结果
// int 10
// long 20
// double 3.140000
// String Hello
// null
// Person Howard Beijing

Record 模式(Java 21)

Record是Java16正式发布的基础类型,提供不可变对象的简单实现(其实就是Java Bean,但是省略一堆的getter、setter、hashcode、equals、toString等方法)。

Record模式,主要是使Record类型可以直接在instanceof和switch模式匹配中使用。

我们一起看个示例,比如有下面几个基础元素:

// 颜色
enum Color { RED, GREEN, BLUE}
// 点
record Point(int x, int y) {}
// 带颜色的点
record ColoredPoint(Point p, Color color) {}
// 正方形
record Square(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

我们分别通过instanceof模式匹配和switch模式匹配判断输入参数的类型,打印不同的格式:

private static void instancePatternsAndPrint(Object o) {
    if (o instanceof Square(ColoredPoint upperLeft, ColoredPoint lowerRight)) {
        System.out.println("Square类型:" + upperLeft + " " + lowerRight);
    } else if (o instanceof ColoredPoint(Point(int x, int y), Color color)) {
        System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
    } else if (o instanceof Point p) {
        System.out.println("Point类型:" + p);
    }
}

private static void switchPatternsAndPrint(Object o) {
    switch (o) {
        case Square(ColoredPoint upperLeft, ColoredPoint lowerRight) -> {
            System.out.println("Square类型:" + upperLeft + " " + lowerRight);
        }
        case ColoredPoint(Point(int x, int y), Color color) -> {
            System.out.println("ColoredPoint类型:" + x + " " + y + " " + color);
        }
        case Point p -> {
            System.out.println("Point类型:" + p);
        }
        default -> throw new IllegalStateException("Unexpected value: " + o);
    }
}

我们通过main方法执行下:

public static void main(String[] args) {
    var p = new Point(1, 2);
    var cp1 = new ColoredPoint(p, Color.RED);
    var cp2 = new ColoredPoint(p, Color.GREEN);
    var square = new Square(cp1, cp2);

    instancePatternsAndPrint(square);
    instancePatternsAndPrint(cp1);
    instancePatternsAndPrint(p);

    switchPatternsAndPrint(square);
    switchPatternsAndPrint(cp1);
    switchPatternsAndPrint(p);
}
// 结果是:
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]
//
// Square类型:ColoredPoint[p=Point[x=1, y=2], color=RED] ColoredPoint[p=Point[x=1, y=2], color=GREEN]
// ColoredPoint类型:1 2 RED
// Point类型:Point[x=1, y=2]

是不是很简洁,就像Java8提供的Lambda表达式一样,很丝滑。

灵活的构造函数主体(预览特性)

我们都知道,在子类的构造函数中,比如通过super(……)调用父类,在super之前是不允许有其他语句的。

大部分的时候这种限制都没问题,但是有时候不太灵活。如果想在super之前加上一些子类特有逻辑,比如想统计下子类构造耗时,就得重写一遍父类的实现。

除了有损灵活性,这种重写的做法也会造成父子类之间的关系变得奇怪。假设父类是SDK中的一个类,SDK升级时在父类构造函数增加了一些逻辑,我们项目中是无法继承这些逻辑的,某次需要升级SDK(比如低版本有安全风险),验证不完整的情况下,就很容易出现bug。

该特性的目标是提高构造函数的可读性和可预测性,同时保持构造函数调用的自上而下的规则。通过允许在显式调用 super()this() 前初始化字段,从而实现更灵活的构造函数主体。这一变化使得代码更具表现力。

我们看下示例代码:

public class PositiveBigInteger extends BigInteger {
    public PositiveBigInteger(long value) {
        if (value <= 0) {
            throw new IllegalArgumentException("non-positive value");
        }
        super(value);
    }
}

文末总结

想要了解各版本的详细特性,可以从从小工到专家的 Java 进阶之旅 系列专栏中查看。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:从Java8到Java23值得期待的x个特性(2):提升幸福感的特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:从Java8到Java23值得期待的x个特性(2):提升幸福感的特性

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋