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();
    }
}

 

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