【问题标题】:Character encoding confusion on WindowsWindows 上的字符编码混淆
【发布时间】:2021-06-21 17:18:17
【问题描述】:

我有一个简单的 Java 程序,它接收十六进制并将其转换为 ASCII。 使用 Java 8,我编译了以下内容:

import java.nio.charset.Charset;
import java.util.Scanner;

public class Main 
{
    public static void main(String[] args) 
    {
        System.out.println("Charset: " + Charset.defaultCharset());
        Scanner in = new Scanner(System.in);
        System.out.print("Type a HEX string: ");
        String s = in.nextLine();
        String asciiStr = new String();
        
        //  Split the string into an array
        String[] hexes = s.split(":");
        
        //  For each hex
        for (String hex : hexes) {
            //  Translate the hex to ASCII
            System.out.print(" " + Integer.parseInt(hex, 16) + "|" + (char)Integer.parseInt(hex, 16));
            asciiStr += ((char) Integer.parseInt(hex, 16));
        }
        
        System.out.println("\nthe ASCII string is " + asciiStr);
        
        in.close();
    }
}

我将C0:A8:96:FE 的十六进制字符串传递给程序。我主要关心的是0x96 值,因为它被定义为控制字符(128 - 159 范围内的字符)。

当我在没有任何 JVM 标志的情况下运行程序时的输出如下:

Charset: windows-1252
Type a HEX string: C0:A8:96:FE
 192|À 168|¨ 150|? 254|þ
the ASCII string is À¨?þ

当我使用 JVM 标志 -Dfile.encoding=ISO-8859-1 设置字符编码时的输出如下所示:

Charset: ISO-8859-1
Type a HEX string: C0:A8:96:FE
 192|À 168|¨ 150|– 254|þ
the ASCII string is À¨–þ

我想知道为什么当字符编码设置为 ISO-8859-1 时,我会得到字符 128 - 159 的额外 Windows-1252 字符?这些字符不应该在 ISO-8859-1 中定义,而应该在 Windows-1252 中定义,但在这里似乎是倒退的。在 ISO-8859-1 中,我认为 0x96 字符应该被编码为空白字符,但事实并非如此。相反,Windows-1252 编码会执行此操作,它应该正确地将其编码为。这里有什么帮助吗?

