1. 概述

在大多数应用中,检查字符串是否符合业务规则至关重要。通常,我们需要验证名称是否只包含允许的字符、电子邮件是否格式正确,或者密码是否有限制。

在这篇教程中,我们将学习如何检查一个字符串是否全为字母数字字符,这在很多情况下都非常有用。

2. 字母数字字符

首先,让我们明确这个术语以避免混淆。字母数字字符是指字母和数字的组合。更具体地说,指的是拉丁字母和阿拉伯数字。因此,我们不会将任何特殊字符或下划线视为字母数字字符的一部分。

3. 检查方法

一般来说,我们有两种主要的方法来解决这个问题。第一种使用正则表达式模式,第二种是逐个检查字符。

3.1. 使用正则表达式

这是最简单的方法,需要提供正确的正则表达式模式。在我们的例子中,我们将使用以下模式:

String REGEX = "^[a-zA-Z0-9]*$";

从技术上讲,我们可以使用\\w*快捷方式来识别"单词字符",但不幸的是,它不符合我们的要求,因为这个模式可能会包含下划线,可以这样表示:\[a-zA-Z0-9_\]

确定了正确的模式后,下一步是将给定的字符串与之进行匹配。可以直接在字符串本身上进行操作:

boolean result = TEST_STRING.matches(REGEX);

然而,这不是最好的方法,特别是如果我们经常需要进行这样的检查。每次调用match(String)方法时,String都会重新编译正则表达式。因此,最好使用一个静态的Pattern:

Pattern PATTERN = Pattern.compile(REGEX);
Matcher matcher = PATTERN.matcher(TEST_STRING);
boolean result = matcher.matches();

总的来说,这是一个简单、灵活的方法,使代码简洁易懂。

3.2. 逐个检查字符

另一种方法是逐个检查字符串中的每个字符。我们可以使用任何方法来遍历给定的字符串。为了演示目的,我们使用简单的for循环:

boolean result = true;
for (int i = 0; i < TEST_STRING.length(); ++i) {
    int codePoint = TEST_STRING.codePointAt(i);
    if (!isAlphanumeric(codePoint)) {
        result = false;
        break;
    }
}

我们可以用多种方式实现isAlphanumeric(int),但总体上,我们必须匹配字符在ASCII表中的代码。我们使用ASCII表是因为我们定义了初始限制,即使用拉丁字母和阿拉伯数字:

boolean isAlphanumeric(final int codePoint) {
    return (codePoint >= 65 && codePoint <= 90) ||
           (codePoint >= 97 && codePoint <= 122) ||
           (codePoint >= 48 && codePoint <= 57);
}

此外,我们可以使用Character.isAlphabetic(int)Character.isDigit(int)。这些方法高度优化,可能提高应用程序的性能:

boolean result = true;
for (int i = 0; i < TEST_STRING.length(); ++i) {
    final int codePoint = TEST_STRING.codePointAt(i);
    if (!Character.isAlphabetic(codePoint) || !Character.isDigit(codePoint)) {
        result = false;
        break;
    }
}

这种方法需要更多的代码,并且非常命令式。同时,它提供了透明实现的好处。然而,不同的实现可能会无意中恶化这种方法的空间复杂度:

boolean result = true;
for (final char c : TEST_STRING.toCharArray()) {
    if (!isAlphanumeric(c)) {
        result = false;
        break;
    }
}

toCharArray()方法会创建一个单独的数组来存放字符串中的字符,从而将空间复杂度从O(1)降级到O(n)。使用流API的方法也是如此:

boolean result = TEST_STRING.chars().allMatch(this::isAlphanumeric);

请注意这些陷阱,特别是如果性能对应用程序至关重要的话。

4. 优缺点

从之前的示例中可以看出,第一种方法编写和阅读起来更简单,而第二种方法需要更多代码,且可能包含更多错误。但从性能角度看,让我们使用JMH进行比较。测试设置为仅运行一分钟,因为这足以比较它们的吞吐量。

我们得到如下结果。得分表示操作在秒内的数量。因此,得分越高,解决方案越高效:

Benchmark                                                                   Mode  Cnt           Score   Error  Units
AlphanumericPerformanceBenchmark.alphanumericIteration                     thrpt        165036629.641          ops/s
AlphanumericPerformanceBenchmark.alphanumericIterationWithCharacterChecks  thrpt       2350726870.739          ops/s
AlphanumericPerformanceBenchmark.alphanumericIterationWithCopy             thrpt        129884251.890          ops/s
AlphanumericPerformanceBenchmark.alphanumericIterationWithStream           thrpt         40552684.681          ops/s
AlphanumericPerformanceBenchmark.alphanumericRegex                         thrpt         23739293.608          ops/s
AlphanumericPerformanceBenchmark.alphanumericRegexDirectlyOnString         thrpt         10536565.422          ops/s

如我们所见,存在可读性与性能之间的权衡。更易于阅读和声明式的解决方案往往性能较低。同时,请注意不必要的优化可能弊大于利。对于大多数应用来说,正则表达式是一个清晰且易于扩展的好解决方案。

然而,如果应用依赖于大量文本匹配特定规则,迭代方法的性能会更好。这最终降低了CPU使用率,减少了停机时间并提高了吞吐量。

5. 总结

检查字符串是否全为字母数字字符有几种方法。每种方法都有其优点和缺点,需要仔细考虑。选择可以归结为可扩展性与性能之间的权衡。

只有在确实需要性能的时候才进行优化,因为优化后的代码通常更难阅读,更容易出现难以调试的错误。

一如既往,代码可在GitHub上找到。