Java 13: Switch表达式

以下代码需要使用Java 13 才能运行,并且需要设置语言级别为13(Preview).IDEA中File-Project Structure-Project language level-选择 13(Preview)-Switch expressions,text blocks.

以下内容机翻整理自OpenJDK官方:JEP 354,稍有改动.

简述

在Java 13发布中,提供了两个预览功能:一个是文本块,另一个是Switch表达式改进.这篇是关于Switch表达式改进的介绍.文本块的文章地址是: Java 13预览:文本块

基本操作

在最开始我们是这样用Switch表达式的:

        int day = SUNDAY;
        switch (day) {
            case MONDAY:
            case FRIDAY:
            case SUNDAY:
                System.out.println(6);
                break;
            case TUESDAY:
                System.out.println(7);
                break;
            case THURSDAY:
            case SATURDAY:
                System.out.println(8);
                break;
            case WEDNESDAY:
                System.out.println(9);
                break;
        }

通过break的”作用”,合并计算多个结果.看起来似乎没什么问题,但如果有个break写错了,也就意味着计算结果出现了偏差.

在Java 13提供的Switch表达式,是这样的:

        switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> {
                System.out.println(5);
            }
            case TUESDAY -> System.out.println(7);
            case THURSDAY, SATURDAY -> System.out.println(8);
            case WEDNESDAY -> System.out.println(9);
        }

通过一种优雅的方式,重写了上面的示例代码.最主要的好处是,去掉了break,减少错误的发生.

case MONDAY, FRIDAY, SUNDAY -> {
                System.out.println(5);
}

这个地方是为了演示,在Switch的case中,也可以使用代码块.

使用Switch动态取值

有时候我们会这样写代码,反正我是这样写过…

通过Switch来动态获取值.

在以前我们是这样写的:

        int numLetters;
        switch (day) {
            case MONDAY:
            case FRIDAY:
            case SUNDAY:
                numLetters = 6;
                break;
            case TUESDAY:
                numLetters = 7;
                break;
            case THURSDAY:
            case SATURDAY:
                numLetters = 8;
                break;
            case WEDNESDAY:
                numLetters = 9;
                break;
            default:
                throw new IllegalStateException("Wat: " + day);
        }
        System.out.println(numLetters);

通过break和case结合起来,对Switch外面的变量进行动态赋值,看起来略微复杂了一些.现在,Java 13又提供了一个简单的办法:

        int day1 = MONDAY;
        String n1umLetters = switch (day1) {
            case MONDAY, FRIDAY, SUNDAY -> "6";
            case TUESDAY -> "7";
            case THURSDAY, SATURDAY -> "8";
            case WEDNESDAY -> "9";
            // 这样使用,必须添加default,否则会提示: Switch expression does not cover all possible input value(开关表达式未涵盖所有可能的输入值)
            // 意思就是,如果输入的值没有在case中,则无法找到返回值.
            default -> "default";
        };

在这个地方用String是为了演示String在其中同样可用.

另外需要注意的一点是:

这样使用,必须添加default,否则会提示: Switch expression does not cover all possible input value(开关表达式未涵盖所有可能的输入值).意思就是,如果输入的值没有在case中,则无法找到返回值.

java标志
image-2833

关于箭头标签

如果case匹配,则执行箭头右侧的表达式或语句,否则不执行任何操作

    static void howMany(int k) {
        switch (k) {
            case 1 -> System.out.println("one");
            case 2 -> System.out.println("two");
            default -> System.out.println("many");
        }
    }

看起来简洁明了.同时也可以在下面的示例中使用上面的方式定义代码:

    static void howMany1(int k) {
        System.out.println(
                switch (k) {
                    case 1 -> "one";
                    case 2 -> "two";
                    default -> "many";
                }
        );
    }

在代码块中使用yield

在Switch表达式返回的值中,有可能会使用到代码块.而这时,我们则可以用到yield语句.如下所示:

int j = switch (day) {
            case MONDAY -> 0;
            case TUESDAY -> 1;
            default -> {
                int k=day;
                int result=fyield(k);
                yield result;
            }
};

static int fyield(int i) {
        return i * 2;
}

这样就可以调用其它方法,来进行返回值的处理之后,再返回.

来自官方文档的可能遇到的错误提示

机翻.

Switch表达的情况必须详尽 ; 对于所有可能的值,必须有一个匹配的开关标签。(显然,Switch声明并非必须详尽。)

