1. 程式人生 > >細說 Java 中的字元和字串( 一 )

細說 Java 中的字元和字串( 一 )

一道經典問題

Java裡的char型別能不能儲存一箇中文字元?

對於這道題,絕大多數的答案都是“可以儲存”。給出的原因包括: 1. java中的char是unicode儲存,unicode編碼字符集中包含了漢字,所以可以儲存中文; 2. java內部其實是使用的UTF-16的編碼,所以是支援大部分非生僻漢字的; 3. 採用Unicode編碼集,一個char佔用兩個位元組,而一箇中文字元也是兩個位元組,因此Java中的char是可以表示一箇中文字元的; 4. Java的char只能表示utf­16中的BMP部分中文字元,不能表示擴充套件字符集裡的中文字元;

那麼,這個問題的終極答案到底是什麼?

Java API中關於char的說明

char型別是按照Unicode規範實現的一種資料型別,固定16bit大小。現如今,
Unicode字符集已經進行了擴充套件,表示的範圍已經超過了16bit。Unicode字符集的
數值範圍擴大到了[U+0000,U+10FFFF]。

也就是說一個char能夠儲存16bit大小的數值,即2個位元組。但是,就常用的UTF-8編碼來說,我們都聽說過他是用3或者4個位元組來表示一個漢字的。就拿3個位元組來算的話,一個char也存不下是不是?

我們繼續看api文件的其他段落:

一個char值可以表示BMP範圍內的Unicode字元。BMP表示[U+0000, U+FFFF]之間的Unicode字元。

而且,絕大部分的中文字元的Unicode範圍是[0x4E00, 0x9FBB],恰好是在BMP範圍內。

是不是說這裡出現了破解不了的矛盾呢?UTF-8佔用3到4個位元組,char只能存2個位元組(16bit),然而UTF-8中的幾乎所有漢字都是在BMP範圍內,也就是在char可儲存的範圍內,是不是矛盾了?

答案是不矛盾!關鍵點就在於接下來給出總結的第一條!

這裡先給出總結,後續再給出解釋: 1.char字元儲存的是Unicode編碼的程式碼點,也就是儲存的是U+FF00這樣的數值,然而我們在除錯或者輸出到輸出流的時候,是JVM或者開發工具按照程式碼點對應的編碼字元輸出的。 2. 所以雖然UTF-8編碼的中文字元是佔用3個或者4個位元組,但是對應的程式碼點仍然集中在[0x4E00, 0x9FBB],所以char是能夠存下在這個範圍內的中文字元的。 3. 但是對於超過16bit的Unicode字符集,也就是Unicode的擴充套件字符集,一個char是放不下的,需要兩個char才能放下。

Unicode編碼

Unicode的出現是對混亂的ANSI編碼世界的一個大一統,因而也叫做統一碼、萬國碼、單一碼。Unicode編碼把世界上常用的語言字元都進行了統一的編碼,一個數值就代表一個字元,而且世界範圍內公認。

ANSI的編碼世界裡,各中語言有自己的編碼規範,同一個數值在不同的國家代表不同的字元。所以當文字在不同國家傳遞的時候(比如發郵件,看國外網頁),問題就很大了,我明明寫的是“愛我中華”,美國朋友看到的確實”°®ÎÒÖлª”,一定是一臉問號!

        //=====模擬文字在不同編碼語言間傳遞的過程=====
        //發帖子
        String s = "愛我中華";
        //編碼成位元組流,通過網路傳入,或者儲存到檔案
        byte[] bytes = s.getBytes("GB2312");
        System.out.println(s);
        //國外朋友用自己電腦的編碼方式解析位元組流
        String s2 = new String(bytes, "ISO-8859-1");
        //oh! shit, wtf!        
        System.out.println(s2);

有了Unicode這個統一編碼之後,全世界的計算機都能正確的解析到原始的字元,對於國內的文字資訊,國外的朋友唯一要做的就是懂中文!

UTF-8只是Unicode編碼的一種編碼轉換規範,也就是怎麼儲存Unicode程式碼點的方案之一。另外還有UTF-16和UTF-32等編碼規範。Unicode為什麼需要這麼多編碼規範?直接儲存程式碼點行不行?

當然不行,儲存了就需要解析比如”漢字”兩個字的Unicode程式碼點是“0x6c49和0x5b57”也就是”6c495b57”。而且,Unicode的程式碼點還有3個位元組的,比如”10FF3B”,對於一個很長的上述數字串該怎麼解析?比如“10FF3B6c495b57”!

所以,需要某種編碼方案來區分那幾個數值是一個Unicode程式碼點,這種方案就是UTF-8、UTF-16、UTF-32這樣的編碼方案。

UTF-8編碼和程式碼點對應關係

UTF-8以位元組為單位對Unicode進行編碼。從Unicode到UTF-8的編碼方式如下:

Unicode編碼(十六進位制) UTF-8 位元組流(二進位制)
000000-00007F 0xxxxxxx
000080-0007FF 110xxxxx 10xxxxxx
000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF 11110xxx10xxxxxx10xxxxxx10xxxxxx

有沒有發現點什麼?當一個位元組表示一個字元時,二進位制開頭是0;當兩個位元組表示一個字元時,二進位制開頭是11;當3個位元組表示一個字元時,二進位制開頭是111;依次類推!

UTF-8編碼加入了多餘的標識位來區分一個Unicode程式碼點!才會出現中文漢字集中在[0x4E00, 0x9FBB]範圍的16bit數值內,UTF-8卻需要3個位元組儲存的原因。

另一個經典問題

怎麼判斷Java字串是否包含中文?

這個問題也很經典,一般我們可以查到的方法如下:

    //程式碼來自HanLP自然語言處理庫,git地址:https://github.com/hankcs/HanLP/blob/master/src/main/java/com/hankcs/hanlp/utility/TextUtility.java
    /**
     * 判斷某個字元是否為漢字
     *
     * @param c 需要判斷的字元
     * @return 是漢字返回true,否則返回false
     */
    public static boolean isChinese(char c)
    {
        String regex = "[\\u4e00-\\u9fa5]";
        return String.valueOf(c).matches(regex);
    }
//來源地址:https://blog.csdn.net/z69183787/article/details/53162069這裡考慮進了CJK的擴充套件字符集


// GENERAL_PUNCTUATION 判斷中文的“號  
    // CJK_SYMBOLS_AND_PUNCTUATION 判斷中文的。號  
    // HALFWIDTH_AND_FULLWIDTH_FORMS 判斷中文的,號  
    private static final boolean isChinese(char c) {  
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);  
        if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS  
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS  
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A  
                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION  
                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION  
                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS) {  
            return true;  
        }  
        return false;  
    }

總結一下,一般來說用第一種方法就足夠了,擴充套件字符集用的比較少,另外從HanLP的github星數來說,這種方案的通用度還是可信的。

這裡寫圖片描述

好了,先到這裡。這裡也留一個坑,mysql資料庫裡邊的VARCHAR型別和Java的char型別是一種處理方式麼?下一篇也會從String原始碼的角度對這裡的分析進行一個佐證。