Skip to content

Elvis 运算符 (The Elvis Operator)

Elvis 运算符 (?:) 是三元运算符语法的简写,其灵感源自 Groovy 语言。

在使用标准的三元运算符语法时,你通常需要将一个变量重复写两次,如下面的 Java 示例所示:

java
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

相比之下,你可以使用 Elvis 运算符(其命名是因为该符号酷似猫王 Elvis Presley 的发型)。以下示例展示了如何在 SpEL 表达式中使用它:

java
ExpressionParser parser = new SpelExpressionParser();

// 如果 name 为 null 或空字符串,则返回 'Unknown'
String name = parser.parseExpression("name ?: 'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'
kotlin
val parser = SpelExpressionParser()

// 如果 name 为 null 或空字符串,则返回 'Unknown'
val name = parser.parseExpression("name ?: 'Unknown'").getValue(Inventor(), String::class.java)
println(name)  // 'Unknown'

注意:对空字符串的处理

SpEL 的 Elvis 运算符不仅会将 null 视为缺失,还会将空字符串 ("") 视同 null 处理。因此,前文的 Java 示例若要完全模拟 SpEL 的语义,谓词部分应写为 name != null && !name.isEmpty()

提示:对 Optional 的支持 (Spring 7.0+)

Spring Framework 7.0 开始,SpEL 的 Elvis 运算符支持对 java.util.Optional 进行透明解包。

例如,对于表达式 A ?: B

  • 如果 Anull空的 Optional,则表达式结果为 B
  • 如果 A 是一个非空的 Optional,则表达式会自动解包并返回该 Optional 中包含的对象(等同于调用 A.get())。

以下是一个更复杂的示例:

java
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
// 1. name 有值,返回原值
String name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

// 2. 将 name 设置为空字符串
tesla.setName("");
// 3. 空字符串触发默认值,返回 'Elvis Presley'
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley
kotlin
val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()

val tesla = Inventor("Nikola Tesla", "Serbian")
// 1. name 有值
var name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name)  // Nikola Tesla

// 2. 设置为空字符串
tesla.setName("")
// 3. 触发默认值
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name)  // Elvis Presley

Elvis 运算符非常适合用于在表达式中应用默认值。以下示例展示了如何在 @Value 注解中使用它:

java
@Value("#{systemProperties['pop3.port'] ?: 25}")

如果定义了名为 pop3.port 的系统属性,则注入该属性值;如果未定义(即返回 null),则注入 25


补充教学

1. 为什么叫猫王 (Elvis)?

这是一个有趣的编程梗:将符号 ?: 顺时针旋转 90 度,? 就像是猫王标志性的偏分斜刘海,而 : 则是他的眼睛。在编程界,这就是“默认值”运算符的代名词。

2. 核心区别:对“空字符串”的宽容度

在原生 Java 或大部分语言中,value ?: default 往往只针对 null。 但 Spring 团队考虑到配置文件(如 .properties.yml)中经常会出现配置了 Key 但 Value 为空的情况,因此 SpEL 的实现逻辑是:

  • 触发默认值的条件value == null 或者 value.equals("")
  • 注意:如果字符串全是空格(" "),它不会触发默认值,除非你链式调用了 .trim(),如 name?.trim() ?: 'Default'

3. Spring 7.0 的 Optional 飞跃

在 Spring 7 之前,如果你处理一个返回 Optional 的属性,你可能需要写成: myOptional.isPresent() ? myOptional.get() : 'Default' 现在,通过透明解包,你只需要: myOptional ?: 'Default' 这极大地简化了 SpEL 在响应式编程和现代化 Java API 中的应用。

4. Elvis 运算符 vs 属性占位符默认值

在 Spring 中有两种常见的默认值写法,它们的使用场景不同:

  • ${timeout:1000}:这是属性占位符。它在容器启动阶段解析,逻辑简单,仅用于系统配置。
  • #{systemProperties['timeout'] ?: 1000}:这是 SpEL。它在运行时解析,可以包含复杂的逻辑运算、方法调用等。
  • 建议:如果是纯配置项注入,优先使用 ${...:...};如果涉及计算或从 Bean 属性动态获取,使用 #{... ?: ...}

5. 组合技:安全引用 + Elvis

Elvis 运算符经常与下一章的“安全引用运算符 (?.)”配合使用: @Value("#{user?.address?.city ?: '未知城市'}") 这样即使 useraddressnull,整个表达式也不会抛出 NPE,而是最终优雅地落在 '未知城市' 上。

Based on Spring Framework.