自己实现UTF-8编码解码

特别鸣谢:最近达内的刘苍松老师给我发了一段自己实现UTF-8编码的代码,我查阅了相关资料,并获得老师同意,特此记录一下
1. Unicode
  1. 这是一种所有符号的编码。将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,让乱码问题消失了。
  2. Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储
  3. 但是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,每个英文字母前都必然有二到三个字节是0,对于存储是极大的浪费,文本文件大小会大出二三倍
  4. 因此,出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode
  5. Unicode 在很长一段时间内无法推广,直到互联网的出现
  6. 而最大的问题, Unicode 的多种存储方式,让编码极度不统一,一个用第一种存储方式存储的文本,到其它人电脑上,很可能因为存储方式不同,出现乱码
2. UTF-8编码
  1. 随着互联网的普及,强烈要求出现一种统一的编码方式(各国家共同遵守)。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式,当然还有UTF-16和UTF-32
  2. UTF-8 就表示 字符最小需要用一个字节来表示,最大4个字节,UTF-16最小需要用2个字节来表示,UTF-32需要4个字节来表示
  3. UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度
3. UTF-8 的编码规则很简单
  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的
  2. 对于n字节(2、3、4)字节的符号,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码
    在这里插入图片描述
  1. 假设编码2字节符号,第一个字节,是几个字节就几个1开头,然后跟一个0,然后加上Unicode码。剩下的字节一律10开头,然后跟上Unicode码
  2. 编码后就是这样110xxxxx 10xxxxxx,x表示字符Unicode码
  1. 有人可能不理解,如果一个字符表示需要两个字节,为什么还能110开头,不会占用字符位数么?
  2. 其实仔细看上图Unicode符号编码范围,两个字节的编码只有0x07FF,也就是0b10000000 00000000 ~ 0b00000111 11111111,0b00000111 11111111前面正好五个0,正好可以填充110xxxxx 10xxxxxx,110+10正好5位
java中编码:以下内容都是从老师源码看来的
1. java如何存储字符
  1. Java中字符存储,是一个符号的Unicode编码
  2. 可以显示为10进制或16进制形式
  3. Java的字符范围 0 ~ 65536 (0xFFFF)
public class Test{
    public static void main(String[] args) {
        /*
         * Unicode 编码
         * - Java 中的字符存储的是一个符号的Unicode编码
         * - 可以显示为 10进制或16进制形式
         * - Java的字符范围 0 ~ 65535(FFFF)
         */
        char c1 = 'A';  // 41     65
        char c2 = '中'; // 4e2d   20013
        char c3 = '듏'; // U+B4CF 46287
        char c4 = 'α'; // U+03B1 945
        System.out.println((int)c1);//'A'十进制65,强转,字符转成Unicode码,英文和ASCII编码一样
        System.out.println(Integer.toHexString(c1));//A十六进制41
        System.out.println((int)c2);//'中'十进制20013
        System.out.println(Integer.toHexString(c2));//'中'十六进制4e2d
        System.out.println((int)c3);//'듏'十进制46287
        System.out.println(Integer.toHexString(c3));//듏'十六进制b4cf
        System.out.println((int)c4);//'α'十进制945
        System.out.println(Integer.toHexString(c4));//'α'十六进制3b1

    }
}
2. 如何将Unicode编码为UTF-8
  1. 要编码字符c,若<= 0x7F,表示单字节字符,就是ASCII,直接转即可
  2. c <= 0x7FF,处理两个字节的UTF-8编码处理成110xxxxx 10xxxxxx,只需要取出c字符的Unicode编码然后加前缀即可
  1. 将两个字节分开处理,b1为110xxxxx,b2为10xxxxxx
  2. b2截取字符的后6位,c & 00111111 可以拿到c的后6位,然后 | 10000000 正好填充10在前面
  3. b1截取剩下的5位,(c >>> 6)先把后6为右移,方便我们运算剩下的5位,然后 & 00011111,拿到这5位,最后 | 11000000,正好填充110在前面
  1. 3个字节,4个字节情况同理
