Java String.replaceAll() 与后向引用(backreference)

问题

昨天看到一篇博文,文中谈到一道 Java 面试题:

给定一字符串,若该字符串中间包含 "",则删除该 "";若该字符串首字符或尾字符为 "",则保留该 ""。

举几个例子(箭头左边为输入,箭头右边为输出):

* -- *
** -- **
**** -- ** 
*ab**de** -- *abde*

我觉得应该用正则表达式来处理,但想不出正则表达式该怎么写。

第一种解答

该博文的回复中有人给出下面的答案

str.replaceAll("(^\\*)|(\\*$)|\\*", "$1$2")

上机验证一下,答案是对的,但不懂为什么正则表达式要这么写。到 stackoverflow 上发帖问了一下,才大概明白是怎么回事儿。当时问的时候, 对这个问题想得不清楚,所以问的问题也是糊里糊涂。

下面是我的理解,不对之处请多拍砖:

replaceAll() 是 Java String 类的一个方法:

public String replaceAll(String regex, String replacement)

Replaces each substring of this string that matches the given regular expression with the given replacement.

(特别要注意的是,这个方法的第一个参数是一个正则表达式。我过去在第一个参数上栽过跟头。不过,这回我栽在第二个参数上。)

"(^\)|(\$)|\*" 解释:

(^\) :capturing group 1, 匹配字符串开始处的 * (\$) :capturing group 2, 匹配字符串结尾处的 * \* : 匹配任意位置的 *

  • 因为 "" 在正则表达式中是特殊字符,所以需要使用转义字符 ""。但在Java中 "" 也是特殊字符,所以需要再一次使用 "",这样就造成 "" 前面有两个 ""。
  • 圆括号 "()" 把括号内的内容作为一个 capturing group,为后面的 backreference 做准备。关于 capturing group 请看这里
  • "|" 表明左右的表达式是 "或" 的关系。
  • "\" 单独使用的话可以匹配字符串中任意位置的 ""。但在上述的表达式中,开始和结尾处的 "" 优先被 "(^\)" 或 "(\*$)" 匹配了。

因此上面的表达式可以匹配字符串开始处的 "",或者匹配字符串结尾处的 "",或者匹配字符串任意位置的 ""。也就是说,字符串中所有的 "" 都匹配上了。

"$1$2" 解释:

$1 :backreference 第一个 capturing group
$2 :backreference 第二个 capturing group

这个参数中 "$1" 和 "$2" 的内容被用来替换前一个参数中匹配的字符串。

以字符串 "abde*" 为例:

  1. 第一个 "" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为 "","$2" 的内容为空,所以第一个 "*" 被它自己替换。

  2. 接下来 "a" 和 "b" 都不匹配,略过,继续往后走。

  3. 第二个 "" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为空,"$2" 的内容为空,所以这个 "" 被替换为空。

  4. 第三个 "" 跟第二个 "" 一样,也被替换为空。

  5. 接下来的 "d" 和 "e" 不匹配,继续往后走。

  6. 第四个 "" 匹配,跟第二个、第三个 "" 一样,被替换为空。

  7. 最后一个 "" 匹配,使用 "$1$2" 来替换。这时 "$1" 的内容为空,"$2" 的内容为 "",所以最后一个 "*" 被它自己替换。

  8. 最后的结果是:"abde"

这里有一点要注意:在正则表达式中,backreference 是用 "反斜杠 + 数字" 来表示的,比如:\1, \2 。但是,当 backreference 出现在替换字符串中时,Java 的 backreference 使用 "美元符号 + 数字" 来表示,比如:$1, $2 。据说这是跟 Perl 学的。不嫌累的话看看这个帖子吧。

第二种解答

另外一种使用正则表达式的方法是:如果 "*" 不在头,也不在尾,则替换为空。这种想法很自然,但实现起来却不容易。

String repl = str.replaceAll("(?<!^)\\*+(?!$)", "");

正则表达式解释:

(?<!^) # 如果前一个位置不是行首
\*+ # 匹配一个或多个 * (?!$) # 如果下一个位置不是行尾

"?<!" 表示 Negative Lookahead,"?!" 表示 Negative Lookbehind 。详细说明请参考这里这里

第三种解答

String repl = str.replaceAll("(^\\*)|(\\*$)|\\*+", "$1$2");

这个跟上面的第二种解答都是由同一个人回复的,但这个解答有点问题:如果结尾处有两个或两个以上的 "" 时,这些 "" 都被替换为空。

例如,若输入为 "abde",则输出为"abde",最后的那个 "*" 不见了。

这是因为缺省情况下,正则匹配处于 Greediness(贪婪) 匹配模式,会匹配尽量多的字符。"*+" 可以匹配一个或多个 "" 。在倒数第二个 "" 的时候,匹配一个 "" 或两个 "" 都可以。但它比较贪婪,所以把最后两个 "*" 都匹配上了,然后被 "$1$2" 替换为空。

把正则匹配改为 Laziness(偷懒)匹配可以解决这个问题。在表达式后面加一个 "?" 就变成 Laziness 匹配了:"*+?" 。

String repl = str.replaceAll("(^\)|(\$)|\*+?", "$1$2");

关于 Greediness 和 Laziness 请看这里

正则表达式效率

该网站可以测试正则表达式,并给出详细的解释。它还给出匹配所需的步数,你可以用这个步数来比较表达式的效率。从这个网站上看,第二种方法效率最高。

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

推荐阅读更多精彩内容