问题

昨天看到一篇博文,文中谈到一道 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" 的内容被用来替换前一个参数中匹配的字符串。

以字符串 "*ab**de**" 为例:

  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");

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

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

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

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

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

关于 Greediness 和 Laziness 请看这里。

正则表达式效率

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

参考链接

Java String.replaceAll() 与后向引用(backreference)的更多相关文章

  1. Java String.replaceAll()方法

    声明  以下是java.lang.String.replaceAll()方法的声明 public String replaceAll(String regex, String replacement) ...

  2. JAVA中string.replace()和string.replaceAll()的区别及用法

    乍一看,字面上理解好像replace只替换第一个出现的字符(受javascript的影响),replaceall替换所有的字符,其实大不然,只是替换的用途不一样.    public String r ...

  3. java基础之 数据类型 & 值传递 引用传递 & String & 四种引用类型

    一.Java数据类型 分为基本数据类型与引用数据类型 基本数据类型: byte:Java中最小的数据类型,在内存中占1个字节(8 bit),取值范围-128~127,默认值0 short:短整型,2个 ...

  4. Java String类详解

    Java String类详解 Java字符串类(java.lang.String)是Java中使用最多的类,也是最为特殊的一个类,很多时候,我们对它既熟悉又陌生. 类结构: public final ...

  5. Java String类具体解释

    Java String类具体解释 Java字符串类(java.lang.String)是Java中使用最多的类,也是最为特殊的一个类,非常多时候,我们对它既熟悉又陌生. 类结构: public fin ...

  6. Java String, StringBuffer和StringBuilder实例

    1- 分层继承2- 可变和不可变的概念3- String3.1- 字符串是一个非常特殊的类3.2- String 字面值 vs. String对象3.3- String的方法3.3.1- length ...

  7. String replaceAll 正则注意事项及特殊用法(xjl456852原创)

    我们知道String replaceAll(参数a, 参数b) 参数a是需要些正则表达式的. 但是今天试了试,发现参数b也有一些其它特性. 查看源码后,发现有些特性是平时不怎么用的.下面我来介绍一下这 ...

  8. Java String 综述(上篇)

    摘要: Java 中的 String类 是我们日常开发中使用最为频繁的一个类,但要想真正掌握的这个类却不是一件容易的事情.笔者为了还原String类的真实全貌,先分为上.下两篇博文来综述Java中的S ...

  9. Java 8新特性-4 方法引用

    对于引用来说我们一般都是用在对象,而对象引用的特点是:不同的引用对象可以操作同一块内容! Java 8的方法引用定义了四种格式: 引用静态方法     ClassName :: staticMetho ...

随机推荐

  1. jQuery 树型菜单插件(Treeview)

    jQuery Treeview 提供了一个无序灵活的可折叠的树形菜单.试用于一些菜单的导航,支持基于 cookie 的持久性菜单

  2. 数据结构(逻辑结构,物理结构,特点) C#多线程编程的同步也线程安全 C#多线程编程笔记 String 与 StringBuilder (StringBuffer) 数据结构与算法-初体验(极客专栏)

    数据结构(逻辑结构,物理结构,特点) 一.数据的逻辑结构:指反映数据元素之间的逻辑关系的数据结构,其中的逻辑关系是指数据元素之间的前后件关系,而与他们在计算机中的存储位置无关.逻辑结构包括: 集合 数 ...

  3. Springboot接收参数

    接收参数有三个方法. 1.接收id的方法: @RestController public class ControllerTest { //在这里读取配置文件 @Autowired private T ...

  4. FPGA开发流程1(详述每一环节的物理含义和实现目标)

    要知道,要把一件事情做好,不管是做哪们技术还是办什么手续,明白这个事情的流程非常关键,它决定了这件事情的顺利进行与否.同样,我们学习FPGA开发数字系统这个技术,先撇开使用这个技术的基础编程语言的具体 ...

  5. (2.1)Smali系列学习之基础语法

    一.什么是Smali?Smali,Baksmali分别是指安卓系统里的Java虚拟机(Dalvik)所使用的一种dex格式文件的汇编器,反汇编器.其语法是一种宽松式的Jasmin/dedexer语法, ...

  6. python学习之join()

    str.join(iterable) 该方法用来分隔字符串的. 例子 >>> b':'.join((b'leo',b'999')) b'leo:999' >>> ' ...

  7. Android实例-IdHTTP下载(并实现自动安装)(XE10+小米2)

    相关资料: 1.群号 383675978 2.运行时提示"connection closed gracefully"错误原因与解决 http://www.delphifans.co ...

  8. PHP学习笔记(3)GD库画图

    <?php //加header头,不然浏览器乱码 header("content-type: image/png"); //创建画布资源 $img = imagecreate ...

  9. mybatis的foreach写用法

    一.mybatis查询 public abstract List<Model> findByIds(@Param("ids")List<Integer> i ...

  10. TCP/IP笔记(八)应用层协议

    TCP/IP的应用层涵盖了OSI参考模型中第5.第6.第7层的所有功能,不仅包含了管理通信连接的会话层功能.转换数据格式的标识层功能,还包括与对端主机交互的应用层功能在内的所有功能. 利用网络的应用程 ...