要不要升级?Java 21强大的新特性,代码量减半

2024年 5月 13日 110.0k 0

1. record模式

Record模式由 JEP 405 作为预览功能提出,并在 JDK 19 中发布,JEP 432 进行了第二次预览,并在 JDK 20 中发布。该功能与用于switch的模式匹配(JEP 441)共同发展,两者之间有相当多的交互

1.1 instanceof类型模式

Object obj = "Pack" ;
// Java 16之前
if (obj instanceof String) {
    String s = (String) obj ;
    System.out.println("强转为String") ;
}
// 自Java 16起
if (obj instanceof String s) {
    System.out.println("简便多了") ;
}

在上面的代码中从java16开始,运行时obj的值是String的实例,则obj与类型模式String s匹配。如果模式匹配,则表达式的实例为true,并且模式变量s初始化为obj转换为String的值,然后可以在包含的代码块中使用该值。

1.2 模式匹配与Records

Records (JEP 395)是数据的透明载体。接收record类实例的代码通常将使用内置的组件访问器方法提取数据,称为组件。例如,我们可以使用类型模式来测试值是否是record类Point的实例,如果是,则从值中提取x和y组件:

// 自Java 16起
public record Point(int x, int y) {
}
public static void main(String[] args) {
  Object obj = new Point(10, 20);
  if (obj instanceof Point p) {
    int x = p.x();
    int y = p.y();
    System.out.println(x + y);
  }
}

上面的代码看着与1.1中介绍的没撒区别就是类型模式,在上面的代码中我们仅仅是访问了record类x与y的方法,如果是这样我们还可以像下面这样操作:

Object obj = new Point(10, 20) ;
// 自java 21起
if (obj instanceof Point(int x, int y)) {
  System.out.println(x + y) ;
}

这里的Point(int x, int y) 是一个record模式。它将提取组件的局部变量声明移至模式本身,并在值与模式匹配时通过调用访问器方法初始化这些变量。

1.3 嵌套record模式

有如下定义

public record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

如果要提取左上角点的颜色,我们可以这样写:

Object r = new Rectangle(
    new ColoredPoint(new Point(0, 0), Color.RED), 
    new ColoredPoint(new Point(100, 100), Color.BLUE)
  ) ;
// 从java 21起  
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
  System.out.printf("%s, %s%n", ul, lr) ;
}

输出结果

ColoredPoint[p=Point[x=0, y=0], c=RED], ColoredPoint[p=Point[x=100, y=100], c=BLUE]

如果你希望访问具体的颜色值,record模式还支持嵌套,如下示例:

// 从java 21起
if (r instanceof Rectangle(
    ColoredPoint(Point(int x, int y), Color c1), 
    ColoredPoint lr
  )
) {
  System.out.printf("x = %d, y = %d%n", x, y) ;
}

1.4 嵌套模式无法匹配情况

在下面这情况下是无法进行匹配的

public record Pair(Object x, Object y) {}
Pair p = new Pair(42, 42);
if (p instanceof Pair(String s, String t)) {
  System.out.println(s + ", " + t);
} else {
  System.out.println("Not a pair of strings") ;
}

以上是关于record 模式的所有内容。

2. switch模式匹配

该功能最初由 JEP 406(JDK 17)提出,后经 JEP 420(JDK 18)、427(JDK 19)和 433(JDK 20)改进。它与 "1. record模式 "功能(JEP 440)共同发展。

先来看下如下这段代码

Object obj = 100L ;
if (obj instanceof Integer) {
  Integer i = (Integer) obj ;
  obj = String.format("int %d", i);
} else if (obj instanceof Long) {
  Long l = (Long) obj ;
  obj = String.format("long %d", l);
} else if (obj instanceof String) {
  String s = (String) obj ;
  obj = String.format("String %s", s);
}

有个instanceof 模式以后就可以简化这样了

Object obj = 100L ;
if (obj instanceof Integer i) {
  obj = String.format("int %d", i);
} else if (obj instanceof Long l) {
  obj = String.format("long %d", l);
} else if (obj instanceof String s) {
  obj = String.format("String %s", s);
}
System.out.printf("result obj = %s%n", obj) ;

