不要再用 StringBuilder 拼接字符串了,来试试字符串模板

引言

字符串操作是 Java 中使用最频繁的操作,没有之一。其中非常常见的操作之一就是对字符串的组织,由于常见所以就衍生了多种方案。比如我们要实现 x + y = ?,方案有如下几种

使用 + 进行字符串拼接

String s = x + " + " + y + " = " + (x + y);

使用 StringBuilder

String s = new StringBuilder()

.append(x)

.append(" + ")

.append(y)

.append(" = ")

.append(x + y)

.toString()

String::format 和 String::formatted 将格式字符串从参数中分离出来

String s = String.format("%2$d + %1$d = %3$d", x, y, x + y);

or

String s = "%2$d + %1$d = %3$d".formatted(x, y, x + y);

java.text.MessageFormat

String s = MessageFormat.format("{0} + {1} = {2}", x,y, x + y);

这四种方案虽然都可以解决,但很遗憾的是他们或多或少都有点儿缺陷,尤其是面对 Java 13 引入的文本块(Java 13 新特性—文本块)更是束手无措。

字符串模板

为了简化字符串的构造和格式化,Java 21 引入字符串模板功能,该特性主要目的是为了提高在处理包含多个变量和复杂格式化要求的字符串时的可读性和编写效率。

它的设计目标是:

通过简单的方式表达混合变量的字符串,简化 Java 程序的编写。

提高混合文本和表达式的可读性,无论文本是在单行源代码中(如字符串字面量)还是跨越多行源代码(如文本块)。

通过支持对模板及其嵌入式表达式的值进行验证和转换,提高根据用户提供的值组成字符串并将其传递给其他系统(如构建数据库查询)的 Java 程序的安全性。

允许 Java 库定义字符串模板中使用的格式化语法(java.util.Formatter ),从而保持灵活性。

简化接受以非 Java 语言编写的字符串(如 SQL、XML 和 JSON)的 API 的使用。

支持创建由字面文本和嵌入式表达式计算得出的非字符串值,而无需通过中间字符串表示。

该特性处理字符串的新方法称为:Template Expressions,即:模版表达式。它是 Java 中的一种新型表达式,不仅可以执行字符串插值,还可以编程,从而帮助开发人员安全高效地组成字符串。此外,模板表达式并不局限于组成字符串——它们可以根据特定领域的规则将结构化文本转化为任何类型的对象。

STR 模板处理器

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

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

我们先看一个简单的例子:

@Test

public void STRTest() {

String sk = "死磕 Java 新特性";

String str1 = STR."{sk},就是牛";

System.out.println(str1);

}

// 结果.....

死磕 Java 新特性,就是牛

上面的 STR."{sk},就是牛" 就是一个模板表达式,它主要包含了三个部分:

模版处理器:STR

包含内嵌表达式({blog})的模版

通过.把前面两部分组合起来,形式如同方法调用

当模版表达式运行的时候,模版处理器会将模版内容与内嵌表达式的值组合起来,生成结果。

这个例子只是 STR模版处理器一个很简单的功能,它可以做的事情有很多。

数学运算

比如上面的 x + y = ?:

@Test

public void STRTest() {

int x = 1,y =2;

String str = STR."{x} + {y} = {x + y}";

System.out.println(str);

}

这种写法是不是简单明了了很多?

调用方法

STR模版处理器还可以调用方法,比如:

ini 代码解读复制代码String str = STR."今天是:{ LocalDate.now()} ";

当然也可以调用我们自定义的方法:

@Test

public void STRTest() {

String str = STR."{getSkStr()},就是牛";

System.out.println(str);

}

public String getSkStr() {

return "死磕 Java 新特性";

}

访问成员变量

STR模版处理器还可以访问成员变量,比如:

public record User(String name,Integer age) {

}

@Test

public void STRTest() {

User user = new User("大明哥",18);

String str = STR."{user.name()}今年{user.age()}";

System.out.println(str);

}

需要注意的是,字符串模板表达式中的嵌入表达式数量没有限制,它从左到右依次求值,就像方法调用表达式中的参数一样。例如:

@Test

public void STRTest() {

int i = 0;

String str = STR."{i++},{i++},{i++},{i++},{i++}";

System.out.println(str);

}

// 结果......

0,1,2,3,4

同时,表达式中也可以嵌入表达式:

@Test

public void STRTest() {

String name = "大明哥";

String sk = "死磕 Java 新特性";

String str = STR."{name}的{STR."{sk},就是牛..."}";

System.out.println(str);

}

// 结果......

大明哥的死磕 Java 新特性,就是牛...

但是这种嵌套的方式会比较复杂,容易搞混,一般不推荐。

多行模板表达式

