How hard is it to check for whitespace?
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
How hard is it to check for whitespace?
By Sergey Tselovalnikov on 30 October 2026. When looking through a piece of source code recently, I stumbled across this method from Guava – a popular Java library from Google. As you might’ve guessed from the name, this method checks whether a given character is a whitespace character. Yet the code looks a lot more complex than a simple c == ' ' check. This made me curious and sent me down a small rabbit hole trying to understand where all this complexity was coming from. If you’re curious how this code works too, this article is for you.
Sergey Tselovalnikov 于 2026 年 10 月 30 日撰写。最近在查看一段源代码时,我偶然发现了 Google 旗下流行的 Java 库 Guava 中的一个方法。正如你从名字中可能猜到的那样,该方法用于检查给定的字符是否为空白字符。然而,这段代码看起来比简单的 c == ' ' 检查要复杂得多。这引起了我的好奇心,并让我陷入了一个小小的研究困境,试图弄清楚所有这些复杂性从何而来。如果你也对这段代码的工作原理感到好奇,那么这篇文章就是为你准备的。
What is whitespace, anyway?
The first thing one needs when inspecting a function that checks whether a character is whitespace is to define what whitespace actually is. While the SPACE (U+0020) is probably the best-known whitespace character, it’s not the only one. If you’ve been writing code for a while, you can probably name a few more – the non-breaking space NO-BREAK SPACE (U+00A0), a tab CHARACTER TABULATION (U+0009), a newline LINE FEED (U+000A). Well, there are more. Unicode defines a White_Space property that gives us a list of 25 characters.
到底什么是空白字符?
在检查一个判断字符是否为空白的函数时,首先需要定义什么是空白字符。虽然空格(SPACE, U+0020)可能是最广为人知的空白字符,但它并不是唯一的。如果你写过一段时间的代码,你可能还能说出几个——不换行空格(NO-BREAK SPACE, U+00A0)、制表符(CHARACTER TABULATION, U+0009)和换行符(LINE FEED, U+000A)。实际上,还有更多。Unicode 定义了一个 White_Space 属性,为我们提供了 25 个字符的列表。
Each of these characters has a different purpose. FIGURE SPACE (U+2007), for example, has the same width as a digit in a table, while THIN SPACE (U+2009) and HAIR SPACE (U+200A) provide progressively smaller gaps. NO-BREAK SPACE (U+00A0) has the width of an ordinary space, but keeps the surrounding text together when wrapping a line. It’s not always easy to tell whether a character is in the whitespace set from the name or by looking at the glyph either. For instance, ZERO WIDTH SPACE (U+200B) isn’t a whitespace character at all. Despite the name, it’s a format character that can indicate a word or line break opportunity without adding a visible gap.
这些字符中的每一个都有不同的用途。例如,数字空格(FIGURE SPACE, U+2007)与表格中的数字宽度相同,而窄空格(THIN SPACE, U+2009)和头发空格(HAIR SPACE, U+200A)则提供逐渐变小的间隙。不换行空格(NO-BREAK SPACE, U+00A0)的宽度与普通空格相同,但在换行时能保持周围文本在一起。仅凭名称或字形外观并不总是容易判断一个字符是否属于空白字符集。例如,零宽空格(ZERO WIDTH SPACE, U+200B)根本不是空白字符。尽管名字如此,它实际上是一个格式字符,可以在不增加可见间隙的情况下指示单词或换行的机会。
There are also some less obvious members of the set too. OGHAM SPACE MARK (U+1680) can actually be rendered as a visible horizontal line. In Ogham fonts with a stemline, the space continues that line between letters, while in stemless fonts, it’s blank. So even the assumption that whitespace must look empty isn’t always true.
该集合中还有一些不太明显的成员。欧甘字母空格标记(OGHAM SPACE MARK, U+1680)实际上可以呈现为一条可见的水平线。在带有基线的欧甘字母字体中,该空格会延续字母之间的线条,而在无基线的字体中,它则是空白的。因此,即使是“空白字符必须看起来是空的”这一假设也并不总是正确的。
Different runtimes, different rules
Having a single definition of whitespace is only useful if everyone can agree on it. But even within a single runtime, some libraries might disagree on what whitespace is. Consider the NO-BREAK SPACE (U+00A0) I mentioned above. It’s included in Unicode’s White_Space property, but Java explicitly excludes it, along with two other non-breaking spaces. The javadoc usefully highlights that the method checks for whitespace according to Java, rather than according to the Unicode property we had a look at before.
不同的运行时,不同的规则
只有当所有人都能达成共识时,统一的空白字符定义才有意义。但即使在同一个运行时内,一些库对空白字符的定义也可能存在分歧。以我上面提到的不换行空格(U+00A0)为例。它被包含在 Unicode 的 White_Space 属性中,但 Java 明确将其排除在外,同时排除的还有另外两个不换行空格。Javadoc 有益地强调了该方法是根据 Java 的定义来检查空白字符,而不是根据我们之前看到的 Unicode 属性。
But what about other runtimes? Looking at two more popular ones, Python and JavaScript, we can see that there isn’t a single authoritative list of characters everyone agrees on. Python, for example, has a convenient str.isspace() method. And unlike Java, it considers U+00A0 a whitespace character. So possibly Python just follows Unicode’s White_Space property? Not quite. The interesting one here is FILE SEPARATOR (U+001C). It’s an ASCII control character originally intended to separate pieces of information, and it isn’t included in Unicode’s White_Space property. So Python recognises 29 whitespace characters, rather than Unicode’s 25.
那么其他运行时呢?看看另外两个流行的运行时 Python 和 JavaScript,我们可以发现并没有一个大家都认可的权威字符列表。例如,Python 有一个方便的 str.isspace() 方法。与 Java 不同,它将 U+00A0 视为空白字符。那么 Python 是否只是遵循 Unicode 的 White_Space 属性呢?并不完全是。这里有趣的是文件分隔符(FILE SEPARATOR, U+001C)。它是一个 ASCII 控制字符,最初旨在分隔信息片段,且未包含在 Unicode 的 White_Space 属性中。因此,Python 识别出 29 个空白字符,而不是 Unicode 定义的 25 个。
JavaScript takes yet another approach. It doesn’t have an equivalent of the Character.isWhitespace() method, but we can use the \s character class in a regular expression. The odd one out above is NEXT LINE (U+0085). Unicode explicitly includes it in White_Space, and Python accepts it too, but JavaScript doesn’t. Instead, JavaScript recognises ZERO WIDTH NO-BREAK SPACE (U+FEFF), which Unicode doesn’t include in the set. I haven’t looked through any other runtimes, but something tells me the inconsistencies won’t stop at these three.
JavaScript 则采取了另一种方法。它没有等同于 Character.isWhitespace() 的方法,但我们可以在正则表达式中使用 \s 字符类。上述情况中的异类是下一行字符(NEXT LINE, U+0085)。Unicode 明确将其包含在 White_Space 中,Python 也接受它,但 JavaScript 不接受。相反,JavaScript 识别零宽不换行空格(ZERO WIDTH NO-BREAK SPACE, U+FEFF),而 Unicode 并不将其包含在该集合中。我没有查看其他运行时,但直觉告诉我,这种不一致性并不会止步于这三个。
Nothing is set in stone
What adds more complexity is that Unicode is not frozen in time, and is constantly evolving. And this affects the definition of whitespace. For example, Unicode 6.3 had reclassified MONGOLIAN VOWEL SEPARATOR (U+180E) from a space separator to a format character, removing its White_Space property. The Unicode whitespace set had changed from 26 characters to 25 as a result. For runtimes and libraries that strive for backwards compatibility, these changes can be a pretty significant challenge as you’d want the standard library of a language to be as stable as it can be.
没有什么是永恒不变的
更复杂的是,Unicode 并非一成不变,而是在不断演进。这影响了空白字符的定义。例如,Unicode 6.3 将蒙古文元音分隔符(MONGOLIAN VOWEL SEPARATOR, U+180E)从空格分隔符重新分类为格式字符,从而移除了它的 White_Space 属性。结果,Unicode 空白字符集从 26 个变成了 25 个。对于追求向后兼容性的运行时和库来说,这些变化可能是一个相当大的挑战,因为你总是希望语言的标准库尽可能稳定。
Checking for whitespace fast
Let’s say we’ve finally agreed on the set of characters we want to check – the current set of Unicode whitespace characters. A naive way to implement this would be to go through the list of characters and check whether any of them matches. While this would work, it’s definitely not the most efficient approach. So what Guava’s implementation that we saw at the beginning of the article did was implement a perfect hash function – a hash function that guarantees no hash collisions for the characters in the known set.
快速检查空白字符
假设我们最终确定了要检查的字符集——即当前的 Unicode 空白字符集。实现这一点的简单方法是遍历字符列表,检查其中是否有任何字符匹配。虽然这可行,但绝对不是最高效的方法。因此,我们在文章开头看到的 Guava 实现方式是实现了一个完美哈希函数——一种保证已知集合中的字符不会发生哈希冲突的哈希函数。
Above, TABLE is a string of 32 characters – effectively a hash table mapping each index (the key) to a character (the value). It contains all 25 Unicode whitespace characters, with some duplicates filling the remaining positions. The most interesting part here is calculating the index. The multiplication 1682554634 * c produces a 32-bit integer, and >>> 27 shifts it 27 bits to the right, isolating the five most significant bits – a number in the [0, 32) range.
在上面,TABLE 是一个包含 32 个字符的字符串——实际上是一个将每个索引(键)映射到字符(值)的哈希表。它包含了所有 25 个 Unicode 空白字符,并用一些重复项填充了剩余位置。这里最有趣的部分是索引的计算。乘法 1682554634 * c 产生一个 32 位整数,而 >>> 27 将其向右移动 27 位,从而隔离出最重要的 5 位——即一个 [0, 32) 范围内的数字。