Java 字符集编解码及乱码示例

一、说明

1、字节与位

1个字节是1个byte(B),1个byte是8个bit(位),也就是1个字节=8位

1024个字节是1KB

1、bit 位:位是计算机中存储数据的最小单位,指二进制数中的一个位数,其值为“0”或“1”。
2、byte 字节:字节是计算机存储容量的基本单位,一个字节由8位二进制数组成。
简单说就是,计算机使用二进制,比如:11010110,每个1或者0就是1个比特(bit),上面的11010110这8个1或者0就是一个字节(B)。

3、使用 位与(&) 运算可以取字节中的某些特定位。
如 ch&0x01 取第一个比特位
ch&0x72 取第二个以及第五、六、七个比特位

对于10110011,11100000,如何把第一个字节的前(高)3位和后一个字节的后(低)5位重新组成个字节00000101
以num1=10110011

num2=11100000

则 num = ((num2 & 0x1f)<<3) | ((num1 & 0xe0)>>5);

2、文件存储形式本质

  • 存储到硬盘上的文件中的内容都是字节,字符只是字节的一种表示形式,最终存储都是以字节形式进行存储,虽然通过记事本等工具打开存储在硬盘的文件时看到的都是可读的字符,这实际上是记事本等工具将字节进行了解码后呈现给使用者。
  • 存储在磁盘上的文件都是以字节的形式、都有特定的编码
  • 编码形式与字节、字符之间的理解
    • 字节,是  具有  特定编码的,也就是UTF-8编码的字节、与GBK编码的字节是不同的字节
    • 字符,是  没有  特定编码的,也就是中文字符,比如:你好,是不区分编码形式的,编码是对字节说的
    • 因此,在不同编码之间进行转换,是通过:编码A的字节--->解码成字符(使用编码A的解码器)--->编码成编码B的字节(编码B的编码器)

二、各种编码简介

最早出现:ASCII(外国)(American Standard Code for information interchange,美国信息交换标准代码)

ASCII是用7 bit(位) 来表示一个字符(使用1个字节,其中一位用不上),总共可以表示128个(2的7次方)字符。这么多个字符,对于西方国家是足够用的。

问题出现:之后随着计算机传播到其他国家,比如法国、德国等,就发现ASCII的128个字符不够用了,这样才出现了ISO-8859-1

ISO-8859-1(外国)

ISO-8859-1是用8bit(位)  来表示一个字符(使用1个字节,这一个字节中8bit(位)全都充分利用上了),总共可以表示256个(2的8次方)字符。由于ISO-8859-1是基于ASCII的,所以完全兼容ASCII表示的字符,即ISO-8859-1前128个字符=ASCII所表示的字符。

 

ISO-8859-1是用一个完整的字节(8bit)全都充分的利用上来表示一个字符。

 

问题出现:而一些其他国家,比如韩国、中国的文字的字符是无法使用ISO-8859-1进行表示的,所以GB2312就产生了。

GB2312(中国制订的)

GB2312用2个字节,表示一个汉字

问题出现:汉字存在一些生僻字,GBK就产生了。

GBK(中国)

GBK扩展了GB2312,增加中文生僻字,完全兼容GB2312

问题出现:汉字数量也是不全的,GB18030就产生了。

GB18030(中国)

扩展了GBK,中文最全的编码

问题出现:GB2312、GBK、GB18030仅是用来表示中文,但是对于韩文、日文无法表示,此时如果各个国家都制订自己的一套标准,就比较乱了。此时,国际标准化组织就将全世界所有国家的字符进行了汇总,提出了unicode。

BIG5(台湾) BIG5是繁体中文,台湾用,与GBK、GB18030完全不同、没有关系
unicode(国际标准化组织)

unicode采用2个字节表示一个字符,unicode可以表示全球所有国家的所有字符

 

新的问题:采用unicode后存储容量是问题:英美国家表示英文也需要2个字节,导致存储空间进行了膨胀,虽然unicode能表示所有字符,但是此格式不适合进行存储,因为针对使用英文的国家使用1个字节就可以表示并存储自己的字符了,这样使用2个字节表示并存储非常不适合使用此编码进行存储,因为会导致空间浪费。原来存储英文需要1m使用unicode需要2m。此时UTF-8登场

1、先介绍下UTF与unicode的区别:

UTF与unicode的区别:unicode是一种编码方式,比如用XXXX的编码方式表示汉字你,unicode与ascii、gbk等上面的编码是一种类型,即表示字符的方式。而UTF是一种存储格式(基于unicode的编码及存储格式),不管是UTF-8、UTF-16都是unicode是实现方式之一

UTF-16又分为:UTF-16LE(little endian)、UTF-16BE(big endian)

   文件开头:0xFEFF是小端

   文件开头:0xFFFE是大端

