Java中的UTF-16转ASCII转换

4

我一直忽略它,但现在我正在努力学习Java中的Unicode。我需要完成一个关于将UTF-16字符串转换为8位ASCII的练习。请问有人能告诉我如何在Java中实现这个功能吗?我知道你不能用ASCII表示所有可能的Unicode值,所以在这种情况下,我希望超过0xFF的代码仍然可以添加(无效数据也应该默默地添加)。

谢谢!


“added away”?你是指“thrown away”吗?被丢弃了? - Stephen C
抱歉一开始没有表达清楚。实际上,我自己也不是很清楚。我读的书中的练习只是说:“超过0xFF的代码应该仅被转换为一个字节并被添加(坏数据应该被静默地添加)。” - His
0xFF 不是 ASCII 字符的有效值。ASCII 是 7 位编码,因此最大的有效值为 0x7F。 - Joachim Sauer
5个回答

13

您可以使用java.nio进行简单的解决方案:

// first encode the utf-16 string as a ByteBuffer
ByteBuffer bb = Charset.forName("utf-16").encode(CharBuffer.wrap(utf16str));
// then decode those bytes as US-ASCII
CharBuffer ascii = Charset.forName("US-ASCII").decode(bb);

9
这个怎么样:
String input = ... // my UTF-16 string
StringBuilder sb = new StringBuilder(input.length());
for (int i = 0; i < input.length(); i++) {
    char ch = input.charAt(i);
    if (ch <= 0xFF) {
        sb.append(ch);
    }
}

byte[] ascii = sb.toString().getBytes("ISO-8859-1"); // aka LATIN-1

对于大字符串来说,这可能不是最有效的转换方式,因为我们复制字符两次。然而,它有一个直接的优点。

顺便说一下,严格来说,没有8位ASCII字符集。ASCII是一个7位字符集。 LATIN-1是最接近“8位ASCII”字符集的东西(Unicode的块0相当于LATIN-1),所以我会假设这就是你的意思。

编辑:根据问题的更新,解决方案甚至更简单:

String input = ... // my UTF-16 string
byte[] ascii = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
    ascii[i] = (byte) input.charAt(i);
}

这种解决方案更加高效。由于我们现在知道要期望多少字节,因此我们可以预先分配字节数组,并在不使用StringBuilder作为中间缓冲区的情况下复制(截断的)字符。

然而,我并不认为以这种方式处理错误数据是明智的。

编辑2:还有一个更加隐晦的“陷阱”。Unicode实际上将代码点(字符)定义为“大约21位”值... 0x000000到0x10FFFF... 并使用代理表示代码> 0x00FFFF。换句话说,Unicode代码点> 0x00FFFF实际上在UTF-16中表示为两个“字符”。我的答案或其他任何答案都没有考虑到这一点(尽管有些奇特)。事实上,在Java中处理> 0x00FFFF的代码点通常相当棘手。这源于'char'是一个16位类型,而String是以'char'为基础定义的。

编辑3:也许更明智的处理无法转换为ASCII的意外字符的解决方案是用标准替换字符替换它们:

String input = ... // my UTF-16 string
byte[] ascii = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
    char ch = input.charAt(i);
    ascii[i] = (ch <= 0xFF) ? (byte) ch : (byte) '?';
}

鉴于上面的“编辑2”,我们能不能不将其标记为解决方案?这不是一个解决方案,因此不应该被标记为解决方案。 - rplankenhorn
@rplankenhorn - 实际上,由于问题实际上是关于将Unicode“强制”转换为ASCII,因此即使面对代理,两个版本的转换也是足够的解决方案。在第一个版本中,任何代码单元>=FF都将被删除。在第二个版本中,任何代码单元>=FF都将“被添加”,这正是OP明确要求的。(尽管我认为这不是一个明智的方法。) - Stephen C

3

Java在内部使用UTF-16表示字符串。如果您有String对象,可以使用String.getBytes(Charset c)进行编码,其中您可以指定US-ASCII(该字符集可以映射代码点0x00-0x7f)或ISO-8859-1(该字符集可以映射代码点0x00-0xff,并且可能是您所说的“8位ASCII”)。

至于添加“坏数据”... ASCII或ISO-8859-1字符串根本无法表示某个范围外的值。我认为getBytes会简单地删除不能在目标字符集中表示的字符。


我相信getBytes会简单地丢弃它无法在目标字符集中表示的字符。这取决于Charset的默认替换字节数组...根据Javadoc所述。 - Stephen C
我在Javadoc中也看到了这个,但是我找不到有关默认Charset对象实现的任何信息。你知道当你调用Charset.forName("US-ASCII")时实际上会发生什么吗? - Phil
唤醒这个老问题,但现在已经是2021年了,有了Windows WSL2,当我从Windows侧获取WSL挂载驱动器的路径时,在java.nio.Path中没有得到标准的“ASCII”文件字符串。基本上它是一个ASCII字符串,每隔一个字节设置为0。解决方案很简单(阅读了这篇文章之后),通过new String(s.getBytes(StandardCharsets.US_ASCII))将字符串带回我需要的方式。 - Manabu Tokunaga

2

由于这是一项练习,所以听起来您需要手动实现。您可以将编码(例如UTF-16或ASCII)视为查找表,将一系列字节与逻辑字符(代码点)匹配。

Java使用UTF-16字符串,这意味着任何给定的代码点都可以用一个或两个char变量表示。是否要处理两个char代理对取决于您认为应用程序遇到它们的可能性有多大(请参见Character类以检测它们)。ASCII仅使用一个八位字节的前7位,因此有效值范围为0到127。UTF-16对于此范围使用相同的值(只是更宽)。可以通过此代码进行确认:

Charset ascii = Charset.forName("US-ASCII");
byte[] buffer = new byte[1];
char[] cbuf = new char[1];
for (int i = 0; i <= 127; i++) {
  buffer[0] = (byte) i;
  cbuf[0] = (char) i;
  String decoded = new String(buffer, ascii);
  String utf16String = new String(cbuf);
  if (!utf16String.equals(decoded)) {
    throw new IllegalStateException();
  }
  System.out.print(utf16String);
}
System.out.println("\nOK");

因此,您可以通过将char转换为byte来将UTF-16转换为ASCII。
您可以在这里了解更多关于Java字符编码的信息。

0

为了优化已接受的答案并避免支付任何罚款,如果字符串已经全部是ASCII字符,则这里是优化版本。感谢@stephen-c

public static String toAscii(String input) {
  final int length = input.length();
  int ignoredChars = 0;
  byte[] ascii = null;
  for (int i = 0; i < length; i++) {
    char ch = input.charAt(i);
    if (ch > 0xFF) {
      //-- ignore this non-ascii character
      ignoredChars++;
      if (ascii == null) {
        //-- first non-ascii character. Create a new ascii array with all ascii characters
        ascii = new byte[input.length() - 1];  //-- we know, the length will be at less by at least 1
        for (int j = 0; j < i-1; j++) {
          ascii[j] = (byte) input.charAt(j);
        }
      }
    } else if (ascii != null) {
      ascii[i - ignoredChars] = (byte) ch;
    }
  }
  //-- (ignoredChars == 0) is the same as (ascii == null) i.e. no non-ascii characters found
  return ignoredChars == 0 ? input : new String(Arrays.copyOf(ascii, length - ignoredChars));
}

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