为了解决多行字符串处理的复杂性,Java 13 引入文本块(Java 13 新特性—文本块),它是使用三个双引号(""")来标记字符串的开始和结束,允许字符串跨越多行而无需显式的换行符或字符串连接。如下:

String html = """

<html>

<body>

http://skjava.com

<ul>

<li>死磕 Java 新特性</li>

<li>死磕 Java 并发</li>

<li>死磕 Netty</li>

<li>死磕 Redis</li>

</ul>

</body>

</html>

""";

如果字符串模板表达式,我们就只能拼接这串字符串了,这显得有点儿繁琐和麻烦。而字符串模版表达式也支持多行字符串处理,我们可以利用它来方便的组织html、json、xml等字符串内容,比如这样:

@Test

public void STRTest() {

String title = "死磕 Java - 一站式 Java 学习平台";

String sk1 = "死磕 Java 新特性";

String sk2 = "死磕 Java 并发";

String sk3 = "死磕 Netty";

String sk4 = "死磕 Redis";

String html = STR."""

<html>

<body>

<h2>{title}</h2>

<ul>

<li>{sk1}</li>

<li>{sk2}</li>

<li>{sk3}</li>

<li>{sk4}</li>

</ul>

</body>

</html>

""";

System.out.println(html);

}

如果决定定义四个 sk 变量麻烦,可以整理为一个集合,然后调用方法生成 <li> 标签。

FMT 模板处理器

FMT 是 Java 定义的另一种模板处理器。它除了与STR模版处理器一样提供插值能力之外,还提供了左侧的格式化处理。

Java 版本更新类型JEP更新内容Java 17第一次预览JEP 406引入模式匹配的 Swith 表达式作为预览特性。Java 18第二次预览JEP 420对其做了改进和细微调整Java 19第三次预览JEP 427进一步优化模式匹配的 Swith 表达式Java 20第四次预览JEP 433Java 21正式特性JEP 441成为正式特性

如果使用 STR 模板处理器,代码如下:

@Test

public void STRTest() {

SwitchHistory[] switchHistories = new SwitchHistory[]{

new SwitchHistory("Java 17","第一次预览","JEP 406","引入模式匹配的 Swith 表达式作为预览特性。"),

new SwitchHistory("Java 18","第二次预览","JEP 420","对其做了改进和细微调整"),

new SwitchHistory("Java 19","第三次预览","JEP 427","进一步优化模式匹配的 Swith 表达式"),

new SwitchHistory("Java 20","第四次预览","JEP 433",""),

new SwitchHistory("Java 21","正式特性","JEP 441","成为正式特性"),

};

String history = STR."""

Java 版本 更新类型 JEP 更新内容

{switchHistories[0].javaVersion()} {switchHistories[0].updateType()} {switchHistories[0].jep()} {switchHistories[0].content()}

{switchHistories[1].javaVersion()} {switchHistories[1].updateType()} {switchHistories[1].jep()} {switchHistories[1].content()}

{switchHistories[2].javaVersion()} {switchHistories[2].updateType()} {switchHistories[2].jep()} {switchHistories[2].content()}

{switchHistories[3].javaVersion()} {switchHistories[3].updateType()} {switchHistories[3].jep()} {switchHistories[3].content()}

{switchHistories[4].javaVersion()} {switchHistories[4].updateType()} {switchHistories[4].jep()} {switchHistories[4].content()}

""";

System.out.println(history);

}

得到的效果是这样的:

版本 更新类型 JEP 更新内容

Java 17 第一次预览 JEP 406 引入模式匹配的 Swith 表达式作为预览特性。

Java 18 第二次预览 JEP 420 对其做了改进和细微调整

Java 19 第三次预览 JEP 427 进一步优化模式匹配的 Swith 表达式

Java 20 第四次预览 JEP 433

Java 21 正式特性 JEP 441 成为正式特性

是不是很丑?完全对不齐,没法看。为了解决这个问题,就可以采用FMT模版处理器,在每一列左侧定义格式:

@Test

public void STRTest() {

SwitchHistory[] switchHistories = new SwitchHistory[]{

new SwitchHistory("Java 17","第一次预览","JEP 406","引入模式匹配的 Swith 表达式作为预览特性。"),

new SwitchHistory("Java 18","第二次预览","JEP 420","对其做了改进和细微调整"),

new SwitchHistory("Java 19","第三次预览","JEP 427","进一步优化模式匹配的 Swith 表达式"),

new SwitchHistory("Java 20","第四次预览","JEP 433",""),

new SwitchHistory("Java 21","正式特性","JEP 441","成为正式特性"),

};

String history = FMT."""

Java 版本 更新类型 JEP 更新内容

%-10s{switchHistories[0].javaVersion()} %-9s{switchHistories[0].updateType()} %-10s{switchHistories[0].jep()} %-20s{switchHistories[0].content()}

%-10s{switchHistories[1].javaVersion()} %-9s{switchHistories[1].updateType()} %-10s{switchHistories[1].jep()} %-20s{switchHistories[1].content()}

%-10s{switchHistories[2].javaVersion()} %-9s{switchHistories[2].updateType()} %-10s{switchHistories[2].jep()} %-20s{switchHistories[2].content()}

%-10s{switchHistories[3].javaVersion()} %-9s{switchHistories[3].updateType()} %-10s{switchHistories[3].jep()} %-20s{switchHistories[3].content()}

%-10s{switchHistories[4].javaVersion()} %-9s{switchHistories[4].updateType()} %-10s{switchHistories[4].jep()} %-20s{switchHistories[4].content()}

""";

System.out.println(history);

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容