实际上,这通常意味着需要一个default子句。但是,enum Switch对于覆盖所有已知常量的表达式,default编译器会插入一个子句以指示该enum定义在编译时和运行时之间已更改。依靠这种隐式default子句的插入可以使代码更健壮。现在,当重新编​​译代码时,编译器将检查所有情况是否得到明确处理。如果开发人员插入了显式default子句(如今天的情况),则可能的错误将被隐藏。

此外,Switch表达式必须以一个值正常完成,或者必须通过引发异常来突然完成。这有许多后果。首先,编译器会检查每个开关标签是否匹配,然后产生一个值。

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a yield statement
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a yield statement
};

另一种后果是,控制语句,break,yield,return和continue,无法通过跳出Switch表达式,如在下文中:

 for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                yield 1;
            case 1:
                yield 2;
            default: 
                continue z; 
                // ERROR! Illegal jump through a Switch expression 
        };
    ...
    }

示例源码

示例完整源码如下:

import static java.util.Calendar.*;


public class Java13SwitchTestDemo {


    public static void main(String[] args) {
        // 简述
        int day = SUNDAY;
        switch (day) {
            case MONDAY:
            case FRIDAY:
            case SUNDAY:
                System.out.println(6);
                break;
            case TUESDAY:
                System.out.println(7);
                break;
            case THURSDAY:
            case SATURDAY:
                System.out.println(8);
                break;
            case WEDNESDAY:
                System.out.println(9);
                break;
        }


        // Java 13 新的方式
        switch (day) {
            case MONDAY, FRIDAY, SUNDAY -> {
                System.out.println(5);
            }
            case TUESDAY -> System.out.println(7);
            case THURSDAY, SATURDAY -> System.out.println(8);
            case WEDNESDAY -> System.out.println(9);
        }

        int numLetters;
        switch (day) {
            case MONDAY:
            case FRIDAY:
            case SUNDAY:
                numLetters = 6;
                break;
            case TUESDAY:
                numLetters = 7;
                break;
            case THURSDAY:
            case SATURDAY:
                numLetters = 8;
                break;
            case WEDNESDAY:
                numLetters = 9;
                break;
            default:
                throw new IllegalStateException("Wat: " + day);
        }
        System.out.println(numLetters);


        int day1 = MONDAY;
        String n1umLetters = switch (day1) {
            case MONDAY, FRIDAY, SUNDAY -> "6";
            case TUESDAY -> "7";
            case THURSDAY, SATURDAY -> "8";
            case WEDNESDAY -> "9";
            default -> "default";
        };

        howMany(1);
        howMany(2);
        howMany(3);

        System.out.println("------");
        howMany1(8);
        howMany1(9);
        howMany1(10);
        System.out.println("------");

        int j = switch (day) {
            case MONDAY -> 0;
            case TUESDAY -> 1;
            default -> {
                int k=day;
                int result=fyield(k);
                yield result;
            }
        };
        System.out.println(j);

    }

    static int fyield(int i) {
        return i * 2;
    }

    /**
     * 箭头标签
     * <p>
     * 如果case匹配,则执行箭头右侧的表达式或语句,否则不执行任何操作,
     *
     * @param k
     */
    static void howMany(int k) {
        switch (k) {
            case 1 -> System.out.println("one");
            case 2 -> System.out.println("two");
            default -> System.out.println("many");
        }
    }


    /**
     * 可以重写上面的方法为
     *
     * @param k
     */
    static void howMany1(int k) {
        System.out.println(
                switch (k) {
                    case 1 -> "one";
                    case 2 -> "two";
                    default -> "many";
                }
        );
    }

}

Java 13预览: 文本块

以下代码需要使用Java 13 才能运行,并且需要设置语言级别为13(Preview).IDEA中File-Project Structure-Project language level-选择 13(Preview)-Switch expressions,text blocks.

以下内容机翻整理自OpenJDK官方:JEP 355,稍有改动.

HTML示例

// HTML: Java 13 之前.
        String html = "<html>\n" +
                "    <body>\n" +
                "        <p>Hello, world</p>\n" +
                "    </body>\n" +
                "</html>\n";

        System.out.println(html);

        // HTML: Java 13
        html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;
        System.out.println(html);
        System.out.println("-------------------------");
        /// ---------------------