【问题讨论】:

    标签: java character-encoding iso-8859-1 windows-1252


    【解决方案1】:

    tl;博士

    我的猜测:虽然您的 JVM 的默认 Charset 可能是“windows-1252”,但您的 System.out 实际上使用的是 Unicode。

    你说:

    当我使用 JVM 标志 -Dfile.encoding=ISO-8859-1 来设置字符编码时

    我在下面的实验让我怀疑你所做的一切没有实际上影响System.out 使用的字符集。我相信在您的两次运行中,当您认为您的 System.out 使用“windows-1252”或“ISO-8859-1”时,您的 System.out 实际上使用的是 Unicode,可能是 UTF-8。

    我希望我知道how to get the Charset of System.out

    这种行为将来可能会改变,proposal (JEP 400) to use UTF-8 by default 跨平台。

    详情

    实际上,您问的是Unicode 而不是ASCII。 ASCII 只有 128 个字符。

    你说:

    我主要关心的是 0x96 值,因为它被定义为控制字符(128 - 159 范围内的字符)。

    实际上,控制字符范围从 Unicode(和 ASCII)的 127 开始,而不是 128。代码点 127 是 DELETE character。所以 127-159 是控制字符。

    首先,让我们拆分您输入的十六进制代码字符串。

            final List < String > hexInputs = List.of( "C0:A8:96:FE".split( ":" ) );
            System.out.println( "hexInputs = " + hexInputs );
    

    运行时。

    hexInputs = [C0, A8, 96, FE]
    

    现在将每个十六进制文本转换为十六进制整数。我们将该整数用作 Unicode code point

    与其依赖某些默认字符编码,不如明确设置System.outCharset。我不是这方面的专家,但是一些网络搜索发现了下面的代码,我们将System.out 包装在一个新的PrintStream 中,同时通过它的名称设置Charset。我找不到得到PrintStreamCharset 的方法,所以I asked

    UTF-8

            // UTF-8
            System.out.println( "----------|  UTF-8  |--------------------------" );
            try
            {
                PrintStream printStream = new PrintStream( System.out , true , StandardCharsets.UTF_8.name() ); // "UTF-8".
    
                for ( String hexInput : hexInputs )
                {
                    int codePoint = Integer.parseInt( hexInput , 16 );
                    String string = Character.toString( codePoint );
                    printStream.println( "hexInput: " + hexInput + " = codePoint: " + codePoint + " = string: [" + string + "] = isLetter: " + Character.isLetter( codePoint ) + " = name: " + Character.getName( codePoint ) );
                }
            }
            catch ( UnsupportedEncodingException e )
            {
                e.printStackTrace();
            }
    

    运行时。

    ----------|  UTF-8  |--------------------------
    hexInput: C0 = codePoint: 192 = string: [À] = isLetter: true = name: LATIN CAPITAL LETTER A WITH GRAVE
    hexInput: A8 = codePoint: 168 = string: [¨] = isLetter: false = name: DIAERESIS
    hexInput: 96 = codePoint: 150 = string: [] = isLetter: false = name: START OF GUARDED AREA
    hexInput: FE = codePoint: 254 = string: [þ] = isLetter: true = name: LATIN SMALL LETTER THORN
    

    Windows-1252

    接下来,我们做同样的事情,但将"windows-1252" 设置为我们包​​装的System.outCharset。在进行包装之前,我们验证这样的字符编码在我们当前的 JVM 上确实可用。

            // windows-1252
            System.out.println( "----------|  windows-1252  |--------------------------" );
    
            // Verify windows-1252 charset is available on the current JVM.
            String windows1252CharSetName = "windows-1252";
            boolean isWindows1252CharsetAvailable = Charset.availableCharsets().keySet().contains( windows1252CharSetName );
            if ( isWindows1252CharsetAvailable )
            {
                System.out.println( "isWindows1252CharsetAvailable = " + isWindows1252CharsetAvailable );
            } else
            {
                System.out.println( "FAIL - No charset available for name: " + windows1252CharSetName );
            }
    
            try
            {
                PrintStream printStream = new PrintStream( System.out , true , windows1252CharSetName );
    
                for ( String hexInput : hexInputs )
                {
                    int codePoint = Integer.parseInt( hexInput , 16 );
                    String string = Character.toString( codePoint );
                    printStream.println( "hexInput: " + hexInput + " = codePoint: " + codePoint + " = string: [" + string + "] = isLetter: " + Character.isLetter( codePoint ) + " = name: " + Character.getName( codePoint ) );
                }
            }
            catch ( UnsupportedEncodingException e )
            {
                e.printStackTrace();
            }
    

    运行时。

    ----------|  windows-1252  |--------------------------
    isWindows1252CharsetAvailable = true
    hexInput: C0 = codePoint: 192 = string: [�] = isLetter: true = name: LATIN CAPITAL LETTER A WITH GRAVE
    hexInput: A8 = codePoint: 168 = string: [�] = isLetter: false = name: DIAERESIS
    hexInput: 96 = codePoint: 150 = string: [?] = isLetter: false = name: START OF GUARDED AREA
    hexInput: FE = codePoint: 254 = string: [�] = isLetter: true = name: LATIN SMALL LETTER THORN
    

    拉丁语-1

    我们也可以尝试Latin-1,产生不同的结果。

            // ISO-8859-1
            System.out.println( "----------|  Latin-1  |--------------------------" );
    
            // Verify that  charset is available on the current JVM.
            String latin1CharsetName = "ISO-8859-1"; // Also known as "Latin-1".
            boolean isLatin1CharsetNameAvailable = Charset.availableCharsets().keySet().contains( latin1CharsetName );
            if ( isLatin1CharsetNameAvailable )
            {
                System.out.println( "isLatin1CharsetNameAvailable = " + isLatin1CharsetNameAvailable );
            } else
            {
                System.out.println( "FAIL - No charset available for name: " + latin1CharsetName );
            }
    
            try
            {
                PrintStream printStream = new PrintStream( System.out , true , latin1CharsetName );
    
                for ( String hexInput : hexInputs )
                {
                    int codePoint = Integer.parseInt( hexInput , 16 );
                    String string = Character.toString( codePoint );
                    printStream.println( "hexInput: " + hexInput + " = codePoint: " + codePoint + " = string: [" + string + "] = isLetter: " + Character.isLetter( codePoint ) + " = name: " + Character.getName( codePoint ) );
                }
            }
            catch ( UnsupportedEncodingException e )
            {
                e.printStackTrace();
            }
    

    运行时。

    ----------|  Latin-1  |--------------------------
    isLatin1CharsetNameAvailable = true
    hexInput: C0 = codePoint: 192 = string: [�] = isLetter: true = name: LATIN CAPITAL LETTER A WITH GRAVE
    hexInput: A8 = codePoint: 168 = string: [�] = isLetter: false = name: DIAERESIS
    hexInput: 96 = codePoint: 150 = string: [�] = isLetter: false = name: START OF GUARDED AREA
    hexInput: FE = codePoint: 254 = string: [�] = isLetter: true = name: LATIN SMALL LETTER THORN
    

    结论

    所以你可以看到,当硬编码我们包裹的System.outCharset 时,我们确实看到了不同。使用 UTF-8,我们得到实际字符 [À], [¨], [], [þ],而使用 windows-1252,我们得到三个时髦的问号字符和一个常规问号,[�], [�], [?], [�]。请记住,我们在代码中添加了方括号。

    我的代码的这种行为符合我的期望,显然也符合你的期望。这四个十六进制/十进制整数中有两个是 Unicode 中的字母,而它们都不是 Windows 1252 字符集和 Latin-1 中的字母。对我来说唯一神秘的是,十六进制 96 十进制 150 数字有两种不同的表示形式,一个是 UTF-8 的空格,一个是 windows-1252 的问号,然后是 Latin-1 下的一个时髦的问号。

    结论:您的System.out 没有使用您认为它正在使用的Charset。我怀疑虽然你的 JVM 的 JVM 的默认 Charset 可能被命名为“windows-1252”,但你的 System.out 实际上是 Unicode 字符集,可能使用 UTF-8 编码。


    读者注意:如果不熟悉字符集和字符编码,我推荐有趣且易于阅读的帖子The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)

    【讨论】:

    • 你的回答很有启发性。我认为 JVM 可能使用我指定的编码是对的,但 System.out 使用的是不同的编码。非常奇怪的行为。感谢您提供的信息,我现在对此更加了解。有趣的是,windows-1252 字符集用 150 字符代替了 ? ,几乎就像它不知道角色应该是什么一样。例如,我认为 Windows 会这样做(替换为?)而不是空白框。
    • 虽然,在 Windows-1252 字符编码中,我相信字符 150 应该是 em-dash,而不是控制字符。
    • @noc-tune 实际上,根据Wikipedia page on Windows-1252,十进制数字 150 是 En dash,而 151 是 Em dash。我无法解释为什么我在输出中得到? 而不是短划线。所以,尽管我回答了,我真的不知道发生了什么! (仅供参考,在Latin-1 150 中是“未定义的”。)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    • 1970-01-01
    • 2012-10-18
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    相关资源
    最近更新 更多