一个有效的Unicode字符串能包含FFFF吗?Java/CharacterIterator有问题吗?

26
这是java.text.CharacterIterator文档的一部分:
这个接口定义了一个文本的双向迭代协议。迭代器遍历一个有限序列的字符。previous()和next()方法用于迭代。如果...,它们将返回DONE,表示迭代器已到达序列的末尾。 static final char DONE:当迭代器到达文本的结尾或开头时返回的常量。该值为\uFFFF,“不是字符”的值,在任何有效的Unicode字符串中都不应出现
我不理解的是加粗的部分,因为根据我的测试,Java String 明显可以包含 \uFFFF,并且似乎没有任何问题,除了预定的 CharacterIterator 遍历惯用语会因错误的判断而中断(例如,当实际上没有结束时,next() 返回 '\uFFFF' == DONE)。下面是一个代码片段来说明这个“问题”(在ideone.com上也可见):
import java.text.*;
public class CharacterIteratorTest {

    // this is the prescribed traversal idiom from the documentation
    public static void traverseForward(CharacterIterator iter) {
       for(char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) {
          System.out.print(c);
       }
    }

    public static void main(String[] args) {
        String s = "abc\uFFFFdef";

        System.out.println(s);
        // abc?def

        System.out.println(s.indexOf('\uFFFF'));
        // 3
        
        traverseForward(new StringCharacterIterator(s));
        // abc
    }
}

那么这里发生了什么?

  • 指定的遍历习惯用法是否“失效”,因为它对\uFFFF做出了错误的假设?
  • StringCharacterIterator实现是否“失效”,因为它没有例如在有效的Unicode字符串中禁止\uFFFF时抛出IllegalArgumentException
  • 实际上,有效的Unicode字符串不应包含\uFFFF吗?
  • 如果是这样,那么Java是否“失效”,因为(大多数情况下)允许String包含\uFFFF而违反了Unicode规范?

链接:http://www.mail-archive.com/java-dev@lucene.apache.org/msg15483.html “(LUCENE-1241) 0xffff char 不是字符串终止符” - polygenelubricants
链接:http://icu-project.org/apiref/icu4c/utf_8h.html - “在 ICU 2.4 之前设计的 ICU API 通常定义了特定于服务的“完成”值,大多数为 0xffff。这些值可能需要通过调用诸如...等函数来与实际的 U+ffff 文本内容区分开来...” - polygenelubricants
5
Java 在处理 Unicode 上已经“破损”无法修复,但原因不是你提到的那个琐碎的问题。Java 是在 Unicode 3.1 发布之前创建的,因此代码点不超过2**16。这就是为什么他们非常愚蠢地选择 char 为16位,现在我们有了一组完全破损的方法,如 size() 和 *getCharAt(...)*,它们将在未来几十年内持续让人困惑。这比你提到的小问题严重得多、深刻而令人遗憾,是 String 类中一个严重的破损方面。 - NoozNooz42
但是一如既往...向你传递爱,因为你专注于大局而不是模糊的JLS细节。那才是值得花时间的事情 ;) - NoozNooz42
1
@NoozNooz42:嗯,在过去,UCS-2 确实 是处理Unicode的一种好方式。而UTF-16是从那里迈出的合理一步。只是不要假设char实际上保存了一个字符(或者码点 - 作为通常[人类]意义上的字符将是一个可以任意长的图形素) ,而是保存了一个单个的UTF-16代码单元。它并没有出错,只是一种处理Unicode的特定方式而已。顺便说一句,Windows也是一样。 - Joey
4个回答

29

编辑(2013-12-17):Peter O.在下面提出了一个很好的观点,使得这个答案是错误的。为了历史准确性,以下是旧答案。


回答您的问题:

规定的遍历习惯用语“失效”了吗?因为它对\uFFFF做出了错误的假设?

不是的。U+FFFF是所谓的非字符。来自Unicode标准第16.7节

非字符是永久保留在Unicode标准中供内部使用的代码点。它们禁止在Unicode文本数据的开放交换中使用。

