字符集&乱码 - java

常见字符集有:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,就需要进行字符编码,以便计算机能够识别和存储各种文字。编码有两大类:一类是非Unicode编码;另一类是Unicode编码。


非Unicode

ASCII

在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平低电平分别表示1和0),例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码),而大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码,统一规定了上述常用符号用哪些二进制数来表示。

ASCII (American Standard Code for Information Interchange):美国信息交换标准代码是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是最通用的信息交换标准,并等同于国际标准 ISO/IEC 646。ASCII第一次以规范标准的类型发表是在1967年,最后一次更新则是在1986年,到目前为止共定义了128个字符。

128个字符用7位刚好可以表示,计算机存储的最小单位是byte(字节),即8位(bit,比特),ASCII码中最高位设置为0,用剩下的7位表示字符。这7位可以看作数字0~127, ASCII码规定了从0~127的每个数字代表什么含义。

数字32~126表示的字符都是可打印字符,0~31和127表示一些不可以打印的字符,这些字符一般用于控制目的,这些字符中大部分都是不常用的

标准ASCii使用1个字节存储字符,首位是0,总共可表示128个字符(首位固定为0,2的7次方=128)。

英文字符:0xxxxxxx


GBK

ASCII码对美国是够用了,但对其他国家而言却是不够的,于是,各个国家的各种计算机厂商就发明了各种各种的编码方式以表示自己国家的字符(例如中国的GBK标准。注意中文第一个标准是GB2312),为了保持与ASCII码的兼容性,一般都是将最高位设置为1。也就是说,当最高位为0时,表示ASCII码,当为1时就是各个国家自己的字符。

GBK全称《汉字内码扩展规范》(GBK即“国标-GB”、“扩展-K”汉语拼音的第一个字母,英文名称:Chinese Internal Code Specification),中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订。

GBK中一个中文字符编码成两个字节的形式存储(汉字2字节,第一位固定为1,2的15次方=32768),包含了2万多个汉字等字符,兼容ASCII(用一个字节表示,依然是首位为0)。

汉字:1xxxxxxx xxxxxxxx

英文字符(ASCII):0xxxxxxx


小结

  • ASCII码是基础,使用一个字节表示,最高位设为0,其他7位表示128个字符。其他编码都是兼容ASCII的,最高位使用1来进行区分。

  • 西欧主要使用Windows-1252,使用一个字节,增加了额外128个字符。

  • 我国内地的三个主要编码GB2312、GBK、GB18030,香港特别行政区和我国台湾地区的主要编码是Big5。


Unicode

统一码(Unicode),也叫万国码、单一码,由统一码联盟开发。是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

Unicode 做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000~0x10FFFF,包括110多万。但大部分常用字符都在0x0000~0xFFFF之间,即65536个数字之内。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。

简单理解,Unicode主要做了这么一件事,就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示。编号怎么对应到二进制表示呢?有多种方案,主要有UTF-32、UTF-16和UTF-8。

各国都有自己的编码标准,过于混乱,最终国际组织提出了Unicode编码方案。

Unicode字符集,常见编码方案:UTF-8、UTF-16、UTF-32。

大部分中文的编号范围为U+4E00~U+9FFF,例如,“马”的Unicode是U+9A6C


UTF-8

UTF-8(8位元,Universal Character Set/Unicode Transformation Format)是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符,而且其编码中的第一个字节仍与ASCII相容,使得原来处理ASCII字符的软件无须或只进行少部分修改后,便可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。

UTF是“UCS Transformation Format”的缩写,可以翻译成统一码字符集转换格式,即怎样将统一码定义的数字转换成程序数据。

UTF-8编码方式(二进制)

  • 0xxxxxxx (ASCII码)
  • 110xxxxx 10xxxxxx
  • 1110xxxx 10xxxxxx 10xxxxxx
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • 二进制表示的每字节数据,除了ASCII中包含的字符是0开头,其他的都是1开头

UTF-8是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节。英文字符、数字等只占1个字节(兼容标准ASCI编码),汉字字符占用3个字节。

注意:技术人员在开发时都应该使用UTF-8编码!



小结

ASCii字符集:只有英文、数字、符号等,占1个字节。
GBK字符集:汉字占2个字节,英文、数字占1个字节。
UTF-8字符集:汉字占3个字节,英文、数字占1个字节。

注意1:字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
注意2:英文,数字一般不会乱码,因为很多字符集都兼容了ASCII编码。



java实现编/解码

Java代码完成对字符的编码

String提供了如下方法 说明
byte[] getBytes() 使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中
byte[] getBytes(String charsetName) 使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中

Java代码完成对字符的解码

String提供了如下方法 说明
String(byte[] bytes) 通过使用平台的默认字符集解码指定的字节数组来构造新的String
String(byte[] bytes, String charsetName) 通过指定的字符集解码指定的字节数组来构造新的 String