注意:上面的代码有2个问题

  • 上面的代码有如果没有编译器的作用,那么它的时间复杂度将是O(n)
  • 隐藏了一个BUG,当if,else没有判断到某个类型时可能会出现问题上面的代码并没有else,因为不强制所以当判断遗漏了某种类型时可能会给程序带来潜在的问题。
  • 从Java 21开始,我们可以如下处理上面的if.. else 

    var ret = switch (obj) {
      case Integer i -> String.format("int %d", i);
      case Long l    -> String.format("long %d", l);
      case String s  -> String.format("String %s", s);
      default        -> obj.toString() ;
    };
    System.out.printf("result ret = %s%n", ret) ;

    在过去我们知道如果switch的每个case没有break或者return,那么它会穿透到下一个case直到遇到break或return。并且在传统的switch中没有default也是可以的。但是在上面的代码中必须要有default子句。

    2.1 switch与null值

    传统上,如果switch表达式值为空,switch 语句和表达式会抛出 NullPointerException,因此必须在 switch 之外进行空判断:

    String s = null ;
    switch (s) {
      // 如果不清楚这里的语法,你应该先看看java14对switch新语法的介绍
      case "a", "b" -> System.out.println("a or b") ;
      default -> System.out.println("defualt value") ;
    }

    控制台输出

    要不要升级?Java 21强大的新特性,代码量减半-1图片

    在上面的代码中在过去,我们要先对s进行null的判断,再进行switch,否则有可能就会出现上面的错误。修改如下:

    if (s == null) {
      return ;
    }
    switch (s) {
      // TODO
    }

    以上代码是Java 21之前,从Java 21起,我们可以如下:

    switch (s) {
      case null -> System.out.println("oops") ;
      case "a", "b" -> System.out.println("a or b") ;
      default -> System.out.println("defualt value") ;
    }

    无需单独的if判断是否为null情况。

    2.2 switch条件判断

    在case中还可以添加if...else判断

    static void fn1(String resp) {
      switch (resp) {
        case String s -> {
          if (s.equalsIgnoreCase("success"))
            System.out.println("处理成功");
          else if (s.equalsIgnoreCase("failure"))
            System.err.println("处理失败");
          else
            System.out.println("未知结果") ;
        }
      }
    }

    在case中是使用when子句

    static void fn2(String resp) {
      switch (resp) {
        case null -> {}
        case String s 
        when s.equalsIgnoreCase("success") -> {
          System.out.println("处理成功");
        }
        case String s
        when s.equalsIgnoreCase("failure") -> {
          System.err.println("处理失败");
        }
        case String s -> {
            System.out.println("未知结果") ;
        }
      }
    }

    这样,switch的可读性就更强了。

    2.3 switch与enum常量

    在Java 21之前,switch的case表达式必须是枚举类型,标签必须是枚举常量的简单名称,如下示例:

    public enum Color { RED, BLUE, GREEN }
    public static void fn1(Color c) {
      switch (c) {
        case RED, BLUE -> System.out.println("我喜欢的颜色") ;
        case GREEN -> {
          // TODO
        }
        default -> System.out.println("我讨厌的颜色") ;
      }
    }

    上面说的标签必须是枚举常量的简单名称什么意思呢?就是说在java21之前使用枚举时的标签不能是下面这种写法:

    case Color.GREEN -> {}

    而从Java 21起可以使用这种语法。

    3. 虚拟线程

    关于虚拟线程请查看这篇文章:

    【技术革命】JDK21虚拟线程来袭,让系统的吞吐量翻倍!

    4. 字符串模版

    注:这是一个预览功能

    编译:javac --enable-preview --source 21 -Xlint:preview Xxx.java

    运行:java --enable-preview Xxx

    在开发中字符串相关的操作是非常非常多的,虽然Java 提供了多种字符串组成机制,但遗憾的是,所有机制都有缺点。

    • 使用+操作符拼接字符串,看着都不好理解
    String result = x + " + " + y + " = " + (x + y) ;
    • 冗余的StringBuilder
    String s = new StringBuilder().append(x).append(" + ")
      .append(y).append(" = ").append(x + y).toString() ;
    • String#format 与 String#formatted将格式字符串与参数分离,避免了类型错配:
    int x = 10, y = 20 ;
    String s = String.format("%2$d + %1$d = %3$d", x, y, x + y);
    String t = "%2$d + %1$d = %3$d".formatted(x, y, x + y) ;
    • java.text.MessageFormat要求太多,而且格式字符串中使用了不熟悉的语法:
    String ret = MessageFormat.format("{0} + {1} = {2}", x, y, x + y) ;

    4.1 STR 模板处理器

    STR 是 Java 平台定义的模板处理器。它通过用表达式的(字符串化)值替换模板中的每个嵌入表达式来执行字符串插值。

    String firstName = "Bill" ;
    String lastName  = "Duck" ;
    String fullName  = STR."\{firstName} \{lastName}" ;
    System.out.println(fullName) ;

    输出结果

    Bill Duck

    注:STR 是一个公共静态最终字段,会自动导入到每个 Java 源文件中。

    表达式还可以执行相应的操作,如下:

    int x = 10, y = 20 ;
    String result = STR."\{x} + \{y} = \{x + y}" ;
    System.out.println(result) ;
    // 10 + 20 = 30

    表达式中还可以调用方法

    static String getName() {
      return "张三" ;
    }
    static record Req(String date, String time) {}
    static void fn5() {
      String s = STR."我的名字是 \{getName()} ";
      System.out.println(s) ;
      Req req = new Req("2000-01-01", "23:59:59") ;
      String t = STR."Access at \{req.date} \{req.time}";
      System.out.println(t) ;
    }

    输出结果

    我的名字是 张三
    Access at 2000-01-01 23:59:59

    多行模版字符串

    static void fn6() {
      String name    = "张三";
      String phone   = "1899999999";
      String address = "xxxooo";
      String json = STR."""
      {
        "name":    "\{name}",
        "phone":   "\{phone}",
        "address": "\{address}"
      }
      """;
      System.out.println(json);
    }

    输出结果

    {
       "name": "张三",
       "phone": "1899999999",
       "address": "xxxooo"
    }

    以上是基于STR模版处理器的内容,接下来介绍另外一个。

    4.2 FMT 模板处理器

    FMT 是 Java 平台定义的另一种模板处理器。FMT 与 STR 类似,它执行插值,但也解释嵌入式表达式左侧的格式规范。格式说明符与 java.util.Formatter 中定义的格式说明符相同。

    record Rectangle(String name, double width, double height) {
      double area() {
        return width * height;
      }
    }
    public static void main(String[] args) {
      Rectangle[] zone = new Rectangle[] {
        new Rectangle("Alfa", 17.8, 31.4),
        new Rectangle("Bravo", 9.6, 12.4),
      };
      String s = 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()}
        \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
      """;
      System.out.println(s) ;
    }

    5. 序列集合

    在Java21 之前的集合类中要获取第一个和最后一个元素,不同的集合操作方式不同或者压根就没有对应的方法。如下示例:

    要不要升级?Java 21强大的新特性,代码量减半-2图片

    在说遍历集合,正向时(从第一个到最后一个)操作方法基本一致。但是反向时遍历时每个集合就又不相同了。

    在JDK21中提供了如下3个序列接口:

    • SequencedCollection
    public interface SequencedCollection extends Collection {
      SequencedCollection reversed() ;
      default void addFirst(E e) ;
      default void addLast(E e) ;
      default E getFirst() ;
      default E getLast() ;
      default E removeFirst() ;
      default E removeLast() ;
    }
    • SequencedSet
    public interface SequencedSet extends SequencedCollection, Set {
      SequencedSet reversed();
    }
    • SequencedMap
    public interface SequencedMap extends Map {
      SequencedMap reversed() ;
      default Map.Entry firstEntry() ;
      default Map.Entry lastEntry() ;
      default Map.Entry pollFirstEntry() ;
      default Map.Entry pollLastEntry() ;
      default V putFirst(K k, V v) ;
      default V putLast(K k, V v) ;
      // other
    }

    以上3个集合都提供了对应的获取第一个和最后一个元素的方法及集合反转方法。上面定义的三个新接口与现有的集合类型层次结构非常吻合,如下图:

    要不要升级?Java 21强大的新特性,代码量减半-3图片

    对现有的类和接口进行了如下调整:

    • List 现在将 SequencedCollection 作为其直接超接口、
    • Deque 现在将 SequencedCollection 作为其直接超接口、
    • LinkedHashSet 进一步实现了 SequencedSet、
    • SortedSet 现在将 SequencedSet 作为其直接超接口、
    • LinkedHashMap 进一步实现了 SequencedMap,而
    • SortedMap 现在将 SequencedMap 作为其直接超接口。

    6. 未命名模式&变量

    注:这是一个预览功能

    先看下面这个示例

    public record Point(int x, int y) {}
    enum Color { RED, GREEN, BLUE }
    record ColoredPoint(Point p, Color c) {}
    record Rectangle(ColoredPoint cp) {}
      
    Object obj = new Rectangle(
        new ColoredPoint(new Point(10, 10), Color.RED)
      ) ;
    if (obj instanceof Rectangle(ColoredPoint(Point(int x, int y), Color c))) {
      System.out.printf("x = %d, y = %d%n", x, y) ;
    }

    在上面的if判断中,对于Color c变量并没有使用,从Java 21开始我们可以像下面这样改写:

    if (obj instanceof Rectangle(ColoredPoint(Point(int x, int y), _))) {
      System.out.printf("x = %d, y = %d%n", x, y) ;
    }

    使用一个 "_" 下划线代替即可。

    未使用的变量

    int[] arr = {1, 2, 3, 4, 5} ;
    int total = 0 ;
    for (var a : arr) {
      total++ ;
    }

    在这个示例中,变量a并没有使用,所以从Java 21开始可以改写如下:

    for (var _ : arr) {
      total++ ;
    }

    对于这样没有使用的变量,我们可以用一个 "_" 下划线代替。其它示例:

    try {
      int a = 1 / 0 ;
    } catch (Exception _) { // 这里没有用到异常通过可以使用 _
    }

    注:我用的Eclipse没法直接使用,我这里是通过记事本编写,通过命令行编译&运行。

    7. 未命名的类&Main方法

    注:这是一个预览功能

    下面这个代码是学习java的入门代码

    public class UnnamedClassAndMain {
     public static void main(String[] args) {
       System.out.println("Hello World!!!") ;
     }
    }

    从Java 21开始,我们可以简化成如下形式了

    public class UnnamedClassAndMain {
      void main() {
        System.out.println("Hello World!!!") ;
      }
    }

    未命名的类

    还是拿上面的程序演示,我们还可以继续简化如下形式:

    void main() {
      System.out.println("Hello World!!!") ;
    }

    对,文件中只有一个极简的方法,连类的声明都没有了。你甚至还可以如下,定义方法,方法调用

    String name = "Pack" ;
    String getName() {
      return name ;
    }
    void main() {
      System.out.println(getName()) ;
    }

    类文件直接定义方法,声明变量。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论