Java 打印 unicode 故障
Java print unicode glitch
我目前正在编写一个程序来读取 java class 个文件。目前,我正在读取 class 文件的常量池(读取 here)并将其打印到控制台。但是当它被打印出来时,一些 unicode 似乎以这种方式弄乱了我的终端,它看起来像这样(以防万一,我正在阅读的 class 文件是从 Kotlin 编译的,我正在使用的终端是 IntelliJ IDEA 终端,尽管在使用常规 Ubuntu 终端时它似乎没有出现故障。):
我注意到的是一个奇怪的 Unicode 序列,我认为它可能是某种转义序列。
这是没有奇怪的 unicode 序列的整个输出:
{1=UTF8: (42)'deerangle/decompiler/main/DecompilerMainKt', 2=Class index: 1, 3=UTF8: (16)'java/lang/Object', 4=Class index: 3, 5=UTF8: (4)'main', 6=UTF8: (22)'([Ljava/lang/String;)V', 7=UTF8: (35)'Lorg/jetbrains/annotations/NotNull;', 8=UTF8: (4)'args', 9=String index: 8, 10=UTF8: (30)'kotlin/jvm/internal/Intrinsics', 11=Class index: 10, 12=UTF8: (23)'checkParameterIsNotNull', 13=UTF8: (39)'(Ljava/lang/Object;Ljava/lang/String;)V', 14=Method name index: 12; Type descriptor index: 13, 15=Bootstrap method attribute index: 11; NameType index: 14, 16=UTF8: (12)'java/io/File', 17=Class index: 16, 18=UTF8: (6)'<init>', 19=UTF8: (21)'(Ljava/lang/String;)V', 20=Method name index: 18; Type descriptor index: 19, 21=Bootstrap method attribute index: 17; NameType index: 20, 22=UTF8: (15)'getAbsolutePath', 23=UTF8: (20)'()Ljava/lang/String;', 24=Method name index: 22; Type descriptor index: 23, 25=Bootstrap method attribute index: 17; NameType index: 24, 26=UTF8: (16)'java/lang/System', 27=Class index: 26, 28=UTF8: (3)'out', 29=UTF8: (21)'Ljava/io/PrintStream;', 30=Method name index: 28; Type descriptor index: 29, 31=Bootstrap method attribute index: 27; NameType index: 30, 32=UTF8: (19)'java/io/PrintStream', 33=Class index: 32, 34=UTF8: (5)'print', 35=UTF8: (21)'(Ljava/lang/Object;)V', 36=Method name index: 34; Type descriptor index: 35, 37=Bootstrap method attribute index: 33; NameType index: 36, 38=UTF8: (19)'[Ljava/lang/String;', 39=Class index: 38, 40=UTF8: (17)'Lkotlin/Metadata;', 41=UTF8: (2)'mv', 42=Int: 1, 43=Int: 11, 44=UTF8: (2)'bv', 45=Int: 0, 46=Int: 2, 47=UTF8: (1)'k', 48=UTF8: (2)'d1', 49=UTF8: (58)'WEIRD_UNICODE_SEQUENCE', 50=UTF8: (2)'d2', 51=UTF8: (0)'', 52=UTF8: (10)'Decompiler', 53=UTF8: (17)'DecompilerMain.kt', 54=UTF8: (4)'Code', 55=UTF8: (18)'LocalVariableTable', 56=UTF8: (15)'LineNumberTable', 57=UTF8: (13)'StackMapTable', 58=UTF8: (36)'RuntimeInvisibleParameterAnnotations', 59=UTF8: (10)'SourceFile', 60=UTF8: (20)'SourceDebugExtension', 61=UTF8: (25)'RuntimeVisibleAnnotations'}
AccessFlags: {ACC_PUBLIC, ACC_FINAL, ACC_SUPER}
这是在 Sublime Text 中打开的 Unicode 序列:
我对这整件事的问题是:为什么这个 Unicode 破坏了 IntelliJ IDEA 中的控制台,这在 Kotlin-Class-Files 中很常见,以及如何删除所有这些 "escape sequences" 在打印之前从字符串中提取?
出于某些深不可测的原因,Sun Microsystems 在设计时 Java,他们决定使用非 UTF8 的编码对常量池中的字符串进行编码。它是一种自定义编码,仅供 java 编译器和类加载器使用。
雪上加霜的是,在 JVM 文档中,他们决定将其称为 UTF8。但它是 而不是 UTF8,他们选择的名称会引起很多不必要的混淆。所以,我在这里推测的是,您看到他们称它为 UTF8,所以您将其视为 真实 UTF8,结果您收到垃圾。
您将需要在 JVM 规范中查找 CONSTANT_Utf8_info
的描述,并编写一个根据规范对字符串进行解码的算法。
为方便起见,下面是我为此编写的一些代码:
public static char[] charsFromBytes( byte[] bytes )
{
int t = 0;
int end = bytes.length;
for( int s = 0; s < end; )
{
int b1 = bytes[s] & 0xff;
if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
s++;
else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
s += 2;
else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
s += 3;
t++;
}
char[] chars = new char[t];
t = 0;
for( int s = 0; s < end; )
{
int b1 = bytes[s++] & 0xff;
if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
chars[t++] = (char)b1;
else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
{
assert s < end : new IncompleteUtf8Exception( s );
int b2 = bytes[s++] & 0xff;
assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
chars[t++] = (char)(((b1 & 0x1f) << 6) | (b2 & 0x3f));
}
else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
{
assert s < end : new IncompleteUtf8Exception( s );
int b2 = bytes[s++] & 0xff;
assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
assert s < end : new IncompleteUtf8Exception( s );
int b3 = bytes[s++] & 0xff;
assert (b3 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
chars[t++] = (char)(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f));
}
else
assert false;
}
return chars;
}
Mike 的回答已经涵盖了 Java 类文件并不完全使用 UTF8 编码这一事实,但我想我会提供更多相关信息。
Java 类文件中使用的编码称为 Modified UTF-8(或 MUTF-8)。它在两个方面不同于常规的 UTF-8:
- 空字节使用双字节序列编码
- BMP 之外的代码点用 UTF16 中的代理对表示。对中的每个代码点依次使用常规 UTF8 编码以三个字节编码。
第一个变化是编码数据不包含原始空字节,这使得编写 C 代码时更容易处理。第二个变化是因为在 90 年代,UTF-16 风靡一时,UTF-8 最终会胜出还不清楚。事实上,Java 出于类似的原因使用 16 位字符。使用代理对对星体字符进行编码可以使 16 位世界中的事情更容易处理。请注意,大约在同一时间设计的 Javascript 与 UTF-16 字符串有类似的问题。
总之,编码和解码 MUTF-8 非常容易。这很烦人,因为它没有内置在任何地方。解码时,您以与 UTF-8 相同的方式进行解码,您只需要更加宽容,并且排除技术上无效的 UTF-8 序列(尽管使用相同的编码),然后根据需要替换代理项对。编码时,你做相反的事情。
请注意,这仅适用于 Java 字节码。 Java 中的程序员通常不必处理 MUTF-8,因为 Java 在其他地方混合使用 UTF-16 和真正的 UTF-8。
IntelliJ 的控制台很可能将字符串的某些字符解释为控制字符(与 Colorize console output in Intellij products 相比)。
很可能是 ANSI 终端仿真,您可以通过执行
轻松验证
System.out.println("Hello "
+ "[31mc[32mo[33ml[34mo[35mr[36me[37md"
+ " [30mtext");
如果您看到此文本使用不同颜色打印,则这是 ANSI 终端兼容解释。
但在打印来自未知来源的字符串时,删除控制字符始终是个好主意。 class 文件中的字符串常量不需要具有人类可读的内容。
一个简单的方法是
System.out.println(string.replaceAll("\p{IsControl}", "."));
这将在打印前用点替换所有控制字符。
如果您想获得有关实际 char 值的一些诊断,您可以使用,例如
System.out.println(Pattern.compile("\p{IsControl}").matcher(string)
.replaceAll(mr -> String.format("{%02X}", (int)string.charAt(mr.start()))));
这需要 Java 9,当然,同样的逻辑也可以用于更早的 Java 版本。它只需要更冗长的代码。
Pattern.compile("\p{IsControl}")
返回的Pattern
实例可以存储和重复使用。
我目前正在编写一个程序来读取 java class 个文件。目前,我正在读取 class 文件的常量池(读取 here)并将其打印到控制台。但是当它被打印出来时,一些 unicode 似乎以这种方式弄乱了我的终端,它看起来像这样(以防万一,我正在阅读的 class 文件是从 Kotlin 编译的,我正在使用的终端是 IntelliJ IDEA 终端,尽管在使用常规 Ubuntu 终端时它似乎没有出现故障。):
这是没有奇怪的 unicode 序列的整个输出:
{1=UTF8: (42)'deerangle/decompiler/main/DecompilerMainKt', 2=Class index: 1, 3=UTF8: (16)'java/lang/Object', 4=Class index: 3, 5=UTF8: (4)'main', 6=UTF8: (22)'([Ljava/lang/String;)V', 7=UTF8: (35)'Lorg/jetbrains/annotations/NotNull;', 8=UTF8: (4)'args', 9=String index: 8, 10=UTF8: (30)'kotlin/jvm/internal/Intrinsics', 11=Class index: 10, 12=UTF8: (23)'checkParameterIsNotNull', 13=UTF8: (39)'(Ljava/lang/Object;Ljava/lang/String;)V', 14=Method name index: 12; Type descriptor index: 13, 15=Bootstrap method attribute index: 11; NameType index: 14, 16=UTF8: (12)'java/io/File', 17=Class index: 16, 18=UTF8: (6)'<init>', 19=UTF8: (21)'(Ljava/lang/String;)V', 20=Method name index: 18; Type descriptor index: 19, 21=Bootstrap method attribute index: 17; NameType index: 20, 22=UTF8: (15)'getAbsolutePath', 23=UTF8: (20)'()Ljava/lang/String;', 24=Method name index: 22; Type descriptor index: 23, 25=Bootstrap method attribute index: 17; NameType index: 24, 26=UTF8: (16)'java/lang/System', 27=Class index: 26, 28=UTF8: (3)'out', 29=UTF8: (21)'Ljava/io/PrintStream;', 30=Method name index: 28; Type descriptor index: 29, 31=Bootstrap method attribute index: 27; NameType index: 30, 32=UTF8: (19)'java/io/PrintStream', 33=Class index: 32, 34=UTF8: (5)'print', 35=UTF8: (21)'(Ljava/lang/Object;)V', 36=Method name index: 34; Type descriptor index: 35, 37=Bootstrap method attribute index: 33; NameType index: 36, 38=UTF8: (19)'[Ljava/lang/String;', 39=Class index: 38, 40=UTF8: (17)'Lkotlin/Metadata;', 41=UTF8: (2)'mv', 42=Int: 1, 43=Int: 11, 44=UTF8: (2)'bv', 45=Int: 0, 46=Int: 2, 47=UTF8: (1)'k', 48=UTF8: (2)'d1', 49=UTF8: (58)'WEIRD_UNICODE_SEQUENCE', 50=UTF8: (2)'d2', 51=UTF8: (0)'', 52=UTF8: (10)'Decompiler', 53=UTF8: (17)'DecompilerMain.kt', 54=UTF8: (4)'Code', 55=UTF8: (18)'LocalVariableTable', 56=UTF8: (15)'LineNumberTable', 57=UTF8: (13)'StackMapTable', 58=UTF8: (36)'RuntimeInvisibleParameterAnnotations', 59=UTF8: (10)'SourceFile', 60=UTF8: (20)'SourceDebugExtension', 61=UTF8: (25)'RuntimeVisibleAnnotations'}
AccessFlags: {ACC_PUBLIC, ACC_FINAL, ACC_SUPER}
这是在 Sublime Text 中打开的 Unicode 序列:
我对这整件事的问题是:为什么这个 Unicode 破坏了 IntelliJ IDEA 中的控制台,这在 Kotlin-Class-Files 中很常见,以及如何删除所有这些 "escape sequences" 在打印之前从字符串中提取?
出于某些深不可测的原因,Sun Microsystems 在设计时 Java,他们决定使用非 UTF8 的编码对常量池中的字符串进行编码。它是一种自定义编码,仅供 java 编译器和类加载器使用。
雪上加霜的是,在 JVM 文档中,他们决定将其称为 UTF8。但它是 而不是 UTF8,他们选择的名称会引起很多不必要的混淆。所以,我在这里推测的是,您看到他们称它为 UTF8,所以您将其视为 真实 UTF8,结果您收到垃圾。
您将需要在 JVM 规范中查找 CONSTANT_Utf8_info
的描述,并编写一个根据规范对字符串进行解码的算法。
为方便起见,下面是我为此编写的一些代码:
public static char[] charsFromBytes( byte[] bytes )
{
int t = 0;
int end = bytes.length;
for( int s = 0; s < end; )
{
int b1 = bytes[s] & 0xff;
if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
s++;
else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
s += 2;
else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
s += 3;
t++;
}
char[] chars = new char[t];
t = 0;
for( int s = 0; s < end; )
{
int b1 = bytes[s++] & 0xff;
if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
chars[t++] = (char)b1;
else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
{
assert s < end : new IncompleteUtf8Exception( s );
int b2 = bytes[s++] & 0xff;
assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
chars[t++] = (char)(((b1 & 0x1f) << 6) | (b2 & 0x3f));
}
else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
{
assert s < end : new IncompleteUtf8Exception( s );
int b2 = bytes[s++] & 0xff;
assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
assert s < end : new IncompleteUtf8Exception( s );
int b3 = bytes[s++] & 0xff;
assert (b3 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
chars[t++] = (char)(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f));
}
else
assert false;
}
return chars;
}
Mike 的回答已经涵盖了 Java 类文件并不完全使用 UTF8 编码这一事实,但我想我会提供更多相关信息。
Java 类文件中使用的编码称为 Modified UTF-8(或 MUTF-8)。它在两个方面不同于常规的 UTF-8:
- 空字节使用双字节序列编码
- BMP 之外的代码点用 UTF16 中的代理对表示。对中的每个代码点依次使用常规 UTF8 编码以三个字节编码。
第一个变化是编码数据不包含原始空字节,这使得编写 C 代码时更容易处理。第二个变化是因为在 90 年代,UTF-16 风靡一时,UTF-8 最终会胜出还不清楚。事实上,Java 出于类似的原因使用 16 位字符。使用代理对对星体字符进行编码可以使 16 位世界中的事情更容易处理。请注意,大约在同一时间设计的 Javascript 与 UTF-16 字符串有类似的问题。
总之,编码和解码 MUTF-8 非常容易。这很烦人,因为它没有内置在任何地方。解码时,您以与 UTF-8 相同的方式进行解码,您只需要更加宽容,并且排除技术上无效的 UTF-8 序列(尽管使用相同的编码),然后根据需要替换代理项对。编码时,你做相反的事情。
请注意,这仅适用于 Java 字节码。 Java 中的程序员通常不必处理 MUTF-8,因为 Java 在其他地方混合使用 UTF-16 和真正的 UTF-8。
IntelliJ 的控制台很可能将字符串的某些字符解释为控制字符(与 Colorize console output in Intellij products 相比)。
很可能是 ANSI 终端仿真,您可以通过执行
轻松验证System.out.println("Hello "
+ "[31mc[32mo[33ml[34mo[35mr[36me[37md"
+ " [30mtext");
如果您看到此文本使用不同颜色打印,则这是 ANSI 终端兼容解释。
但在打印来自未知来源的字符串时,删除控制字符始终是个好主意。 class 文件中的字符串常量不需要具有人类可读的内容。
一个简单的方法是
System.out.println(string.replaceAll("\p{IsControl}", "."));
这将在打印前用点替换所有控制字符。
如果您想获得有关实际 char 值的一些诊断,您可以使用,例如
System.out.println(Pattern.compile("\p{IsControl}").matcher(string)
.replaceAll(mr -> String.format("{%02X}", (int)string.charAt(mr.start()))));
这需要 Java 9,当然,同样的逻辑也可以用于更早的 Java 版本。它只需要更冗长的代码。
Pattern.compile("\p{IsControl}")
返回的Pattern
实例可以存储和重复使用。