Uft-16是定长的存储编码方式,采用2个字节表示1个字符,存储西方字符也会存在浪费,此时UTF-8登场

2、UTF-8登场(Unicode Translation Format)

UTF-8是变长的存储编码方式:根据不同字符,采用不同数量的字节数表示,比如英文+数字采用1个字节表示,表示形式与ascii、iso-8859-1编码后表示形式完全相同(也就是uft-8兼容ascii码、iso-8859-1编码),中文通常用3个字节表示一个中文字符,最多用6个字节表示一个中文字符。

三、示例一

3、实现功能

将EndeCodeTest.txt文件内容读取出来,写入EndeCodeTest_out.txt文件

3.1 如果文件中全是英文,使用是iso-8859-1编码读取与写入是没问题的

package com.mzj.netty.ssy._09_nio._04_Charset;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

/**
 * 将EndeCodeTest.txt文件内容读取出来,写入EndeCodeTest_out.txt文件
 */
public class EndeCodeTest {
    public static void main(String[] args) throws IOException {
        String inputFile = "EndeCodeTest.txt";
        String outputFile = "EndeCodeTest_out.txt";

        RandomAccessFile inputRandomAccessFile = new RandomAccessFile(inputFile,"r");
        RandomAccessFile outputRandomAccessFile = new RandomAccessFile(outputFile,"rw");

        //input文件总长度
        long inputLength = new File(inputFile).length();

        FileChannel inputFileChannel = inputRandomAccessFile.getChannel();
        FileChannel outputFileChannel = outputRandomAccessFile.getChannel();

        //内存映射inputFile文件全部内容,最大支持映射整形int的最大值长度的字节数,也就是2GB文件大小
        MappedByteBuffer inputData = inputFileChannel.map(FileChannel.MapMode.READ_ONLY,0,inputLength);//MappedByteBuffer继承了ByteBuffer

        //创建utf8字符集、获取字符集  编解码器
        //这里设置字符集是utf-8时不会出现中文乱码
//        Charset charset = Charset.forName("utf-8");
        //理解为什么这里是iso-8859-1字符集,在输出文件中也不会出现乱码现象
        Charset charset = Charset.forName("iso-8859-1");
        /**
         * 原因:
         * 1、iso-8859-1字符集,1个字符用1个字节表示,原UTF-8文件中,英文是1个字节,中文是3个字节
         * 2、用iso-8859-1字符集进行解码:utf-8编码的英文(1个字节)正常解码成iso-8859-1的英文字符(1个字节),utf-8编码的中文中(3个字节)每一个字节都解码成iso-8859-1对应的一个字符,但是是错误的字符
         * 3、再用iso-8859-1字符集进行编码:将错误的iso-8859-1字符编码成字节(1个字符编码成1个字节),写入输出文件,写入文件的字节与先前输入文件中uft-8编码的字节内容相同
         * 4、IDEA打开文件默认采用uft-8编码,因此不是乱码
         */
        CharsetDecoder decoder = charset.newDecoder();//解码器:字节数组  转换  字符串 -
        CharsetEncoder encoder = charset.newEncoder();//编码器:字符串  转换  字节数组

        //解码:对内存映射文件的bytebuffer进行解码(ByteBuffer  ->  CharBuffer)
        CharBuffer charBuffer = decoder.decode(inputData);
        System.out.println(charBuffer);//此行输出的意义是便于分析使用iso-8859-1解码+编码汉字,没有问题的原因,解码后输出是乱码
        //编码:上面一行解码完的结果再反过来进行编码(CharBuffer  ->  ByteBuffer)
        ByteBuffer outputData = encoder.encode(charBuffer);

        //向output文件写
        outputFileChannel.write(outputData);

        //关闭io
        inputRandomAccessFile.close();
        outputRandomAccessFile.close();
    }
}

2.2 如果文件中有中文,使用是UTF-8编码读取与写入也是没问题的

2.3 如果文件中有中文,使用是iso-8859-1编码读取与写入也是没问题的,为什么?

思考:理论上iso-8859-1是西方字符集,没有中文

分析过程

1、通过下面方式可以输出当前操作系统所有可用的字符集

Charset.availableCharsets()

2、分析

 

UTF-8编码的文件中,英文以及数字,由于UTF-8变长的特性,也是占1个字节,同时与ISO-8859-1编码后内容相同,因此用ISO-8859-1格式先解码再编码,内容不变,很好理解;

UTF-8编码中,你好这两个汉字其中每一个中文占3个字节

比如:”你好”这两个汉字,用UTF-8格式编码后,如果以16进制表示,对应类似如下图的编码结果:其中每3个字节(1个字节(Byte) = 8位(bit)=2个16进制位表示,AB=10101011)代表一个汉字

 