案例

扩展二进制使用最高位表示符号位,用1表示负数,用0表示正数。但哪个是最高位呢?整数有4种类型byte、short、int、long,分别占1、2、4、8个字节,即分别占8、16、32、64位,每种类型的符号位都是其最左边的一位。

@Test
void test01() throws UnsupportedEncodingException {
    
    
    //unicode编码,汉字占3个字节,英文1个字节

    String str = "我是qsdbl。";
    System.out.println("测试字符 = "+str);

    //1、使用 默认字符集 进行编码
    byte[] bytes = str.getBytes();
    System.out.println("\n- 使用 默认字符集 进行编码 -");
    //查看 默认字符集:
    String csn = Charset.defaultCharset().name();
    System.out.println("查看 默认字符集 = "+csn);
    System.out.println("编码后的数据(一个数组元素即一个字节。UTF-8字符集中,汉字占3个字节,数组长度应为14)= "+ Arrays.toString(bytes));
    System.out.println("数组长度实为 = "+bytes.length);
    System.out.println("二进制表示的每字节数据,除了ASCII中包含的字符是0开头,其他的都是1开头,体现在下边的正负符号:");
    byte[] item1 = Arrays.copyOfRange(bytes,11,bytes.length);
    ArrayList<String> item1list1 = new ArrayList<>();
    ArrayList<String> item1list2 = new ArrayList<>();
    for (int b : item1) {
    
    
        String b_binary = Integer.toBinaryString(b).substring(24);
        item1list1.add(b_binary);
        item1list2.add("【byte数据为:"+b+" (已进行补码运算,转换为十进制数),原始二进制数据为:"+b_binary+" 】");
        //二进制使用最高位表示符号位,用1表示负数,用0表示正数。
        //byte,占1个字节,即占8位,符号位是最左边的一位。
        //给定一个负数的二进制表示,要想知道它的十进制值,需采用补码运算。比如:11100110,首先取反,变为00011001,然后加1,结果为00011010,它的十进制值为 16+8+2 = 26,所以原值就是-26
    }
    System.out.println("汉字字符【我】对应"+Arrays.toString(Arrays.copyOfRange(bytes,0,3)));
    System.out.println("汉字字符【是】对应"+Arrays.toString(Arrays.copyOfRange(bytes,3,6)));
    System.out.println("英文字母【q】对应"+Arrays.toString(Arrays.copyOfRange(bytes,6,7)));
    System.out.println("英文字母【s】对应"+Arrays.toString(Arrays.copyOfRange(bytes,7,8)));
    System.out.println("英文字母【d】对应"+Arrays.toString(Arrays.copyOfRange(bytes,8,9)));
    System.out.println("英文字母【b】对应"+Arrays.toString(Arrays.copyOfRange(bytes,9,10)));
    System.out.println("英文字母【l】对应"+Arrays.toString(Arrays.copyOfRange(bytes,10,11)));
    System.out.println("中文符号【。】对应"+Arrays.toString(item1)+" -> "+item1list1);
    System.out.println(" -"+item1list2);
    //解码
    System.out.println("解码:");
    System.out.println("使用 UTF-8字符集 进行解码 = "+new String(bytes,"UTF-8"));
    System.out.println("使用 GBK字符集 进行解码 = "+new String(bytes,"GBK"));


    //2、使用 GBK字符集 进行编码
    byte[] bytes1 = str.getBytes("GBK");
    System.out.println("\n- 使用 GBK字符集 进行编码 -");
    System.out.println("编码后的数据(一个数组元素即一个字节。GBK字符集中,汉字占2个字节,数组长度应为11)= "+Arrays.toString(bytes1));
    System.out.println("数组长度实为 = "+bytes1.length);
    //解码
    System.out.println("使用 GBK字符集 进行解码 = "+new String(bytes1,"GBK"));
    System.out.println("使用 UTF-8字符集 进行解码 = "+new String(bytes1,"UTF-8"));
}

查看默认字符集:Charset.defaultCharset().name()

查看是否支持某种字符集:Charset.isSupported(“GBK”)

十进制转二进制:Integer.toBinaryString(10)

二进制转十进制:Integer.parseInt(“11100110”,2)

复制数组(索引,包括前不包括后):Arrays.copyOfRange(旧数组,起始索引,结束索引)


运行结果

测试字符 = 我是qsdbl。