...

Unicode标准保留了66个非字符代码点。每个平面的最后两个代码点都是非字符:BMP上的U+FFFE和U+FFFF,平面1上的U+1FFFE和U+1FFFF等等,一直到平面16上的U+10FFFE和U+10FFFF,共计34个代码点。此外,在BMP中还有一个连续的范围,包括另外32个非字符代码点:U+FDD0..U+FDEF。

如果实际上在有效的Unicode字符串中禁止使用\uFFFF,那么StringCharacterIterator实现是否“失效”,因为它没有抛出IllegalArgumentException之类的异常呢?

不完全是这样。应用程序可以以任何方式在内部使用这些代码点。再次引用标准:

应用程序可以在内部使用这些非字符代码点,但不应尝试进行交换。如果在开放的交换中收到非字符,则无需以任何方式解释它。然而,识别它为非字符并采取适当的行动(例如,将其替换为U+ FFFD REPLACEMENT CHARACTER)以指示文本中的问题是一个好的做法。不建议从这样的文本中仅删除非字符代码点,因为删除未解释的字符可能导致安全问题。因此,虽然您永远不会从用户、另一个应用程序或文件中遇到这样的字符串,但如果您知道自己在做什么,您可以将其放入Java字符串中(这基本上意味着您不能在该字符串上使用CharacterIterator)。
真的吗?有效的Unicode字符串不应包含\uFFFF吗?
如上所述,任何用于交换的字符串都不得包含它们。在您的应用程序中,您可以以任何想要的方式使用它们。
当然,Java的char只是一个16位无符号整数,实际上不关心它所持有的值。
如果是这样,那么Java是否违反了Unicode规范,让String(在大多数情况下)仍然包含\uFFFF?
不。实际上,非字符的部分甚至建议使用U+ FFFF作为哨兵值:
实际上,非字符可以被视为应用程序内部的专用代码点。与第16.5节“专用字符”中讨论的专用字符不同,这些字符是已分配的字符,并且旨在在开放交换中使用,但受私人协议解释,非字符是永久保留(未分配)的,并且除了可能的应用程序内部私有用途之外,没有任何解释。
U+FFFF和U+10FFFF。这两个非字符代码点具有与特定Unicode编码形式的最大代码单元值相关联的属性。在UTF-16中,U+FFFF与最大的16位代码单元值FFFF16相关联。 U+10FFFF与最大的合法UTF-32 32位代码单元值10FFFF16相关联。此属性使这两个非字符代码点对于内部目的作为哨兵非常有用。例如,它们可以用于表示列表结束,表示在索引中保证比任何有效字符值都要高的值等等。
CharacterIterator也遵循此规则,在没有更多字符可用时返回U+FFFF。当然,这意味着如果您的应用程序还有其他用途需要该代码点,则可以考虑为此目的使用不同的非字符,因为U+FFFF已经被使用了 - 至少如果您正在使用CharacterIterator。

21
一些答案已经发生了变化。
Unicode联盟最近发布了更正9,澄清了非字符,包括U+FFFF,在Unicode字符串中的作用。它指出,虽然非字符是用于内部使用的,但它们可以在合法的Unicode字符串中出现。
这意味着语句“该值为\uFFFF,即不应出现在任何有效的Unicode字符串中的'非字符'值”现在是不正确的,因为U+FFFF可以出现在有效的Unicode字符串中。
因此:
  • 如果Unicode字符串中禁止使用\uFFFF,那么StringCharacterIterator实现是否"有问题",因为它没有抛出异常?由于U+FFFF是有效的,所以这里不适用。但是当实现遇到其他原因非法的文本(例如未配对的代理项代码点)时,它具有广泛的灵活性来发出错误信号(请参见Unicode标准第3章的符合性条款C10)。

  • 有效的Unicode字符串是否应该不包含\uFFFF? U+FFFF在有效的Unicode字符串中不是非法的。

    然而,U+FFFF被保留为非字符,因此通常不会出现在有意义的文本中。勘误表删除了“不应交换非字符”的文本,勘误表称这种情况发生在任何时候Unicode字符串跨越API边界,包括这里涉及的StringCharacterIterator API。

  • 如果是这样,那么Java是否违反了Unicode规范,因为它仍然允许String包含\uFFFF? java.lang.String的规范说:“String表示UTF-16格式的字符串。” U+FFFF在Unicode字符串中是合法的,因此Java允许包含U+FFFF的字符串不违反Unicode规范。