用ISO-8859-1解码时,由于ISO-8859-1解码采用的是一个字节解码成一个字符的方式,所以AB(1个字节)当成一个字符(解码单位)进行解码,CD、EF、AA、BB、CC也是如此。因此解码后得到的字符,肯定是乱码的字符。

再用ISO-8859-1编码时,将原先ISO-8859-1解码的结果再编码(形成字节数组),结果还是AB CD EF AA BB CC。

虽然,以ISO-8859-1对UTF-8编码后文件进行解码的中间过程中文出现了乱码,但是再编码时,字节原封不动、毫不丢失的又写入了目标文件,而IDEA打开文件默认是UTF-8编码,因此没有产生乱码。

证明是这个原因,可以运行上面示例,输出结果为(最后一行输出是每个中文3个字节通过ISO-8859-1解码得到的字符):

hello
nihao
你好å

四、示例二

基于示例一修改:使用ISO-8859-1对UTF-8编码的文件进行解码,再使用UTF-8进行编码后写入目标文件,写入的目标文件中内容出现乱码。

package com.mzj.netty.ssy._09_nio._04_Charset;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

public class EndeCodeTest2 {
    public static void main(String[] args) throws IOException {
        String inputFile = "EndeCodeTest.txt";
        String outputFile = "EndeCodeTest_out.txt";

        RandomAccessFile inputRandomAccessFile = new RandomAccessFile(inputFile,"r");
        RandomAccessFile outputRandomAccessFile = new RandomAccessFile(outputFile,"rw");

        long inputLength = new File(inputFile).length();

        FileChannel inputFileChannel = inputRandomAccessFile.getChannel();
        FileChannel outputFileChannel = outputRandomAccessFile.getChannel();

        MappedByteBuffer inputData = inputFileChannel.map(FileChannel.MapMode.READ_ONLY,0,inputLength);//MappedByteBuffer继承了ByteBuffer

        Charset charset = Charset.forName("iso-8859-1");//使用ISO-8859-1对源UTF-8源文件进行解码

        CharsetDecoder decoder = charset.newDecoder();
        CharsetEncoder encoder = charset.newEncoder();

        CharBuffer charBuffer = decoder.decode(inputData);

        /**
         * 再使用UTF-8进行编码后写入目标文件,出现乱码
         *
         *  原因:先使用iso-8859-1进行解码成字符,此时是错误的字符:你好å(共计6个字符),再对这6个字符使用utf-8编码,肯定后续都是错误的了
         */
        ByteBuffer outputData = Charset.forName("utf-8").encode(charBuffer);


        //向output文件写
        outputFileChannel.write(outputData);

        //关闭io
        inputRandomAccessFile.close();
        outputRandomAccessFile.close();
    }
}

原因:使用ISO-8859-1对源UTF-8源文件进行解码,再使用UTF-8进行编码后写入目标文件,出现乱码,原因是先使用iso-8859-1进行解码成字符,此时是错误的字符:你好å(共计6个字符),再对这6个字符使用utf-8编码,肯定后续都是错误的了。

五、示例三

EndeCodeTest3中代码与EndeCodeTest1完全一样,操作上使用ISO-8859-1对GBK编码的文件进行解码,再使用ISO-8859-1进行编码后写入目标文件,IDEA打开后出现乱码。

原因:IDEA显示文件默认使用UTF-8,如果以GBK进行显示,则不会出现乱码

package com.mzj.netty.ssy._09_nio._04_Charset;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;

public class EndeCodeTest3 {
    public static void main(String[] args) throws IOException {
        String inputFile = "EndeCodeTest.txt";
        String outputFile = "EndeCodeTest_out.txt";

        RandomAccessFile inputRandomAccessFile = new RandomAccessFile(inputFile,"r");
        RandomAccessFile outputRandomAccessFile = new RandomAccessFile(outputFile,"rw");

        long inputLength = new File(inputFile).length();

        FileChannel inputFileChannel = inputRandomAccessFile.getChannel();
        FileChannel outputFileChannel = outputRandomAccessFile.getChannel();

        MappedByteBuffer inputData = inputFileChannel.map(FileChannel.MapMode.READ_ONLY,0,inputLength);//MappedByteBuffer继承了ByteBuffer

        Charset charset = Charset.forName("iso-8859-1");

        CharsetDecoder decoder = charset.newDecoder();
        CharsetEncoder encoder = charset.newEncoder();

        CharBuffer charBuffer = decoder.decode(inputData);

        ByteBuffer outputData = encoder.encode(charBuffer);//使用iso-8859-1对GBK源文件进行解码--编码

        //向output文件写
        outputFileChannel.write(outputData);

        //关闭io
        inputRandomAccessFile.close();
        outputRandomAccessFile.close();
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章