从 String.getBytes 理解 Java 编码和解码

背景

周末一直在想 String.getBytes 原理。查阅了一些资料,终于用代码验证了自己的想法。本篇不会涉及太多源码相关的知识。

知识点

  • Unicode 和 UTF-8 的关系
  • 原码,反码,补码
  • Java 字符串和 Unicode 的关系
  • 理解 String.getBytes,解决乱码问题
  • Demo 验证猜想

用到的工具

进制之间的相互转换

Unicode 编码表

GBK 编码表

Unicode 官网

原码,补码,反码

因为原码,补码,反码比较简单,我这里粘贴一个例子进行展示。

图片内容来源:www.cnblogs.com/yinzhengjie…

Unicode 和 UTF-8 的关系

Uincode 是一个字符集。它规定了我们使用到的字或符号的码点(code point)。码点使用 16 进制保存。

Uincode 字符集规定 一 的码点为 4E00。

Uincode 字符集规定 丁 的码点为 4E01。

计算机呢只能识别二进制的 0 和 1。而 UTF-8 指的是编码规则,规定码点怎么保存成二进制。

还有别的 Unicode 编码规则,UTF-16 和 UTF-32。

十进制 Unicode符号范围(十六进制) UTF-8编码方式(二进制)
0-127 0000 0000-0000 007F 0xxxxxxx
128-2047 0000 0080-0000 07FF 110xxxxx 10xxxxxx
2048-65535 0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
65536-1114111 0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

上述表格简单描述了Unicode 按 UTF-8 编码的格式。

  • 首先将 16 进制的码点,通过进制转换 为十进制
  • 然后使用十进制的数字查找上述表格处于哪个范围中,得出编码规则。
  • 然后将码点转换为 2 进制,从低位到高位替换 x 即可得到字二进制的原码
  • 将二进制的原码转换为补码存储。

java 内存中的字符串采用的是 unicode 编码,也就是内编码。我们可以从 unicode 转变为 GBK 或 UTF-8 等其它规则。

代码验证猜想

以赵为例子讲解。

赵的码点为:8D75

16 进制的码点转换为 10 进制:36213

36213 处于 2048-65535 ,得出对应的 UTF-8 编码格式为:1110xxxx 10xxxxxx 10xxxxxx

赵的 16 进制码点 8D75 转换为二进制 1000

将二进制填充在 1110xxxx 10xxxxxx 10xxxxxx 中的 x 中,不足的补 0.

11101000 10110101 10110101。

对三个字节分别求补码为:

原码:11101000 10110101 10110101

补码:10011000 11001011 11001011

补码对应 java 中的字节数组为:{-24,-75,-75}

@Test
public void run454() throws UnsupportedEncodingException {
    String str ="赵";
    final byte[] bytes = str.getBytes("UTF-8");
    StringBuilder stringBuilder =new StringBuilder();

    for (byte aByte : bytes) {
        stringBuilder.append(aByte).append(",");
    }

    System.out.println(stringBuilder.toString());
}
复制代码
  • 再加一个例子:

且的码点:4E14

16 进制的码点转换为 10 进制:19988

19988 处于 2048-65535 ,得出对应的 UTF-8 编码格式为:1110xxxx 10xxxxxx 10xxxxxx

16 进制的码点转换成二进制:100111000010100

原码:11100100 10111000 10010100

补码:10011100 11001000 11101100

补码对应的字节数组为:{-28,-72,-108}

@Test
public void run43() throws UnsupportedEncodingException {

    // {-28,-72,-108}
    String str ="且";
    final byte[] bytes = str.getBytes("UTF-8");
    StringBuilder stringBuilder =new StringBuilder();

    for (byte aByte : bytes) {
        stringBuilder.append(aByte).append(",");
    }

    System.out.println(stringBuilder.toString());
}
复制代码

GBK 转码

赵的 GBK 码点为:D5D4 十六进制码点转换为二进制:11010101 11010100 源码:11010101 11010100 补码:10101011 10101100

补码对应的字节数组为:{-43,-44}

@Test
public void run454() throws UnsupportedEncodingException {
    String str ="赵";
    final byte[] bytes = str.getBytes("GBK");
    StringBuilder stringBuilder =new StringBuilder();

    for (byte aByte : bytes) {
        stringBuilder.append(aByte).append(",");
    }
    // -43,-44
    System.out.println(stringBuilder.toString());
}
复制代码

JAVA 中乱码问题

java 字符或字符串采用 uincode 作为内编码。

@Test
public void run44() {
    String str="\u0c2c";
//        బ
    System.out.println(str);
//      ✈
    System.out.println("\u2708");
}
复制代码

编码:字符串到字节。

解码:字节到字符串。

当我们读取文件的时候实际读取的是字节。然后根据文件的编码格式,将字节解码成字符串。乱码问题容易出现的地方就是这里。

不要妄想将一个乱码的字符串变成一个非乱码的。这个思路是错误的。应该从乱码之前的字节着手处理。

@Test
public void run100() throws UnsupportedEncodingException {
    String str = "张";
    final byte[] gbks = str.getBytes("GBK");
    final String s = new String(gbks, "UTF-8");
    System.out.println(s);
}
复制代码

上述例子中的 s 已经乱码了,当你操作这个 s 获取字节也是乱码的。

因此思路是操作 gbks 转换解码方式获取字符串。

参考资料

阮一峰字符编码笔记

猜你喜欢

转载自juejin.im/post/5dad9394e51d4531a7423b3a