SQL示例

        // SQL: Java 13之前.
        String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
                "WHERE `CITY` = 'INDIANAPOLIS'\n" +
                "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

        System.out.println(html);
        // SQL: Java 13
        query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;
               """;

        System.out.println(html);

java标志
image-2819

编译时处理

文本块是type 的常量表达式String,就像字符串文字一样。但是,与字符串文字不同,Java编译器通过三个不同的步骤处理文本块的内容:

  1. 内容中的行终止符转换为LF(\u000A)。这种转换的目的是在跨平台移动Java源代码时遵循最小惊喜的原则。
  2. 删除了内容周围附带的空白,以匹配Java源代码的缩进。
  3. 内容中的转义序列被解释。作为最后一步执行解释意味着开发人员可以编写转义序列,例如\n无需通过早期步骤对其进行修改或删除。

处理后的内容class作为CONSTANT_String_info常量池中的一项记录在文件中,就像字符串文字的字符一样。该class文件不会记录CONSTANT_String_info条目是从文本块还是字符串文字派生的。

在运行时,String像字符串文字一样,将文本块评估为的实例。的实例String被来源于文本块是从字符串常量衍生的实例没有区别。具有相同处理内容的两个文本块String由于interning会引用相同的instance ,就像字符串文字一样。

转义

        html = """
              <html>\r
                  <body>\r
                      <p>Hello, world</p>\r
                  </body>\r
              </html>\r
              """;
        System.out.println(html);

在文本块中使用”双引号.是合法的:

        String story = """
        "When I use a word," Humpty Dumpty said,
        in rather a scornful tone, "it means just what I
        choose it to mean - neither more nor less."
        "The question is," said Alice, "whether you
        can make words mean so many different things."
        "The question is," said Humpty Dumpty,
        "which is to be master - that's all."
        """;
        System.out.println(story);

三个双引号,必须进行转义,防止模糊结束定义符:

        String code =
                """
                String text = \"""
                    A text block inside a text block
                \""";
                """;
        System.out.println(code);

文本块的串联

可以在使用字符串文字的任何地方使用文本块:

        code = "public void print(Object o) {" +
                """
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);

但是,涉及文本块的串联可能会变得很笨拙。以以下文本块为起点:

        code = """
                  public void print(Object o) {
                      System.out.println(Objects.toString(o));
                  }
                  """;

假设需要更改它,以便类型o来自变量。使用串联,包含尾随代码的文本块将需要从新行开始。

不幸的是,如下所示,在程序中直接插入换行符会导致类型和开头的文本之间存在很大的空白o:

        String type = "test";
        code = """
              public void print(""" + type + """
                                                 o) {
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);