3. 如何将UTF-8反编码为Unicode
  1. 如果编码c,我们定义为b1,右移7位,如果结果为0(b1 >>> 7) == 0,表示是单字节,直接强转
  2. 如果(b1 >>> 5) == 0b110,表示两个字节,我们需要把它前一个字节也拿出来b2,这两个字节加起来才是一个字符(b1+b2),b2 & 0b111111,将10前缀去掉,b1&0b11111,将110前缀去掉,然后左位移6位,给b2腾地方,然后哥俩拼接b1|b2
  3. 其它的同理
代码

import java.util.Arrays;

public class Test{
    /*
     * 手工编写 UTF-8 编码
   Char. number range  |        UTF-8 octet sequence
      (hexadecimal)    |              (binary)
   --------------------+---------------------------------------------
   0000 0000-0000 007F | 0xxxxxxx
   0000 0080-0000 07FF | 110xxxxx 10xxxxxx
   0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
     */
    /**
     * 将一个字符串编码为 UTF-8 字节数组
     * @param str 被编码的字符串
     * @return 经过UTF-8编码以后字节数组
     */
    public static byte[] getBytes(String str){
        //预估一下返回值的最大情况,一个字符最大4字节
        byte[] bytes = new byte[str.length() * 4];
        //index 代表 bytes 数组中数据的存储位置
        int index = 0;
        //遍历字符串中每个字符,根据字符的Unicode编码范围,进行UTF-8编码
        //将编存储到 bytes,bytes中就是返回值UTF-8数据
        // str = Java
        for(int i=0; i<str.length(); i++){
            char c = str.charAt(i);
            //判断c范围,根据范围进行编码
            if(c <= 0x7F){//0b0000-0000 ~ 0b0111-1111
                // c 在 0 ~ 0x7F 范围内,是1字节编码,1字节编码添加到bytes
                bytes[index++] = (byte)c;//一字节编码就是ASCII,直接转即可
            }else if(c <= 0x7FF){//0b10000000 00000000 ~ 0b00000111 11111111
                // c 在 0x80 ~ 0x7FF 范围,处理两个字节的UTF-8编码
                //  b1         b2    将这两个字符
                // 110xxxxx 10xxxxxx 处理成UTF-8编码格式
                // b2截取字符的后6位
                // c & 00111111 可以拿到c的后6位,然后 | 10000000 正好填充10在前面
                //00000011 10111011  c
                //00000000 00111111  0x3f
                //00000000 00111011  c & 0x3f
                //         10000000
                //         10111011  c & 0x3f | 10000000
                int b2 = (c & 0x3f) | 0b10000000; //0b0011-1111
                // b1截取剩下的5位
                // (c >>> 6)先把后6为右移,方便我们运算剩下的5位,然后 & 00011111,拿到这5位,最后 | 11000000,正好填充110在前面
                //00000011 10111011  c
                //00000000 00001110  c >>> 6  无符号右位移6位
                //00000000 00011111  0x1f
                //00000000 00001110  (c >>> 6) & 0x1f
                //         11000000
                //         11001110  (c >>> 6) & 0x1f | 11000000
                int b1 = ((c >>> 6) & 0x1f) | 0b11000000; //0b11111
                bytes[index++] = (byte)b1;
                bytes[index++] = (byte)b2;
            }else if (c<0xffff){
                //处理3字节编码
                //1110xxxx 10xxxxxx 10xxxxxx
                int b3 = (c & 0b111111) | 0b10000000;
                int b2 = ((c >>> 6) & 0b111111) | 0b10000000;
                int b1 = ((c >>> 12) & 0b1111) | 0b11100000;
                bytes[index++] = (byte) b1;
                bytes[index++] = (byte) b2;
                bytes[index++] = (byte) b3;
            }
        }
        return Arrays.copyOf(bytes, index);
    }

