Java 中 String 的坑:从源码分析 replace 与 replaceAll 的异同

最近快播出了个云帆搜索,公司的项目要用到它的视频搜索 API。

它返回的链接并不能直接调用 QVOD 插件播放,而是先 redirect 到另一个 HTML 页面,然后根据一堆关键字以及 index 从页面或者隐藏的一个 JS 文件中抓取 qvod:// 协议的 URL。

这看起来真的很像是写 Web Spider 了,还好 Java 的 String 类封装了一大堆方法,不然如果用 C 语言连个 replace 方法都没有,还得用 strcatstrcpy 等函数去自己实现。

既然是从 HTML 网页中抓取,肯定会遇到编码问题。比如本来就一个 qvod:// ,但很多网页却出现的是 qvod%3A//qvod:%2F%2Fqvod%3A%2F%2F 这几个变种。

为了后面便于截取播放链接,首先必须将这三个部分 UTF8 编码的 String 替换成 qvod:// 。因为是要全部替换,想当然地就调用了 replaceAll() 方法,但发现并没有达到预期的效果。

然后依然是上 StackOverflow,发现了问题所在:replace()replaceAll() 方法其实都是全部替换,唯一不同的是后者第一个参数是正则表达式!所以像使用 replace 一样传一个非正则表达式的 string 过去肯定不行。

好吧,作为一个从大二开始到现在写了快 4 年 Java 的程序员来说,也许这真的是一个不该有的盲点。

但这两个方法命名总觉得很坑爹,如果定义为 replaceByRegex(String regex, String replacement) 多好!

现在真的体会到 ObjC 中的那种方法书写规则(每个参数前都带签名关键词)的优势所在:

- (NSString *)replaceByRegex:(NSString *)regex withReplacement:(NSString *)replacement {
}

乍一看有点 redundant ,但确实很 descriptive ,一目了然,调用时不容易出错。正如 Objective-C Succinctly 中所描述:

Objective-C method names are designed to be as descriptive as possible.

The idea is to define a method in such a way that reading it aloud literally tells you what it does.

Objective-C makes it very hard to accidentally pass the wrong value to a method.

言归正传,既然遇到这个问题,就从 String 类的源码分析二者究竟有何不同。

首先是 replace() 方法:

public String replace(CharSequence target, CharSequence replacement) {
    return Pattern.compile(target.toString(), Pattern.LITERAL)
        .matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}

然后是 replaceAll() 方法:

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

可以看到,二者内部都是调用的 java.util.regex 包下 Pattern 类的 replaceAll() 方法。

但是二者在调用 compile() 方法的到 Pattern 实例的时候传入的参数不同,replace() 方法多传入了一个 Pattern.LITERAL 常量字段,这就表示当做文字而不是正则表达式处理。

以下是 Pattern 类中对 LITERAL 常量的注释:

Enables literal parsing of the pattern.

When this flag is specified then the input string that specifies the pattern is treated as a sequence of literal characters.

Metacharacters or escape sequences in the input sequence will be given no special meaning.

The flags CASE_INSENSITIVE and UNICODE_CASE retain their impact on matching when used in conjunction with this flag.

The other flags become superfluous.

There is no embedded flag character for enabling literal parsing.