可以手动删除空格,但这会损害引用代码的可读性:

        code = """
              public void print(""" + type + """
               o) {
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);

较干净的替代方法是使用String::replace或String::format,如下所示:

        code = """
        public void print($type o){
            System.out.println(Objects.toString(o));
        }
        """.replace("$type", type);
        System.out.println(code);

        String name = "name";
        String age = "age";
        String address = "address";
        String phone = "phone";
        String height = "height";

        code = """
        public void print($type o){
            String name="$name";
            String age="$age";
            String address="$address";
            String phone="$phone";
            String height="$height";
            System.out.println(Objects.toString(o));
        }
        """.replace("$type", type)
                .replace("$name", name)
                .replace("$age", age)
                .replace("$address", address)
                .replace("$phone", phone)
                .replace("$height", height);
        System.out.println(code);


        code = String.format("""
              public void print(%s o) {
                  System.out.println(Objects.toString(o));
              }
              """, type);

        System.out.println(code);

另一种选择是引入新的实例方法String::formatted,该方法可以按如下方式使用:

        String source = """
                public void print(%s object) {
                    System.out.println(Objects.toString(object));
                }
                """.formatted(type);

        System.out.println(source);

附加方法

将添加以下方法来支持文本块:

  • String::stripIndent():用于从文本块内容中去除附带的空白
  • String::translateEscapes():用于翻译转义序列
  • String::formatted(Object… args):简化文本块中的值替换

其实相对于Python 3的多行字符串(Java的文本块),Java的String::replace,String:format,String::formatted,还是复杂了很多.例如,下面的代码:

        String name = "name";
        String age = "age";
        String address = "address";
        String phone = "phone";
        String height = "height";

        code = """
        public void print($type o){
            String name="$name";
            String age="$age";
            String address="$address";
            String phone="$phone";
            String height="$height";
            System.out.println(Objects.toString(o));
        }
        """.replace("$type", type)
                .replace("$name", name)
                .replace("$age", age)
                .replace("$address", address)
                .replace("$phone", phone)
                .replace("$height", height);
        System.out.println(code);

在Python 3中:

name = "name"
age = "age"
address = "address"
phone = "phone"
height = "height"

code = """
public void print(Object o){{
   String name="{0}";
   String age="{1}";
   String address="{2}";
   String phone="{3}";
   String height="{4}";
   System.out.println(Objects.toString(o));
}}
        """.format(name,age,address,phone,height);
print(code);

这样替换文本,既方便又省事..

完整源码

public class JavaTextBlockDemo {


    public static void main(String[] args) {

        // HTML: Java 13 之前.
        String html = "<html>\n" +
                "    <body>\n" +
                "        <p>Hello, world</p>\n" +
                "    </body>\n" +
                "</html>\n";

        System.out.println(html);

        // HTML: Java 13
        html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;
        System.out.println(html);
        System.out.println("-------------------------");
        /// ---------------------

        // SQL: Java 13之前.
        String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
                "WHERE `CITY` = 'INDIANAPOLIS'\n" +
                "ORDER BY `EMP_ID`, `LAST_NAME`;\n";

        System.out.println(html);
        // SQL: Java 13
        query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;
               """;

        System.out.println(html);
        System.out.println("-------------------------");
        /// ---------------------

        // h2. 转义
        html = """
              <html>\r
                  <body>\r
                      <p>Hello, world</p>\r
                  </body>\r
              </html>\r
              """;
        System.out.println(html);

        // 使用"双引号.是合法的
        String story = """
        "When I use a word," Humpty Dumpty said,
        in rather a scornful tone, "it means just what I
        choose it to mean - neither more nor less."
        "The question is," said Alice, "whether you
        can make words mean so many different things."
        "The question is," said Humpty Dumpty,
        "which is to be master - that's all."
        """;
        System.out.println(story);

        // 三个双引号,必须进行转义,防止模糊结束定义符.
        String code =
                """
                String text = \"""
                    A text block inside a text block
                \""";
                """;
        System.out.println(code);

        // h2.文本块的串联

        // 可以在使用字符串文字的任何地方使用文本块.
        code = "public void print(Object o) {" +
                """
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);

        // 但是,涉及文本块的串联可能会变得很笨拙。以以下文本块为起点:

        code = """
                  public void print(Object o) {
                      System.out.println(Objects.toString(o));
                  }
                  """;
        // 假设需要更改它,以便类型o来自变量。使用串联,包含尾随代码的文本块将需要从新行开始。
        // 不幸的是,如下所示,在程序中直接插入换行符会导致类型和开头的文本之间存在很大的空白o:
        String type = "test";
        code = """
              public void print(""" + type + """
                                                 o) {
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);
        // 可以手动删除空格,但这会损害引用代码的可读性:
        code = """
              public void print(""" + type + """
               o) {
                  System.out.println(Objects.toString(o));
              }
              """;
        System.out.println(code);

        // 较干净的替代方法是使用String::replace或String::format,如下所示:
        code = """
        public void print($type o){
            System.out.println(Objects.toString(o));
        }
        """.replace("$type", type);
        System.out.println(code);

        String name = "name";
        String age = "age";
        String address = "address";
        String phone = "phone";
        String height = "height";

        code = """
        public void print($type o){
            String name="$name";
            String age="$age";
            String address="$address";
            String phone="$phone";
            String height="$height";
            System.out.println(Objects.toString(o));
        }
        """.replace("$type", type)
                .replace("$name", name)
                .replace("$age", age)
                .replace("$address", address)
                .replace("$phone", phone)
                .replace("$height", height);
        System.out.println(code);


        code = String.format("""
              public void print(%s o) {
                  System.out.println(Objects.toString(o));
              }
              """, type);

        System.out.println(code);

        // 另一种选择是引入新的实例方法String::formatted,该方法可以按如下方式使用
        String source = """
                public void print(%s object) {
                    System.out.println(Objects.toString(object));
                }
                """.formatted(type);

        System.out.println(source);

        // 附加方法

        // 将添加以下方法来支持文本块;
        //
        //String::stripIndent():用于从文本块内容中去除附带的空白
        //String::translateEscapes():用于翻译转义序列
        //String::formatted(Object... args):简化文本块中的值替换

    }


}