- 使用 默认字符集 进行编码 -
查看 默认字符集 = UTF-8
编码后的数据(一个数组元素即一个字节。UTF-8字符集中,汉字占3个字节,数组长度应为14= [-26, -120, -111, -26, -104, -81, 113, 115, 100, 98, 108, -29, -128, -126]
数组长度实为 = 14
二进制表示的每字节数据,除了ASCII中包含的字符是0开头,其他的都是1开头,体现在下边的正负符号:
汉字字符【我】对应[-26, -120, -111]
汉字字符【是】对应[-26, -104, -81]
英文字母【q】对应[113]
英文字母【s】对应[115]
英文字母【d】对应[100]
英文字母【b】对应[98]
英文字母【l】对应[108]
中文符号【。】对应[-29, -128, -126] -> [11100011, 10000000, 10000010]
 -[byte数据为:-29 (已进行补码运算,转换为十进制数),原始二进制数据为:11100011,byte数据为:-128 (已进行补码运算,转换为十进制数),原始二进制数据为:10000000,byte数据为:-126 (已进行补码运算,转换为十进制数),原始二进制数据为:10000010]
解码:
使用 UTF-8字符集 进行解码 = 我是qsdbl。
使用 GBK字符集 进行解码 = 鎴戞槸qsdbl銆�

- 使用 GBK字符集 进行编码 -
编码后的数据(一个数组元素即一个字节。GBK字符集中,汉字占2个字节,数组长度应为11= [-50, -46, -54, -57, 113, 115, 100, 98, 108, -95, -93]
数组长度实为 = 11
使用 GBK字符集 进行解码 = 我是qsdbl。
使用 UTF-8字符集 进行解码 = ����qsdbl��

进程已结束,退出代码0


乱码问题


原因分析

乱码有两种常见原因

  • 解析错误。
    • 例如一个GB18030编码的文件被看作Windows-1252来解析显示就是乱码。这种情况下,之所以看起来是乱码,是因为看待或者说解析数据的方式错了。只要使用正确的编码方式进行解读就可以纠正了。
    • 切换查看编码的方式并没有改变数据的二进制本身,而只是改变了解析数据的方式,从而改变了数据看起来的样子。
  • 在错误解析的基础上进行了编码转换。
    • 如果怎么改变查看方式都不对,那很有可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。

使用Java恢复乱码

//假设造成此乱码的原因是“原来编码是【GBK】,被错误解读为【windows-1252】”,对乱码进行恢复:
String str = "ÄãºÃ";
byte[] bytes = str.getBytes("windows-1252");// 转换为 windows-1252(假设被错误解读为了windows-1252)
String newstr = new String(bytes,"GB18030");// 用 GB18030 解析(解码/显示)(假设这是原来的编码类型GB18030)
System.out.println(newstr);//你好

使用for循环分析:

/**
 * 恢复乱码
 * @param str 乱码字符
 */
public static void recover(String str) throws UnsupportedEncodingException {
    
    
    String[] charsetSort = {
    
    "windows-1252","GBK","UTF-8"};//编码类型
    int index = 0;
    for (int i = 0; i < charsetSort.length; i++) {
    
    
        for (int j = 0; j < charsetSort.length; j++) {
    
    
            if(i!=j){
    
    
                String newstr = new String(str.getBytes(charsetSort[i]), charsetSort[j]);
                System.out.println("\n情况"+(++index)+":");
                System.out.println("原来编码假设是【"+charsetSort[j]+"】,被错误解读为了【"+charsetSort[i]+"】");
                System.out.println("【"+str+"】 --> 【"+newstr+"】");
            }
        }
    }
}

运行结果:

//调用:
//恢复乱码
recover(str);

//运行结果:
情况1:
原来编码假设是【GBK,被错误解读为了【windows-1252】
【ÄãºÃ】 --> 【你好】

情况2:
原来编码假设是【UTF-8,被错误解读为了【windows-1252】
【ÄãºÃ】 --> 【���】

情况3:
原来编码假设是【windows-1252,被错误解读为了【GBK】
【ÄãºÃ】 -->????】

情况4:
原来编码假设是【UTF-8,被错误解读为了【GBK】
【ÄãºÃ】 -->????】

情况5:
原来编码假设是【windows-1252,被错误解读为了【UTF-8】
【ÄãºÃ】 --> 【ÄãºÃ】

情况6:
原来编码假设是【GBK,被错误解读为了【UTF-8】
【ÄãºÃ】 --> 【脛茫潞脙】

进程已结束,退出代码0

不难看出,造成此乱码的原因是“原来编码是【GBK】,被错误解读为【windows-1252】”。

不是所有的乱码形式都是可以恢复的,如果形式中有很多不能识别的字符(如?),则很难恢复。另外,如果乱码是由于进行了多次解析和转换错误造成的,也很难恢复。

使用java转换文件编码

使用java转换文件编码,见这篇博客:File&IO


扩展:Base64

概述

Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。

在Java 8中,Base64编码已经成为Java类库的标准。

Java 8 内置了 Base64 编码的编码器和解码器。

Base64工具类提供了一套静态方法获取下面三种BASE64编解码器:

  • 基本:输出被映射到一组字符A-Za-z0-9+/,编码不添加任何行标,输出的解码仅支持A-Za-z0-9+/。
  • URL:输出映射到一组字符A-Za-z0-9+_,输出是URL和文件。
  • MIME:输出隐射到MIME友好格式。输出每行不超过76字符,并且使用’\r’并跟随’\n’作为分割。编码输出最后没有行分割。

内嵌类和方法描述

内嵌类

序号 内嵌类 & 描述
1 static class Base64.Decoder该类实现一个解码器用于,使用 Base64 编码来解码字节数据。
2 static class Base64.Encoder该类实现一个编码器,使用 Base64 编码来编码字节数据

方法

基本编解码、URL编解码、MIME编解码(邮件使用),对应不同的应用场景。

序号 方法名 & 描述
1 static Base64.Decoder getDecoder()返回一个 Base64.Decoder ,解码使用基本型 base64 编码方案。
2 static Base64.Encoder getEncoder()返回一个 Base64.Encoder ,编码使用基本型 base64 编码方案。
3 static Base64.Decoder getMimeDecoder()返回一个 Base64.Decoder ,解码使用 MIME 型 base64 编码方案。
4 static Base64.Encoder getMimeEncoder()返回一个 Base64.Encoder ,编码使用 MIME 型 base64 编码方案。
5 static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator)返回一个 Base64.Encoder ,编码使用 MIME 型 base64 编码方案,可以通过参数指定每行的长度及行的分隔符。
6 static Base64.Decoder getUrlDecoder()返回一个 Base64.Decoder ,解码使用 URL 和文件名安全型 base64 编码方案。
7 static Base64.Encoder getUrlEncoder()返回一个 Base64.Encoder ,编码使用 URL 和文件名安全型 base64 编码方案。

注意:Base64 类的很多方法从 java.lang.Object 类继承。

代码演示

public static void main(String args[]) {
    
    
        try {
    
    

            // 使用基本编码
			String base64encodedString = Base64.getEncoder().encodeToString("itheima?java8".getBytes("utf-8"));
            System.out.println("Base64 编码字符串 (基本) :" + base64encodedString);

            // 解码
            byte[] base64decodedBytes = Base64.getDecoder().decode(base64encodedString);
            System.out.println("原始字符串: " + new String(base64decodedBytes, "utf-8"));
            
            // URL
            base64encodedString = Base64.getUrlEncoder().encodeToString("itheima?java8".getBytes("utf-8"));
            System.out.println("Base64 编码字符串 (URL) :" + base64encodedString);

            // MIME
            StringBuilder stringBuilder = new StringBuilder();

            for (int i = 0; i < 10; ++i) {
    
    
                stringBuilder.append(UUID.randomUUID().toString());
            }

            byte[] mimeBytes = stringBuilder.toString().getBytes("utf-8");
            String mimeEncodedString = Base64.getMimeEncoder().encodeToString(mimeBytes);
            System.out.println("Base64 编码字符串 (MIME) :" + mimeEncodedString);

        }catch(UnsupportedEncodingException e){
    
    
            System.out.println("Error :" + e.getMessage());
        }
    }

运行结果:

Base64 编码字符串 (基本) :aXRoZWltYT9qYXZhOA==
原始字符串: itheima?java8
Base64 编码字符串 (URL) :aXRoZWltYT9qYXZhOA==
Base64 编码字符串 (MIME) :ODM1MWI4MzMtZGZmZi00MDAwLTkwNTAtZjUxMjkzODMwY2E2YTVjZmMwN2QtYzM0My00ZjdhLTll
MDktMDFkMWZmZjA0MWRkOTE5Nzk0YzMtNTkyOC00Yjk0LThhYWEtMmIyNmFhN2Y3YzFmY2I2NDNl
ZmEtY2ZmNC00NTU4LWIzZDktZjAzMmE1M2FiOWM1ZDAyMzAyYzktZTM3MS00MDk3LWI2YWEtZTMz
MzZlMjE4NDdkZmEzYTA0NjktYWFhZC00M2ZiLTkzYTQtYTA0ZDIzMjIxY2RiMTMxYTU1MzgtZjZi
OS00NDcyLWJjOTYtZjViODVkNzdkNjMyODNhZTJhNDktZmU0Ni00ZDI5LWI0MDUtZWRkZGFmYjM2
MDliZTcyNWMxY2ItMWE3Ny00NmM4LTk2ZWUtZjBhYjY4YzgwMzU3N2Q3YWFiMTYtNzBjYi00MzNh
LTkxN2UtNzJmNWE0MjQzMGUx


笔记摘自:百度百科B站-黑马程序员磊哥《Java编程的逻辑》-马俊昌

猜你喜欢

转载自blog.csdn.net/weixin_44773109/article/details/127863612