Java和控制台字符编码
#java #调试 #windows #unicode

所以我被我的好友Snoopy狙击了另一个daaaaay ...

他正在欧洲学习CS,并正在为作业编写一个程序,他必须在Windows上输入某些字符并处理它们。程序的相关部分非常简单。就是这样:

Scanner sc = new Scanner(System.in);
String input = sc.next();
for (int i = 0; i < input.length(); i++) {
    System.out.print(String.format("%02x", (int)input.charAt(i)));
    System.out.println();
}

因此,他运行它并进入一个非ANSI角色:Å(那是U+0161)。他给我的输出是:

>java PrintBytes
š
00

现在很奇怪。我很确定这不是一个无效的角色。我希望看到它的Unicode或UTF-8表示。这是我感到参与的不可控制的冲动。

默认的代码ePASS

我下载了JDK并在我的机器上尝试了。

>java PrintBytes
š
73

好吧,这很奇怪。哦,我的系统编码设置为Windows,而他的系统编码设置为UTF-8。我使用koude0将其更改为65001,即UTF-8,并获得了相同的奇数结果。

从文件重定向输入

下一个测试:如果我从文件中读取相同的输入怎么办?

>java PrintBytes < input.txt
c5
a1

嘿,这是正确的。那就是它的UTF-8表示。因此,与文件输入相比,Java从交互式命令行读取的方式很奇怪,即使两者都通过Stdin。

生锈怎么办?

下一个测试,让我们看看它在Rust中的作用。

use std::io::Read;
fn main() {
    for b in std::io::stdin().bytes() {
        let val = b.unwrap();
        match val {
            0xd => println!(""),
            0xa => (),
            _ => println!("{:#02x}", val),
        }
    }
}

输出很好:

>target\debug\printbytes.exe
š
0xc5
0xa1

所以Rust在交互作用上做正确的事情。 Rust Code实际上检查了STDIN当前是否是控制台句柄,并调用ReadConsoleW,否则调用ReadFile,该ReadFile处理常规文件I/O都可以。

Snoopy还尝试在Python编写同等程序,这也做对了。因此,Java似乎在某些条件下做错了什么...但是原因是什么?

找到答案

一个好的起点可能是检查Rust source。我的第一个猜测是,在某个地方,我在stdin句柄上看到了对koude2的电话,但是我看到Windows呼叫的最低级别是我不熟悉的功能,koude1

阅读文档,它引用了有关ANSI兼容性的内容:

ReadConsole从控制台的输入缓冲区读取键盘输入。它的行为类似于ReadFile函数,除了它可以在Unicode(宽字符)或ANSI模式中读取。

我找到了另一个链接,该链接给出了a good comparison between koude2 and koude5。它证实ReadConsoleA(ANSI版本)仅读取ANSI字符,但是ReadConsoleW可以读取Unicode字符。 Rust正在阅读Unicode字符(希望UTF-16,但我不确定),然后将它们内部转换为UTF-8,因为其字符串类型为entery utf-8。

确认C ++

最简单的确认方法是编写一个小C ++程序,直接通向来源。在不同的模式中,它可以尝试ReadFileReadConsoleW

uint16_t c;
if (argc == 1) {
    ReadFile(GetStdHandle(STD_INPUT_HANDLE), reinterpret_cast<uint8_t*>(&c), 1, nullptr, nullptr);
} else {
    DWORD numRead;
    ReadConsoleW(GetStdHandle(STD_INPUT_HANDLE), &c, 1, &numRead, nullptr);
}

printf("%04x\n", c);

首先,这里是ReadFile模式:

>printbytes_c.exe
š
0000

,然后是ReadConsoleW模式:

>printbytes_c.exe -c
š
0161

u+0161是字符的UTF-16编码,因此似乎显示了一些Unicode支持。有趣的是,ReadConsoleA表现出与ReadFile相同的行为。

结论

这种行为在窗户中有些不幸,但似乎有很好的记录。大多数语言似乎在处理此操作方面做得适当,但Java不是。我们甚至可以在调试器中看到它。我没有适当的符号,但至少堆栈的顶部似乎很清楚地解决了。

0:004> k
 # Child-SP          RetAddr               Call Site
00 00000016`03ffce28 00007fff`7157c7f4     KERNEL32!ReadFile
01 00000016`03ffce30 00007fff`7157bd76     java!handleRead+0x20
02 00000016`03ffce70 00007fff`71572641     java!JNI_OnLoad+0x196
03 00000016`03ffef00 00000171`9146a02e     java!Java_java_io_FileInputStream_readBytes+0x1d

所以Java ...做得更好。有一种方法可以正确处理Unicode Interactive Console输入。也许是...? Java专家可能会知道,但是我在互联网上找不到任何明显的搜索。但是这个问题是特定于Windows的,所以Windows ...为什么您要这样?总之,计算机不好。