通常情况下,更高级的协议可以在Unicode标准之上制定自己的规则,以确定协议所接受的文档中允许使用哪些字符。例如,在XML规范中就是这种情况。一般来说,除非更高级别的协议(例如XML)另有规定,否则U+FFFF(和其他Unicode标量值)可以有效地出现在文本字符串中。实际上,目前(截至2021年11月15日),有一项努力限制在某些编程语言(如Rust)中使用Unicode双向覆盖字符,以减少由于视觉混淆而导致的安全攻击。

好的观点。虽然我现在不会编辑我的旧答案,但是我想接受应该会改变。 - Joey
但是在 "\uFFFF".getBytes("UTF-8"); 中,String是否“损坏”呢?它不会抛出错误,而是返回适当的字节:EF BF BF(至少在Java 7中是这样的)? - theory
@theory:不,不是在那种情况下。在这里,UTF-8转换使用字符串的长度来确定字符串的结尾,而不是使用非字符(如U+FFFF)。 - Peter O.
如果XML包含引用Unicode的数据,那么它是否仍然是无效的XML?因为在XML上下文中,这是不允许的。或者XML库也应该处理这种情况吗? - jan
@jan:XML在Unicode标准的基础上强加了自己的规则,涉及XML文档中允许使用哪些字符。通常情况下,除非更高级别的协议(如XML)另有规定,否则U+FFFF(和其他Unicode标量值)可以在文本字符串中有效出现。实际上,目前正在努力限制在某些编程语言(如Rust)中使用Unicode双向覆盖字符,以减少由于视觉混淆而导致的安全攻击。 - Peter O.

3
StringCharacterIterator实现是否“有问题”,因为它没有抛出IllegalArgumentException,如果实际上\uFFFF在有效的Unicode字符串中是被禁止的?
这不严格符合Unicode标准,但它与Java的其余字符串处理接口不一致,而这种不一致可能会产生非常不愉快的影响。想想我们从处理字符串中遇到的所有安全漏洞,其中有些将\0视为终止符,而有些则不是。
我强烈建议避免使用CharacterIterator接口。

2
是的,CharacterIterator使用0xFFFF作为DONE值有点反常。但从高效处理文本的角度来看,这一切都是有意义的。
String类不禁止0xFFFF“非字符”和其他保留或未映射的Unicode代码点。这样做需要String构造函数检查每个提供的char值。它还会在处理包含在JVM之后版本的Unicode中定义的Unicode代码点的文本时产生问题。
另一方面,CharacterIterator接口旨在通过调用一个方法即可进行迭代。他们决定使用一个特殊的char值来表示“没有更多”,因为其他选择是:
抛出异常(太昂贵)或使用int作为返回类型,这会使调用者的生活更加复杂。
如果CharacterIterator用于“真正”的Unicode文本,则不能包括0xFFFF不是问题。有效的Unicode文本不包含此代码点。 (实际上,将0xFFFF保留为非字符的原因是支持将Unicode文本表示为以非字符值终止的字符串的应用程序。使用0xFFFF作为字符将完全破坏它。)
底线是:
如果您想要严格的Unicode字符串,则不要使用String,
如果要迭代包含0xFFFF值的Java字符串,则不要使用CharacterIterator。

未来分配的代码点不是问题,因为明确禁止交换的非字符代码点集是不变且固定的。 - Joey
@Joey - 实际上,PeterO的回答直接与你关于“不变和固定”的说法相矛盾。现在,非字符代码点已经正式允许进行交换。官方认可。 - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接