Contents

Groovy 中字符串简介

1. 概述

在本教程中,我们将仔细研究Groovy 中的几种类型的字符串,包括单引号、双引号、三引号和斜线字符串。

我们还将探索 Groovy 对特殊字符、多行、正则表达式、转义和变量插值的字符串支持。

2. 增强 java.lang.String

最好先说明一下,由于 Groovy 基于 Java,它具有 Java 的所有 字符串功能,如连接、字符串 API 以及字符串常量池的固有优势。

让我们首先看看 Groovy 如何扩展这些基础知识。

2.1. 字符串连接

字符串连接只是两个字符串的组合:

def first = 'first'
def second = "second"
def concatenation = first + second
assertEquals('firstsecond', concatenation)

Groovy 在此基础上构建的地方是它的其他几种字符串类型,我们稍后会看一下。 请注意,我们可以互换地连接每种类型。

2.2. 字符串插值

现在,Java 通过printf提供了一些非常基本的模板,但 Groovy 更深入,提供 字符串插值,即使用变量模板化字符串的过程

def name = "Kacper"
def result = "Hello ${name}!"
assertEquals("Hello Kacper!", result.toString())

虽然 Groovy 支持所有字符串类型的连接,但它只为某些类型提供插值。

2.3. GString

但是在这个例子中隐藏了一点问题——我们为什么要调用toString()

实际上,result不是 String类型,即使它看起来像。

因为String类是 final,Groovy 的支持插值的字符串类 GString不会继承它。换句话说,为了让 Groovy 提供这种增强,它有自己的字符串类 GString,它不能从 String 扩展。

简单地说,如果我们这样做:

assertEquals("Hello Kacper!", result)

这会调用assertEquals(Object, Object),我们得到:

java.lang.AssertionError: expected: java.lang.String<Hello Kacper!>
  but was: org.codehaus.groovy.runtime.GStringImpl<Hello Kacper!>
Expected :java.lang.String<Hello Kacper!> 
Actual   :org.codehaus.groovy.runtime.GStringImpl<Hello Kacper!>

3. 单引号字符串

Groovy 中最简单的字符串可能是带有单引号的字符串:

def example = 'Hello world'

在底层,这些只是普通的 Java string当我们需要在字符串中包含引号时,它们会派上用场。

代替:

def hardToRead = "Kacper loves \"Lord of the Rings\""

我们可以轻松地将一个字符串与另一个字符串连接:

def easyToRead = 'Kacper loves "Lord of the Rings"'

因为我们可以像这样交换引号类型,所以它减少了转义引号的需要。

4. 三重单引号字符串

三重单引号字符串在定义多行内容的上下文中很有帮助。

例如,假设我们有一些 JSON来表示为字符串:

{
    "name": "John",
    "age": 20,
    "birthDate": null
}

我们不需要使用连接和显式换行符来表示这一点。

相反,让我们使用三重单引号字符串:

def jsonContent = '''
{
    "name": "John",
    "age": 20,
    "birthDate": null
}
'''

Groovy 将其存储为一个简单的 Java string,并为我们添加所需的连接和换行符。

不过,还有一个挑战需要克服。

通常为了代码的可读性,我们缩进我们的代码:

def triple = '''
    firstline
    secondline
'''

但是三重单引号字符串保留空白。这意味着上面的字符串实际上是:

(newline)
    firstline(newline)
    secondline(newline)

不是:

1 firstline(newline)
2 secondline(newline)

就像我们打算的那样。

请继续关注我们如何摆脱它们。

4.1. 换行符

让我们确认我们之前的字符串以换行符开头

assertTrue(triple.startsWith("\n"))

可以去掉那个字符。**为了防止这种情况,我们需要将一个反斜杠 \ **作为第一个和最后一个字符:

def triple = '''\
    firstline
    secondline
'''

现在,我们至少有:

1 firstline(newline)
2 secondline(newline)

解决了一个问题,还有一个问题要解决。

4.2. 剥离代码缩进

接下来,让我们处理缩进。 我们希望保留我们的格式,但删除不必要的空白字符。

Groovy String API 来救场了!

要删除字符串每一行的前导空格,我们可以使用 Groovy 默认方法之一,String#stripIndent()

def triple = '''\
    firstline
    secondline'''.stripIndent()
assertEquals("firstline\nsecondline", triple)

请注意,通过将刻度向上移动一行,我们还删除了尾随换行符。

4.3. 相对缩进

我们应该记住,stripIndent不叫 stripWhitespace

stripIndent确定字符串中缩短的非空白行的缩进量。

所以,让我们对三元组变量的缩进稍作改动:

class TripleSingleQuotedString {
    @Test
    void 'triple single quoted with multiline string with last line with only whitespaces'() {
        def triple = '''\
            firstline
                secondline\
        '''.stripIndent()
        // ... use triple
    }
}

打印*triple *将向我们展示:

firstline
    secondline

由于firstline是缩进最少的非空白行,它变成零缩进,secondline仍然相对于它缩进。

还要注意,这一次,我们用斜线删除尾随空格,就像我们之前看到的那样。

4.4. 用*stripMargin()*剥离

为了获得更多控制,我们可以使用 | 告诉 Groovy 从哪里开始行。和stripMargin

def triple = '''\
    |firstline
    |secondline'''.stripMargin()

这将显示:

firstline
secondline

管道说明了字符串的那一行真正开始的位置。

此外,我们可以使用自定义分隔符将CharacterCharSequence作为参数传递给stripMargin

太好了,我们去掉了所有不必要的空格,我们的字符串只包含我们想要的!

4.5. 转义特殊字符

由于三重单引号字符串的所有优点,需要转义作为我们字符串一部分的单引号和反斜杠的自然结果。