    /**
     * 将UTF-8编码的字节数组解码为字符串(Unicode字符)
     * @param bytes UTF-8 编码的字节
     * @return 解码以后的字符串
     */
    public static String decode(byte[] bytes){
        char[] chs = new char[bytes.length];
        int index = 0;
        //遍历 字节 数组,检查每个字节:
        // 如果字节以0开头 则是单字节编码 0xxxxxxx
        // 如果是以 110 为开头 则是双字节编码 110xxxxx
        // 如果是 1110 开头则是3字节编码
        for (int i = 0; i < bytes.length ; ){
            int b1 = bytes[i++] & 0xff;
            if((b1 >>> 7) == 0){
                //检查 01001010 是否为 单字节编码 0xxxxxxx
                // b1       00000000 00000000 00000000 01001010
                // b1>>>7   000000000000000 00000000 00000000 0
                chs[index++] = (char) b1;
            }else if((b1 >>> 5) == 0b110){
                //检查是否为双字节编码 b1 11001110 b2 10111011
                // b1 -> int
                // b1      00000000 00000000 00000000 11001110
                // b1>>>5  0000000000000 00000000 00000000 110
                int b2 = bytes[i++] & 0xff;
                // b1      00000000 00000000 00000000 11001110
                // b2      00000000 00000000 00000000 10111011
                // c       00000000 00000000 0000001110 111011
                int c = ((b1 & 0b11111)<<6) | (b2 & 0b111111);
                chs[index++] = (char) c;
            }else if ((b1 >>> 4) == 0b1110){
                // 检查是否为3字节编码: 11101000 10100001 10101000
                int b2 = bytes[i++] & 0xff;
                int b3 = bytes[i++] & 0xff;
                int c = ((b1 & 0b1111)<<12) | ((b2 & 0b111111)<<6) | (b3 & 0b111111);
                //System.out.println("b1:" + Integer.toBinaryString(b1));
                //System.out.println("b2:" + Integer.toBinaryString(b2));
                //System.out.println("b3:" + Integer.toBinaryString(b3));
                //System.out.println("c:" + Integer.toBinaryString(c));
                chs[index++] = (char) c;
            }
        }

        return new String(chs, 0, index);
    }

    public static void main(String[] args) {
        String str = "Javaλ表达式";
        System.out.println("Unicode:");
        for(int i=0; i<str.length(); i++){
            char c = str.charAt(i);
            System.out.print(c);
            System.out.print(":");
            System.out.println(Integer.toBinaryString(c));
        }
        //调用手写UTF-8编码方法
        byte[] bytes = getBytes(str);
        for(byte b:bytes){
            System.out.println(Integer.toBinaryString(b & 0xff));
        }
        //检查手写的UTF-8解码运算
        String s = decode(bytes);
        System.out.println(s);
    }
}
java JDK提供的相关API
import java.nio.charset.StandardCharsets;

public class Utf8Demo {
    public static void main(String[] args) throws Exception {
        /*
         * 测试UTF-8编解码API
         */
        String str = "Javaλ表达式";
        //将字符串中的文字进行UTF-8编码
        // str.getBytes(StandardCharsets.UTF_8) 也可以写成 str.getBytes("UTF-8")
        // 经过getBytes方法的转换得到了 UTF-8 编码的字节数组
        byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
        // bytes 就可以利用网络进行传输

        // 将字节数组中的 UTF-8 编码的字符进行解码
        // new String(bytes, "UTF-8");
        String s = new String(bytes, StandardCharsets.UTF_8);
        System.out.println(s);

        /*
         * 输出字符串中每个字符的Unicode
         * String str = "Javaλ表达式";
         */
        System.out.println("Unicode:");
        for(int i=0; i<str.length(); i++){
            //i = 0 1 2 3 4 ...
            char c = str.charAt(i);
            System.out.print(c);
            System.out.print(":");
            System.out.println(Integer.toBinaryString(c));
        }
        System.out.println("UTF-8");
        for(byte b:bytes){
            System.out.println(Integer.toBinaryString(b & 0xff ));
        }
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

殷丿grd_志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值