Java 21字符串模板


Java中字符串模板(String Template)使 String 更安全、更易于使用。

到目前为止,我们有几种与字符串文字和实例一起使用的机制和类型,它们直接内置于语言/JDK 中:

  • +(加)运算符:最大的缺点是每次使用该+运算符时都会分配一个新的字符串。
  • StringBuffer和StringBuilder:尽管StringBuilder提供了出色的性能,主要缺点是冗长,尤其是对于更简单的字符串。
  • String::format和String::formatted
  • java.text.MessageFormat

它们每个都有用例,但也有特定的缺点。

String::format 和 String::formatted缺点
String类型具有三种格式化方法:

  • static String format(String format, Object... args)
  • static String format(Locale locale, String format, Object... args)
  • String formatted(Object... args)(Java 15+)

它们允许可重用​​的模板,但它们需要格式说明符并以正确的顺序提供变量:

var format = "Hello %s, how are you?\nIt's %d°C today!";
var greeting = String.format(format, name, tempC);

// Java 15+
var greeting = format.formatter(name, tempC);

可以想象,使用格式指定器需要为模板字符串创建一个Formatter 。虽然节省了字符串分配的数量,但现在 JVM 必须解析/验证模板字符串。

java.text.MessageFormat缺点
java.text.MessageFormat 类型就像 String::format 的同胞兄弟,因为它使用了与 format String 容器规范相同的方法。不过,它比较啰嗦,而且现在很多开发人员都不熟悉它的语法。

下面的示例是最简单的变体,没有额外的格式化(如前导零):

var format = new MessageFormat("Hello {0}, how are you?\nIt's {1}°C today!");
var greeting = format.format(name, tempC);

它与 String::format 有着相同的缺点。不过,它还有一些额外的技巧,比如处理复数。

Java 字符串模板(String Template)
字符串模板的目标是通过更安全的开箱即用的结果来提供插值的清晰度,并根据需要提供扩展和弯曲功能的选项。

1、模板表达式
在 Java 中处理字符串的新方法称为模板表达式,这是一种在字符串字面中安全插值表达式的可编程方法。比插值更好的是,我们可以将结构化文本转化为任何对象,而不仅仅是字符串。

要创建模板表达式,我们需要两样东西:

  • 模板处理器
  • 一个包含封装表达式(如 \{name} )的模板

这两个要求由一个点组合而成,就像方法调用一样。使用前面的一个示例,它看起来像这样:

var name = "Ben";
var tempC = 28;

var greeting = STR.
"Hello \{this.user.firstname()}, how are you?\nIt's \{tempC}°C today!";

第一个问题可能是:STR 从何而来?

由于 String -> String 模板很可能是 String 模板的默认用例,因此模板处理器 STR 会自动导入到每个 Java 源文件中。因此,Java 方法所带来的不便仅仅是增加了 4 个字符。

多行模板和表达式
模板表达式也适用于文本块(Java 15+):

var json = STR."""
{
   
"user": "\{this.user.firstname()}",
   
"temperatureCelsius: \{tempC}
}
""";

不仅模板本身可以是多行的,表达式也可以是多行的,包括注释!

var json = STR."""
{
 
"user": "\{
   
// We only want to use the firstname
    this.user.firstname()
  }
",
 
"temperatureCelsius: \{tempC}
}
""";

但要注意,表达式仍然需要像单行 lambda,而不是代码块。

不仅仅是字符串
在我看来,与其他语言相比,Java 实现的主要优势在于可以使用其他模板处理器,而不是字符串 -> 字符串处理器。再来看看 JSON 示例。如果插值能返回 JSONObject 而不是字符串,那岂不是更好?

创建自己的模板处理器
模板处理建立在新添加的嵌套函数接口 java.lang.StringTemplate.Processor 上:

@FunctionalInterface
public interface Processor<R, E extends Throwable> {

  R process(StringTemplate stringTemplate) throws E;

