V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
abcbuzhiming
V2EX  ›  Java

[请教]String 的 new String(Byte[]) 和 getByte() 转换的理解不能

  •  
  •   abcbuzhiming · 2023-01-16 20:55:05 +08:00 · 2124 次点击
    这是一个创建于 520 天前的主题,其中的信息可能已经有所发展或是发生改变。
    请看以下代码

    byte[] a1 = //某个 excel 文件读出来的字节数组,xls ,非 xlsx;
    System.out.println("a1:" + Base64.getEncoder().encodeToString(a1));
    String tmp = new String(a1);
    System.out.println("a2:" + Base64.getEncoder().encodeToString(tmp.getBytes()));


    这里面的要素是,a1 的数据里有很多肯定不是可见字符串。

    我认为,打印的两行的结果应该是相同的,然而现实是,它们不相同。连长度都发生变化了

    我知道字符串是存在编码问题的,但是我同时觉得,只要同一台电脑上,这个默认编码是相同的,因此不应该存在编码问题。

    同时我去查了 String 的底层实现,它的底层存储应该是 char ,这里我知道当我们 new String(a1), a1 肯定是被解析成了多个 char ,我原本以为,就算 a1 里有很多不能被识别为字符的字节数据,char 也应该可以以读字节的方式来处理。但现实好像不是这样的。

    请教这个转化过程中到底发生了什么样的魔法?导致一进一出数据就不一样了
    17 条回复    2023-01-29 15:47:41 +08:00
    thinkershare
        1
    thinkershare  
       2023-01-16 21:17:13 +08:00
    string.getBytes(): 使用平台的默认字符集将字符串编码为 byte[]
    new String(byte[]): 使用平台的默认字符集编码解析 byte[]数组为 Java 平台的当前 String 编码,着你存在一个码点转换问题,你的 byte[]种的组合可能是无效的。(这特和 JVM 的当前 String 实现有关,一般是 Unicode 16)
    thinkershare
        2
    thinkershare  
       2023-01-16 21:20:02 +08:00
    本质上说字符串 -> byte[] 天然就需要含有 Encoding 信息,因为同样的字符串按照不同编码规则的内存序列是并不相同的,在没有 Encoding 的情况下,byte[]是没法反向映射到正确的 string 的,这里本质上会做归一化和合法性检测。
    thinkershare
        3
    thinkershare  
       2023-01-16 21:21:02 +08:00
    另外, 尽量不要使用没有编码信息的这 2 个方法,因为 Windows 平台下的 Encoding 是个大坑。
    iOCZ
        4
    iOCZ  
       2023-01-16 21:23:03 +08:00
    直接比较 byte[]不就好了,excel 里读出来会不会是别的编码的数据
    abcbuzhiming
        5
    abcbuzhiming  
    OP
       2023-01-16 21:27:39 +08:00
    @thinkershare
    byte[]是没法反向映射到正确的 string 的。对,我也是这么猜测的,然后呢?不合法的数据是会被丢弃掉吗?还是会以数字的方式转换成其它字符?
    其实我试验过指定 Encoding 的情况,结果是一样的,两次结果不同,所以我就挺困惑,java 的 string 是怎么处置 byte[]中那些“无法被转换成可见字符”的数据的?


    @iOCZ 我对比了啊,你看两次用 base64 打印出来的 byte[]结果不一样
    urnoob
        6
    urnoob  
       2023-01-16 21:30:27 +08:00 via Android
    注意 jdk 版本对 string 实现有变化 从 char 数组到 byte 数组的变动
    thinkershare
        7
    thinkershare  
       2023-01-16 21:36:49 +08:00
    @abcbuzhiming 你搞错了,这里没有 ’无法被转换成可见字符’ 这个概念,很多字符本来就不可见,而是很多字符序列在指定的编码种是非法的(这和无法显示完全不是一个概念), 至于 Java 怎么处理,其实从每个版本都可能发生变化,但 Java SE 的文档由详细记录
    Constructs a new String by decoding the specified array of bytes using the platform's default charset. The length of the new String is a function of the charset, and hence may not be equal to the length of the byte array.
    The behavior of this constructor when the given bytes are not valid in the default charset is unspecified. The CharsetDecoder class should be used when more control over the decoding process is required.
    abcbuzhiming
        8
    abcbuzhiming  
    OP
       2023-01-16 21:37:28 +08:00
    @urnoob 查了一下这个变化是 java9 变的,然后我试验后发现没啥不同,java8 和 java11 的结果是一样的,我目前怀疑问题是出在 java 如何读取 byte[]到字符的这个过程,对于根本无法转换为可见字符的 byte[] 数据部分,不知道 java 是怎么操作的,原本我想的是,char 就是个双字节数据啊,那 java 每次读两个 byte 不就行了吗?又没有可变长问题。可为啥这样一进一出,数据就不一样了呢?
    thinkershare
        9
    thinkershare  
       2023-01-16 21:40:01 +08:00
    @abcbuzhiming 这是一个未定义行为,不要依赖它。因为规范没有规定应该怎么做,所以各种平台可能会完全按照不同的方式去处理。依赖于规范,不要依赖于实现,就是这个道理,我记得以前也有人问过此类问题。
    oldshensheep
        10
    oldshensheep  
       2023-01-16 23:05:33 +08:00 via Android
    > char 就是个双字节数据啊,那 java 每次读两个 byte 不就行了吗?
    ....
    从 byte 到字符串要先解码,解码和 char 几个字节没有关系。而解码的话会有部分无效数据(即不能解码的数据)便会丢失一部分数据。实际上是未定义行为。( The behavior of this constructor when the given bytes are not valid in the default charset is unspecified )

    比如如果是 ASCII 编码,ASCII 只能存在 0-127 的数据
    如果 byte 中有负值就不能正常解析。

    具体怎么解析的看源码就知道了。
    MineDog
        11
    MineDog  
       2023-01-17 10:27:28 +08:00
    首先最后 base64 内部结果 byte[]->String 过程都一样,可以先不管。
    剩下的就是 a1 和 new String(a1).getBytes()的区别,说白了就是编码的区别,a1 就是 byte[],没有转换成 string ,所以没有变化。
    很明显发生变化的就是 new String(a1).getBytes()。你换成 new String(a1, StandardCharsets.ISO_8859_1).getBytes()应该就是相同结果了。原因就是上面你提到的,GBK 或者 utf8 编码不是一个字节,编码过程中有些非法的值会被舍弃
    MineDog
        12
    MineDog  
       2023-01-17 10:55:55 +08:00
    如果是 GBK ,还是有问题,换成 new String(a1, StandardCharsets.ISO_8859_1).getBytes(StandardCharsets.ISO_8859_1)应该就可以了。String.getBytes()方法也会取默认编码之前没注意到 -_-!
    nieyuanhong
        13
    nieyuanhong  
       2023-01-17 14:17:11 +08:00
    这个直接跟踪源码就理解了,比如 windows 下默认 gbk ,其他系统可能默认 utf-8 ,这些字符集都有一个重解析的问题。
    具体来说就是解析时,有些 byte 组合是没有对应的字符的,而有些 byte 组合直接不符合编码标准,所以这些组合会被解析后映射到乱码字符。
    这样经过转换为字符又转回数组,长度和值当然会不一样了。
    要想保证这样两次解析不破坏信息,建议的方式是指定字符集解析为 iso8859-1 ,而为什么要选它呢?因为它仅有的 256 个字符和 byte 的 256 个值是严格一一对应的,这样无论如何互转,都不会产生映射混乱。

    楼主可以自己试一试。

    ```java
    var a1 = new byte[64];
    new Random().nextBytes(a1); // 获取一些随机 byte 以供测试
    System.out.println("a1:" + Base64.getEncoder().encodeToString(a1));
    var tmp = new String(a1, StandardCharsets.ISO_8859_1);
    System.out.println("a2:" + Base64.getEncoder().encodeToString(tmp.getBytes(StandardCharsets.ISO_8859_1)));

    ```
    ql562482472
        14
    ql562482472  
       2023-01-17 15:30:31 +08:00
    String 和 byte 的双向转换,每次都必须要指定 Charset ,否则会使用 Charset.defaultCharset()

    就会出现你的问题
    Aresxue
        17
    Aresxue  
       2023-01-29 15:47:41 +08:00
    指定编码格式为单字节编码,如"ISO-8859-1"
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5490 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:46 · PVG 10:46 · LAX 19:46 · JFK 22:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.