在Java中对UTF-16字符串进行字符排序。

16

TLDR

Java使用两个字符来表示UTF-16。使用Arrays.sort(不稳定排序)会影响字符排序。我应该将char[]转换为int[]还是有更好的方法?

Details

Java将字符表示为UTF-16。但是Character类本身包装了char(16位)。对于UTF-16,它将是两个char(32位)的数组。

使用内置排序算法对一个由UTF-16字符组成的字符串进行排序会影响数据。(Arrays.sort使用双轴快速排序,而Collections.sort使用Arrays.sort来完成繁重的工作。)

具体来说,您是否应该将char[]转换为int[]或者有更好的排序方法?

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] utfCodes = {128513, 128531, 128557};
        String emojis = new String(utfCodes, 0, 3);
        System.out.println("Initial String: " + emojis);

        char[] chars = emojis.toCharArray();
        Arrays.sort(chars);
        System.out.println("Sorted String: " + new String(chars));
    }
}

输出:

Initial String: 
Sorted String: ????

2
这就是我们所说的“排序规则”。你应该使用一个库来处理它,因为有很多排序规则可供选择。 - Guillaume F.
我认为“不稳定排序”不是在这里使用的正确术语:https://dev59.com/lHI_5IYBdhLWcg3wHvQ5 - Artur Biesiadowski
2
你把Unicode和UTF-16混淆了。Java中的char 就是一个UTF-16单元。猜猜为什么它被称为“UTF-16”,以及它与char有16位这一事实有什么关系。你可能需要两个UTF-16单元来编码一个单一的代码点,但这并不是Java的char的错。 - Holger
3个回答

12

我搜索了一下,没有发现任何不使用库的干净方法来按两个元素的分组对数组进行排序。

幸运的是,在此示例中创建String所使用的codePoints就是String本身,因此您可以简单地对它们进行排序,并使用结果创建一个新的String

public static void main(String[] args) {
    int[] utfCodes = {128531, 128557, 128513};
    String emojis = new String(utfCodes, 0, 3);
    System.out.println("Initial String: " + emojis);

    int[] codePoints = emojis.codePoints().sorted().toArray();
    System.out.println("Sorted String: " + new String(codePoints, 0, 3));
}

初始字符串:

排序后的字符串:

我调换了你的示例中字符的顺序,因为它们已经排好序了。


1
哈哈..我的字符串已经排序了...我没发现因为我不能排序(双关语)。我应该转到Java8 =) - dingy
4
@dingy,Java 8已经停止维护了。你必须升级到Java 12。 - Boris the Spider
3
自Java 5以来,Codepoint支持就已经存在。只有Stream API需要Java 8或更新版本才能使其看起来几乎是一行代码。 - Holger

6

如果您正在使用Java 8或更高版本,则这是一种简单的方法,可以在保持多字符代码点不被破坏的情况下对字符串中的字符进行排序:

int[] codepoints = someString.codePoints().sort().toArray();
String sorted = new String(codepoints, 0, codepoints.length);

在 Java 8 之前,我认为您需要使用循环来迭代原始字符串中的代码点,或使用第三方库方法。


幸运的是,对字符串中的代码点进行排序并不常见,因此上述解决方案的笨拙和相对低效很少受到关注。

(您上次测试表情符号的变位词是什么时候?)


谢谢回复。我正在查看Java 7的文档,我应该转移到Java 8。顺便说一句,我来自中国,正在制作一个应用程序,在其中需要对中文字符串进行排序,开玩笑的,但这是一个有效的用例。我在尝试理解Java如何使用UTF-16时偶然发现了它。由于其他答案都相同,我将选择最早到达的答案。再次感谢! - dingy
我并没有说无效,我说的是不常见。(而且你不得不编造一个用例只是加强了我的观点... :-)) - Stephen C
参见:https://chinese.stackexchange.com/questions/24053/chinese-anagrams。(第一个回答:“你为什么需要那个?我们在中国从不使用它。”) - Stephen C
4
雪上加霜的是,单个表情符号可能由多个代码点组成。例如,‍♀️ 由五个代码点(七个char)组成。但即使是拉丁字符也可能由多个代码点组成。 - Holger

4
由于Java的Unicode字符处理存在问题,我们不能使用char类型来表示Unicode编码。在Java早期,Unicode代码点始终为16位(恰好为一个char的固定大小)。然而,Unicode规范改变了,允许使用补充字符,这意味着Unicode字符现在具有可变宽度,并且可能比一个char更长。不幸的是,Java的char实现无法改变,否则将破坏大量已生产的代码。因此,最好的处理Unicode字符的方式是直接使用代码点,例如,在JDK 1.8及以上版本上使用String.codePointAt(index) 或String.codePoints() 流。附加来源:Unicode 1.0标准第2章(第10页和22页)和Java平台中的补充字符(Sun/Oracle)。

谢谢回复,我完全错过了String :: codePointAt api,我认为我应该转到Java 8。由于其他答案相同,我将选择最早的答案。 - dingy
1
如果您计划升级JDK版本,请考虑跳过Java 8并直接选择(Open)JDK 11 LTS,其中包含一些额外的亮点 - peekay
即使在那个变化之前,已经有了组合字符,这使得单个代码点代表整个字符的假设无效。 - Holger
@MichaWiedenmann 这不正确。在 Unicode 1.x 中,一个代码点始终是 16 位,并映射到一个 Unicode 字符。请参阅 Unicode 1.0 规范。从标准中可以看出:“Unicode 代码点是 16 位数量。”(第 22 页)和“所有 Unicode 字符的宽度均为 16 位。”(第 10 页)。大于 16 位的代码点(_补充字符_)最初是在 Unicode 3.1 中分配的。Java 直到 JDK 5.0(2004 年 9 月)才支持它们。参考链接 - peekay
谢谢你的澄清!我建议你把评论的一部分移到帖子中,这样我们可以清理这里的评论。 - Micha Wiedenmann
显示剩余2条评论

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