  static <T> Processor<T, RuntimeException> of(Function<? super StringTemplate, ? extends T> process) {
    return process::apply;
  }

  // ...
}

处理过程是这样的:包含表达式的字符串字面量被转换成字符串模板(StringTemplate),然后交给处理器。

如果要创建 JSONObject,我们需要先插值字符串字面,然后创建所需返回类型的新实例。使用静态辅助处理器 Processor.of 可以轻松实现这一点:

/// CREATE NEW TEMPLATE PROCESSOR
var JSON = StringTemplate.Processor.of(
  (StringTemplate template) -> new JSONObject(template.interpolate())
);

// USE IT LIKE BEFORE
JSONObject json = JSON.
"""
{
 
"user": "\{
   
// We only want to use the firstname
    this.user.firstname()
  }
",
 
"temperatureCelsius: \{tempC}
}
""";

但这并不是定制处理器的真正威力所在。

StringTemplate 提供给我们的不仅仅是一个无参数插值方法。我们可以访问表达式结果并对其进行操作!这意味着模板可以简化,因为处理器将负责正确处理值,如转义用户值中的双引号等。

这就是我们需要尝试的理想模板:

JSONObject json = JSON."""
{
 
"user": \{this.user.firstname()},
 
"temperatureCelsius: \{tempC}
}
""";

为了达到这个理想模板效果,处理器需要评估表达式的结果(template.values()),并创建新的替换,以便与片段字面意义匹配(template.fragments()):

StringTemplate.Processor<JSONObject, JSONException> JSON = template -> {
  String quote = "\"";
  List<Object> newValues = new ArrayList<>();

  for (Object value : template.values()) {
    if (value instanceof String str) {
     
// 清除字符串
     
// 许多反斜线看起来很奇怪,但这是正确的 regex
      str = str.replaceAll(quote,
"\\\\\"");
      newValues.add(quote + str + quote);
    }
    else if (value instanceof Number || value instanceof Boolean) {
      newValues.add(value);
    }
   
// TODO: support more types
    else {
      throw new JSONException(
"Invalid value type");
    }
  }

  var json = StringTemplate.interpolate(template.fragments(), newValues);

  return new JSONObject(json);
};

就是这样!

字符串模板构建 JSONObject 所需的所有逻辑现在都集中在一处。
我们可以在 JSON 模板中安全地使用任何表达式,而无需考虑是否引用。

无限可能
由于我们可以访问片段和值,因此可以创建任何我们想要的东西。

每当我们有一个基于字符串的模板需要转换、验证或消毒时,Java 的字符串模板就会为我们提供一个内置的简化模板引擎,而无需依赖第三方。

为了不从零开始,除了 STR 之外,Java 平台还提供了两个额外的模板处理器。

请注意,在 Java 21.ea.27 中,这些额外的模板处理器似乎没有发挥作用。至少在我的测试设置中没有运行。

处理器 FMT 结合了 STR 的插值功能和 java.util.Formatter.STR 中定义的格式规范:

record Shape(String name, int corners) { }

var shapes = new Shape[] {
  new Shape("Circle", 0),
  new Shape(
"Triangle", 3),
  new Shape(
"Dodecagon", 12)
};

var table = FMT.
"""
  Name     Corners
  %-12s\{shapes[0].name()}  %3d\{shapes[0].corners()}
  %-12s\{shapes[1].name()}  %3d\{shapes[1].corners()}
  %-12s\{shapes[2].name()}  %3d\{shapes[2].corners()}
  \{
" ".repeat(7)} Total corners %d\{
    shapes[0].corners() + shapes[1].corners() + shapes[2].corners()
  }
""";

// OUTPUT:
// Name        Corners
// Circle         0
// Triangle       3
// Dodecagon     12
//        Total: 15


Java 平台提供的第三个处理器是 RAW,它不进行插值,而是返回一个 StringTemplate。