为了表示特殊字符,我们还需要用反斜杠转义它们。最常见的特殊字符是换行符 ( \n ) 和制表符 ( \t )。 例如:

def specialCharacters = '''hello \'John\'. This is backslash - \\ \nSecond line starts here'''

将导致:

hello 'John'. This is backslash - \
Second line starts here

有一些我们需要记住,即:

  • \t - 制表
  • \n – 换行符
  • \b - 退格
  • \r - 回车
  • \\ – 反斜杠
  • \f - 换页
  • ' - 单引号

5. 双引号字符串

虽然双引号字符串也只是 Java String,但它们的特殊功能是插值。当双引号字符串包含插值字符时,Groovy 将 Java  String切换为GString

5.1. GString和惰性求值

**我们可以通过用${}包围表达式来插入双引号字符串,或者用$来插入带点的表达式。

不过,它的求值是惰性的——在传递给需要 String 的方法之前,它不会被转换为String

def string = "example"
def stringWithExpression = "example${2}"
assertTrue(string instanceof String)
assertTrue(stringWithExpression instanceof GString)
assertTrue(stringWithExpression.toString() instanceof String)

5.2. 引用变量的占位符

我们可能想要对插值做的第一件事就是向它发送一个变量引用:

def name = "John"
def helloName = "Hello $name!"
assertEquals("Hello John!", helloName.toString())

5.2. 带有表达式的占位符

但是,我们也可以给它表达式:

def result = "result is ${2 * 2}"    
assertEquals("result is 4", result.toString())

我们甚至可以将语句放入占位符中,但这被认为是不好的做法。

5.3. 带点运算符的占位符

我们甚至可以在字符串中遍历对象层次结构:

def person = [name: 'John']
def myNameIs = "I'm $person.name, and you?"
assertEquals("I'm John, and you?", myNameIs.toString())

使用 getter,Groovy 通常可以推断出属性名称。

*但是如果我们直接调用一个方法,我们需要使用${}***因为括号:

def name = 'John'
def result = "Uppercase name: ${name.toUpperCase()}".toString()
assertEquals("Uppercase name: JOHN", result)

5.4. GStringString中的hashCode

与普通的java.util.String相比,内插字符串无疑是天赐之物,但它们在一个重要方面有所不同。

请看,Java String是不可变的,因此对给定字符串调用 hashCode总是返回相同的值。

但是,GString哈希码可能会有所不同,因为String表示取决于插值。

实际上,即使对于相同的结果字符串,它们也不会具有相同的哈希码:

def string = "2+2 is 4"
def gstring = "2+2 is ${4}"
assertTrue(string.hashCode() != gstring.hashCode())

因此,我们永远不应该使用GString作为Map中的键!

6. 三重双引号字符串

所以,我们看到了三重单引号字符串,也看到了双引号字符串。

让我们结合两者的力量来获得两全其美 - 多行字符串插值:

def name = "John"
def multiLine = """
    I'm $name.
    "This is quotation from 'War and Peace'"
"""

另外,请注意我们不必转义单引号或双引号

7. 斜线

现在,假设我们正在使用正则表达式做某事,因此我们到处都在转义反斜杠:

def pattern = "\\d{1,3}\\s\\w+\\s\\w+\\\\\\w+"

这显然是一团糟。

为了解决这个问题,Groovy 通过斜线字符串原生支持正则表达式:

def pattern = /\d{3}\s\w+\s\w+\\\w+/
assertTrue("3 Blind Mice\Men".matches(pattern))

斜线字符串可以是插值的和多行的

def name = 'John'
def example = /
    Dear ([A-Z]+),
    Love, $name
/

当然,我们必须避开正斜杠:

def pattern = /.*foobar.*\/hello.*/

而且我们不能用Slashy String表示空字符串,因为编译器将 // 理解为注释:

// if ('' == //) {
//     println("I can't compile")
// }

8. 美元斜杠字符串

斜线字符串很棒,尽管必须逃避正斜线是一件很糟糕的事情。 为了避免正斜杠的额外转义,我们可以使用美元斜杠字符串。

假设我们有一个正则表达式模式:[0-3]+/[0-3]+。它是美元斜线字符串的一个很好的候选者,因为在斜线字符串中,我们必须写:[0-3]+//[0-3]+

Dollar-slashy**字符串是多行 GString,以 $/ 开头并以 /$ 结尾。**要转义美元或正斜杠,我们可以在其前面加上美元符号 ($),但这不是必需的。

我们不需要在GString占位符中转义 $。

例如:

def name = "John"
def dollarSlashy = $/
    Hello $name!,
    I can show you a $ sign or an escaped dollar sign: $$ 
    Both slashes work: \ or /, but we can still escape it: $/

    We have to escape opening and closing delimiters:
    - $$$/  
    - $/$$
 /$

会输出:

Hello John!,
I can show you a $ sign or an escaped dollar sign: $ 
Both slashes work: \ or /, but we can still escape it: /
We have to escape opening and closing delimiter:
- $/  
- /$

9. 字符

熟悉 Java 的人已经想知道 Groovy 对字符做了什么,因为它对字符串使用单引号。

实际上,  Groovy没有明确的字符文字。

三种方法可以使Groovy 字符串成为实际字符:

  • 声明变量时显式使用 ‘char’ 关键字
  • 使用“as”运算符
  • 通过转换为 ‘char’

让我们来看看它们:

char a = 'A'
char b = 'B' as char
char c = (char) 'C'
assertTrue(a instanceof Character)
assertTrue(b instanceof Character)
assertTrue(c instanceof Character)

当我们想将字符作为变量保留时,第一种方法非常方便。当我们想将字符作为参数传递给函数时,其他两种方法更有趣。