<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    posts - 73,  comments - 55,  trackbacks - 0
    版權申明,獲得授權轉載必須保留以下申明和鏈接:
    作者的blog:(http://blog.matrix.org.cn/page/Kaizen)

    在論壇上面常常看到初學者對線程的無可奈何,所以總結出了下面一篇文章,希望對一些正在學習使用java線程的初學者有所幫助。

    首先要理解線程首先需要了解一些基本的東西,我們現在所使用的大多數操作系統都屬于多任務,分時操作系統。正是由于這種操作系統的出現才有了多線程這個概念。我們使用的windows,linux就屬于此列。什么是分時操作系統呢,通俗一點與就是可以同一時間執行多個程序的操作系統,在自己的電腦上面,你是不是一邊聽歌,一邊聊天還一邊看網頁呢?但實際上,并不上cpu在同時執行這些程序,cpu只是將時間切割為時間片,然后將時間片分配給這些程序,獲得時間片的程序開始執行,不等執行完畢,下個程序又獲得時間片開始執行,這樣多個程序輪流執行一段時間,由于現在cpu的高速計算能力,給人的感覺就像是多個程序在同時執行一樣。
    一般可以在同一時間內執行多個程序的操作系統都有進程的概念.一個進程就是一個執行中的程序,而每一個進程都有自己獨立的一塊內存空間,一組系統資源.在進程概念中,每一個進程的內部數據和狀態都是完全獨立的.因此可以想像創建并執行一個進程的系統開像是比較大的,所以線程出現了。在java中,程序通過流控制來執行程序流,程序中單個順序的流控制稱為線程,多線程則指的是在單個程序中可以同時運行多個不同的線程,執行不同的任務.多線程意味著一個程序的多行語句可以看上去幾乎在同一時間內同時運行.(你可以將前面一句話的程序換成進程,進程是程序的一次執行過程,是系統運行程序的基本單位)

    線程與進程相似,是一段完成某個特定功能的代碼,是程序中單個順序的流控制;但與進程不同的是,同類的多個線程是共享一塊內存空間和一組系統資源,而線程本身的數據通常只有微處理器的寄存器數據,以及一個供程序執行時使用的堆棧.所以系統在產生一個線程,或者在各個線程之間切換時,負擔要比進程小的多,正因如此,線程也被稱為輕負荷進程(light-weight process).一個進程中可以包含多個線程.

    多任務是指在一個系統中可以同時運行多個程序,即有多個獨立運行的任務,每個任務對應一個進程,同進程一樣,一個線程也有從創建,運行到消亡的過程,稱為線程的生命周期.用線程的狀態(state)表明線程處在生命周期的哪個階段.線程有創建,可運行,運行中,阻塞,死亡五中狀態.通過線程的控制與調度可使線程在這幾種狀態間轉化每個程序至少自動擁有一個線程,稱為主線程.當程序加載到內存時,啟動主線程.

    [線程的運行機制以及調度模型]
    java中多線程就是一個類或一個程序執行或管理多個線程執行任務的能力,每個線程可以獨立于其他線程而獨立運行,當然也可以和其他線程協同運行,一個類控制著它的所有線程,可以決定哪個線程得到優先級,哪個線程可以訪問其他類的資源,哪個線程開始執行,哪個保持休眠狀態。
    下面是線程的機制圖:


    線程的狀態表示線程正在進行的活動以及在此時間段內所能完成的任務.線程有創建,可運行,運行中,阻塞,死亡五中狀態.一個具有生命的線程,總是處于這五種狀態之一:
    1.創建狀態
    使用new運算符創建一個線程后,該線程僅僅是一個空對象,系統沒有分配資源,稱該線程處于創建狀態(new thread)
    2.可運行狀態
    使用start()方法啟動一個線程后,系統為該線程分配了除CPU外的所需資源,使該線程處于可運行狀態(Runnable)
    3.運行中狀態
    Java運行系統通過調度選中一個Runnable的線程,使其占有CPU并轉為運行中狀態(Running).此時,系統真正執行線程的run()方法.
    4.阻塞狀態
    一個正在運行的線程因某種原因不能繼續運行時,進入阻塞狀態(Blocked)
    5.死亡狀態
    線程結束后是死亡狀態(Dead)

    同一時刻如果有多個線程處于可運行狀態,則他們需要排隊等待CPU資源.此時每個線程自動獲得一個線程的優先級(priority),優先級的高低反映線程的重要或緊急程度.可運行狀態的線程按優先級排隊,線程調度依據優先級基礎上的"先到先服務"原則.
    線程調度管理器負責線程排隊和CPU在線程間的分配,并由線程調度算法進行調度.當線程調度管理器選種某個線程時,該線程獲得CPU資源而進入運行狀態.

    線程調度是先占式調度,即如果在當前線程執行過程中一個更高優先級的線程進入可運行狀態,則這個線程立即被調度執行.先占式調度分為:獨占式和分時方式.
    獨占方式下,當前執行線程將一直執行下去,直 到執行完畢或由于某種原因主動放棄CPU,或CPU被一個更高優先級的線程搶占
    分時方式下,當前運行線程獲得一個時間片,時間到時,即使沒有執行完也要讓出CPU,進入可運行狀態,等待下一個時間片的調度.系統選中其他可運行狀態的線程執行
    分時方式的系統使每個線程工作若干步,實現多線程同時運行

    另外請注意下面的線程調度規則(如果有不理解,不急,往下看):
    ①如果兩個或是兩個以上的線程都修改一個對象,那么把執行修改的方法定義為被同步的(Synchronized),如果對象更新影響到只讀方法,那么只度方法也應該定義為同步的
    ②如果一個線程必須等待一個對象狀態發生變化,那么它應該在對象內部等待,而不是在外部等待,它可以調用一個被同步的方法,并讓這個方法調用wait()
    ③每當一個方法改變某個對象的狀態的時候,它應該調用notifyAll()方法,這給等待隊列的線程提供機會來看一看執行環境是否已發生改變
    ④記住wait(),notify(),notifyAll()方法屬于Object類,而不是Thread類,仔細檢查看是否每次執行wait()方法都有相應的notify()或notifyAll()方法,且它們作用與相同的對象 在java中每個類都有一個主線程,要執行一個程序,那么這個類當中一定要有main方法,這個man方法也就是java class中的主線程。你可以自己創建線程,有兩種方法,一是繼承Thread類,或是實現Runnable接口。一般情況下,最好避免繼承,因為java中是單根繼承,如果你選用繼承,那么你的類就失去了彈性,當然也不能全然否定繼承Thread,該方法編寫簡單,可以直接操作線程,適用于單重繼承情況。至于選用那一種,具體情況具體分析。


    eg.繼承Thread
    public class MyThread_1 extends Thread
    {
    public void run()
    {
    //some code
    }
    }


    eg.實現Runnable接口
    public class MyThread_2 implements Runnable
    {
    public void run()
    {
    //some code
    }
    }



    當使用繼承創建線程,這樣啟動線程:
    new MyThread_1().start()


    當使用實現接口創建線程,這樣啟動線程:
    new Thread(new MyThread_2()).start()


    注意,其實是創建一個線程實例,并以實現了Runnable接口的類為參數傳入這個實例,當執行這個線程的時候,MyThread_2中run里面的代碼將被執行。
    下面是完成的例子:

    public class MyThread implements Runnable
    {

    public void run()
    {
    System.out.println("My Name is "+Thread.currentThread().getName());
    }
    public static void main(String[] args)
    {
    new Thread(new MyThread()).start();
    }
    }



    執行后將打印出:
    My Name is Thread-0

    你也可以創建多個線程,像下面這樣
    new Thread(new MyThread()).start();
    new Thread(new MyThread()).start();
    new Thread(new MyThread()).start();



    那么會打印出:
    My Name is Thread-0
    My Name is Thread-1
    My Name is Thread-2


    看了上面的結果,你可能會認為線程的執行順序是依次執行的,但是那只是一般情況,千萬不要用以為是線程的執行機制;影響線程執行順序的因素有幾點:首先看看前面提到的優先級別


    public class MyThread implements Runnable
    {

    public void run()
    {
    System.out.println("My Name is "+Thread.currentThread().getName());
    }
    public static void main(String[] args)
    {
    Thread t1=new Thread(new MyThread());
    Thread t2=new Thread(new MyThread());
    Thread t3=new Thread(new MyThread());
    t2.setPriority(Thread.MAX_PRIORITY);//賦予最高優先級
    t1.start();
    t2.start();
    t3.start();
    }
    }


    再看看結果:
    My Name is Thread-1
    My Name is Thread-0
    My Name is Thread-2



    線程的優先級分為10級,分別用1到10的整數代表,默認情況是5。上面的t2.setPriority(Thread.MAX_PRIORITY)等價與t2.setPriority(10)
    然后是線程程序本身的設計,比如使用sleep,yield,join,wait等方法(詳情請看JDKDocument)

    public class MyThread implements Runnable
    {
    public void run()
    {
    try
    {
    int sleepTime=(int)(Math.random()*100);//產生隨機數字,
    Thread.currentThread().sleep(sleepTime);//讓其休眠一定時間,時間又上面sleepTime決定
    //public static void sleep(long millis)throw InterruptedException (API)
    System.out.println(Thread.currentThread().getName()+" 睡了 "+sleepTime);
    }catch(InterruptedException ie)//由于線程在休眠可能被中斷,所以調用sleep方法的時候需要捕捉異常
    {
    ie.printStackTrace();
    }
    }
    public static void main(String[] args)
    {
    Thread t1=new Thread(new MyThread());
    Thread t2=new Thread(new MyThread());
    Thread t3=new Thread(new MyThread());
    t1.start();
    t2.start();
    t3.start();
    }
    }


    執行后觀察其輸出:

    Thread-0 睡了 11
    Thread-2 睡了 48
    Thread-1 睡了 69




    上面的執行結果是隨機的,再執行很可能出現不同的結果。由于上面我在run中添加了休眠語句,當線程休眠的時候就會讓出cpu,cpu將會選擇執行處于runnable狀態中的其他線程,當然也可能出現這種情況,休眠的Thread立即進入了runnable狀態,cpu再次執行它。
    [線程組概念]
    線程是可以被組織的,java中存在線程組的概念,每個線程都是一個線程組的成員,線程組把多個線程集成為一個對象,通過線程組可以同時對其中的多個線程進行操作,如啟動一個線程組的所有線程等.Java的線程組由java.lang包中的Thread——Group類實現.
    ThreadGroup類用來管理一組線程,包括:線程的數目,線程間的關系,線程正在執行的操作,以及線程將要啟動或終止時間等.線程組還可以包含線程組.在Java的應用程序中,最高層的線程組是名位main的線程組,在main中還可以加入線程或線程組,在mian的子線程組中也可以加入線程和線程組,形成線程組和線程之間的樹狀繼承關系。像上面創建的線程都是屬于main這個線程組的。
    借用上面的例子,main里面可以這樣寫:

    public static void main(String[] args)
    {
    /***************************************
    ThreadGroup(String name)
    ThreadGroup(ThreadGroup parent, String name)
    ***********************************/
    ThreadGroup group1=new ThreadGroup("group1");
    ThreadGroup group2=new ThreadGroup(group1,"group2");
    Thread t1=new Thread(group2,new MyThread());
    Thread t2=new Thread(group2,new MyThread());
    Thread t3=new Thread(group2,new MyThread());
    t1.start();
    t2.start();
    t3.start();
    }



    線程組的嵌套,t1,t2,t3被加入group2,group2加入group1。
    另外一個比較多就是關于線程同步方面的,試想這樣一種情況,你有一筆存款在銀行,你在一家銀行為你的賬戶存款,而你的妻子在另一家銀行從這個賬戶提款,現在你有1000塊在你的賬戶里面。你存入了1000,但是由于另一方也在對這筆存款進行操作,人家開始執行的時候只看到賬戶里面原來的1000元,當你的妻子提款1000元后,你妻子所在的銀行就認為你的賬戶里面沒有錢了,而你所在的銀行卻認為你還有2000元。
    看看下面的例子:

    class BlankSaving //儲蓄賬戶
    {
    private static int money=10000;
    public void add(int i)
    {
    money=money+i;
    System.out.println("Husband 向銀行存入了 [¥"+i+"]");
    }
    public void get(int i)
    {
    money=money-i;
    System.out.println("Wife 向銀行取走了 [¥"+i+"]");
    if(money<0)
    System.out.println("余額不足!");
    }
    public int showMoney()
    {
    return money;
    }
    }


    class Operater implements Runnable
    {
    String name;
    BlankSaving bs;
    public Operater(BlankSaving b,String s)
    {
    name=s;
    bs=b;



    }
    public static void oper(String name,BlankSaving bs)
    {



    if(name.equals("husband"))
    {
    try
    {
    for(int i=0;i<10;i++)
    {
    Thread.currentThread().sleep((int)(Math.random()*300));
    bs.add(1000);
    }
    }catch(InterruptedException e){}
    }else
    {
    try
    {



    for(int i=0;i<10;i++)
    {
    Thread.currentThread().sleep((int)(Math.random()*300));
    bs.get(1000);
    }
    }catch(InterruptedException e){}
    }
    }
    public void run()
    {
    oper(name,bs);
    }
    }
    public class BankTest
    {
    public static void main(String[] args)throws InterruptedException
    {
    BlankSaving bs=new BlankSaving();
    Operater o1=new Operater(bs,"husband");
    Operater o2=new Operater(bs,"wife");
    Thread t1=new Thread(o1);
    Thread t2=new Thread(o2);
    t1.start();
    t2.start();
    Thread.currentThread().sleep(500);
    }



    }



    下面是其中一次的執行結果:



    ---------first--------------
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Husband 向銀行存入了 [¥1000]


    看到了嗎,這可不是正確的需求,在husband還沒有結束操作的時候,wife就插了進來,這樣很可能導致意外的結果。解決辦法很簡單,就是將對數據進行操作方法聲明為synchronized,當方法被該關鍵字聲明后,也就意味著,如果這個數據被加鎖,只有一個對象得到這個數據的鎖的時候該對象才能對這個數據進行操作。也就是當你存款的時候,這筆賬戶在其他地方是不能進行操作的,只有你存款完畢,銀行管理人員將賬戶解鎖,其他人才能對這個賬戶進行操作。
    修改public static void oper(String name,BlankSaving bs)為public static void oper(String name,BlankSaving bs),再看看結果:

    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Husband 向銀行存入了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]
    Wife 向銀行取走了 [¥1000]




    當丈夫完成操作后,妻子才開始執行操作,這樣的話,對共享對象的操作就不會有問題了。
    [wait and notify]
    你可以利用這兩個方法很好的控制線程的執行流程,當線程調用wait方法后,線程將被掛起,直到被另一線程喚醒(notify)或則是如果wait方法指定有時間得話,在沒有被喚醒的情況下,指定時間時間過后也將自動被喚醒。但是要注意一定,被喚醒并不是指馬上執行,而是從組塞狀態變為可運行狀態,其是否運行還要看cpu的調度。
    事例代碼:

    class MyThread_1 extends Thread
    {
    Object lock;
    public MyThread_1(Object o)
    {
    lock=o;
    }
    public void run()
    {
    try
    {
    synchronized(lock)
    {
    System.out.println("Enter Thread_1 and wait");
    lock.wait();
    System.out.println("be notified");
    }
    }catch(InterruptedException e){}
    }
    }
    class MyThread_2 extends Thread
    {
    Object lock;
    public MyThread_2(Object o)
    {
    lock=o;
    }
    public void run()
    {
    synchronized(lock)
    {
    System.out.println("Enter Thread_2 and notify");
    lock.notify();
    }
    }
    }
    public class MyThread
    {
    public static void main(String[] args)
    {
    int[] in=new int[0];//notice
    MyThread_1 t1=new MyThread_1(in);
    MyThread_2 t2=new MyThread_2(in);
    t1.start();
    t2.start();
    }
    }




    執行結果如下:
    Enter Thread_1 and wait
    Enter Thread_2 and notify
    Thread_1 be notified


    可能你注意到了在使用wait and notify方法得時候我使用了synchronized塊來包裝這兩個方法,這是由于調用這兩個方法的時候線程必須獲得鎖,也就是上面代碼中的lock[],如果你不用synchronized包裝這兩個方法的得話,又或則鎖不一是同一把,比如在MyThread_2中synchronized(lock)改為synchronized(this),那么執行這個程序的時候將會拋出java.lang.IllegalMonitorStateException執行期異常。另外wait and notify方法是Object中的,并不在Thread這個類中。最后你可能注意到了這點:int[] in=new int[0];為什么不是創建new Object而是一個0長度的數組,那是因為在java中創建一個0長度的數組來充當鎖更加高效。

    Thread作為java中一重要組成部分,當然還有很多地方需要更深刻的認識,上面只是對Thread的一些常識和易錯問題做了一個簡要的總結,若要真正的掌握java的線程,還需要自己多做總結
    posted @ 2006-12-12 11:28 保爾任 閱讀(189) | 評論 (0)編輯 收藏
    從基礎的開始

    ? 最小的單元是位(bit),接著是字節(Byte),一個字節=8位,英語表示是1 byte=8 bits 。機器語言的單位Byte。接著是KB,1 KB=1024 Byte;? 接著是MB,1 MB=1024 KB;? 接著是GB,1 GB=1024 MB ;接著是TB, 1TB=1024 GB。
    ? 接著是進制:二進制0和1,8進制0-7, 十進制不用說,10進制0-9后面是A,B,C,D,E,F 他們關系如下:
    Binary???? Octal? Decimal Hex
    0????????? 0????? 0?????? 0
    1????????? 1????? 1?????? 1
    10???????? 2????? 2?????? 2
    11???????? 3????? 3?????? 3
    100??????? 4????? 4?????? 4
    101??????? 5????? 5?????? 5
    110??????? 6????? 6?????? 6
    111??????? 7????? 7?????? 7
    1000?????? 10???? 8?????? 8
    1001?????? 11???? 9?????? 9
    1010?????? 12???? 10????? A
    1011?????? 13???? 11????? B
    1100?????? 14???? 12????? C
    1101?????? 15???? 13????? D
    1110?????? 16???? 14????? E
    1111?????? 17???? 15????? F

    接著是上層建筑字符:

    ? 字符是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。字符集是多個字符的集合,字符集種類較多,每個字符集包含的字符個數不同,常見字符集名稱:ASCII字符集、GB2312字符集、BIG5字符集、 GB 18030字符集、Unicode字符集等。計算機要準確的處理各種字符集文字,需要進行字符編碼,以便計算機能夠識別和存儲各種文字。

    ASCII 字符集
    ? ASCII(American Standard Code for Information Interchange,美國信息互換標準代碼)是基于羅馬字母表的一套電腦編碼系統,它主要用于顯示現代英語和其他西歐語言。它是現今最通用的單字節編碼系統,并等同于國際標準ISO 646。

    包含內容:

    控制字符:回車鍵、退格、換行鍵等。

    可顯示字符:英文大小寫字符、阿拉伯數字和西文符號

    ASCII擴展字符集擴展:表格符號、計算符號、希臘字母和特殊的拉丁符號。

      第0~32號及第127號(共34個)是控制字符或通訊專用字符,如控制符:LF(換行)、CR(回車)、FF(換頁)、DEL(刪除)、BEL(振鈴)等;通訊專用字符:SOH(文頭)、EOT(文尾)、ACK(確認)等;

      第33~126號(共94個)是字符,其中第48~57號為0~9十個阿拉伯數字;65~90號為26個大寫英文字母,97~122號為26個小寫英文字母,其余為一些標點符號、運算符號等。?

      注意:在計算機的存儲單元中,一個ASCII碼值占一個字節(8個二進制位),其最高位(b7)用作奇偶校驗位。所謂奇偶校驗,是指在代碼傳送過程中用來檢驗是否出現錯誤的一種方法,一般分奇校驗和偶校驗兩種。奇校驗規定:正確的代碼一個字節中1的個數必須是奇數,若非奇數,則在最高位b7添1;偶校驗規定:正確的代碼一個字節中1的個數必須是偶數,若非偶數,則在最高位b7添1。

    DEC?? HEX? CHAR? CODE? C 程序(轉義)?
    0???? 00   NUL (’\0’)?
    1???? 01   SOH  ?
    2???? 02   STX  ?
    3???? 03   ETX  ?
    4???? 04   EOT  ?
    5???? 05   ENQ  ?
    6???? 06   ACK  ?
    7???? 07   BEL (’\a’)?
    8???? 08   BS (’\b’)?
    9???? 09   HT (’\t’)?
    10??? 0A   LF (’\n’)?
    11??? 0B   VT (’\v’)?
    12??? 0C   FF (’\f’)?
    13??? 0D   CR (’\r’)?
    14??? 0E   SO  ?
    15??? 0F   SI  ?
    16??? 10   DLE  ?
    17??? 11   DC1  ?
    18??? 12   DC2  ?
    19??? 13   DC1  ?
    20??? 14   DC4  ?
    21??? 15   NAK  ?
    22??? 16   SYN  ?
    23??? 17   ETB  ?
    24??? 18   CAN  ?
    25??? 19   EM  ?
    26??? 1A   SUB  ?
    27??? 1B   ESC  ?
    28??? 1C   FS  ?
    29??? 1D   GS  ?
    30??? 1E   RS  ?
    31??? 1F   US  ?
    32??? 20 (space,空格)    ?
    33??? 21??? !    ?
    34??? 22??? "    ?
    35??? 23??? #    ?
    36??? 24??? $    ?
    37??? 25??? %    ?
    38??? 26??? &    ?
    39??? 27??? ’    ?
    40??? 28??? (    ?
    41??? 29??? )    ?
    42??? 2A??? *    ?
    43??? 2B??? +    ?
    44??? 2C??? ,    ?
    45??? 2D??? -    ?
    46??? 2E??? .    ?
    47??? 2F??? /    ?
    48??? 30??? 0    ?
    49??? 31??? 1    ?
    50??? 32??? 2    ?
    51??? 33??? 3    ?
    52??? 34??? 4    ?
    53??? 35??? 5    ?
    54??? 36??? 6    ?
    55??? 37??? 7    ?
    56??? 38??? 8    ?
    57??? 39??? 9    ?
    58??? 3A??? :    ?
    59??? 3B??? ;    ?
    60??? 3C??? <    ?
    61??? 3D??? =    ?
    62??? 3E??? >    ?
    63??? 3F??? ?    ?
    64??? 40??? @    ?
    65??? 41??? A    ?
    66??? 42??? B    ?
    67??? 43??? C    ?
    68??? 44??? D    ?
    69??? 45??? E    ?
    70??? 46??? F    ?
    71??? 47??? G    ?
    72??? 48??? H    ?
    73??? 49??? I    ?
    74??? 4A??? J    ?
    75??? 4B??? K    ?
    76??? 4C??? L    ?
    77??? 4D??? M    ?
    78??? 4E??? N    ?
    79??? 4F??? O    ?
    80??? 50??? P    ?
    81??? 51??? Q    ?
    82??? 52??? R    ?
    83??? 53??? S    ?
    84??? 54??? T    ?
    85??? 55??? U    ?
    86??? 56??? V    ?
    87??? 57??? W    ?
    88??? 58??? X    ?
    89??? 59??? Y    ?
    90??? 5A??? Z    ?
    91??? 5B??? [    ?
    92??? 5C??? \   (’\\’)?
    93??? 5D??? ]    ?
    94??? 5E??? ^    ?
    95??? 5F??? _    ?
    96??? 60??? `    ?
    97??? 61??? a    ?
    98??? 62??? b    ?
    99??? 63??? c    ?
    100?? 64??? d    ?
    101?? 65??? e    ?
    102?? 66??? f    ?
    103?? 67??? g    ?
    104?? 68??? h    ?
    105?? 69??? i    ?
    106?? 6A??? j    ?
    107?? 6B??? k    ?
    108?? 6C??? l    ?
    109?? 6D??? m    ?
    110?? 6E??? n    ?
    111?? 6F??? o    ?
    112?? 70??? p    ?
    113?? 71??? q    ?
    114?? 72??? r    ?
    115?? 73??? s    ?
    116?? 74??? t    ?
    117?? 75??? u    ?
    118?? 76??? v    ?
    119?? 77??? w    ?
    120?? 78??? x    ?
    121?? 79??? y    ?
    122?? 7A??? z    ?
    123?? 7B??? {    ?
    124?? 7C??? |    ?
    125?? 7D??? }    ?
    126?? 7E??? ~    ?
    127?? 7F????   DEL


    GB2312 字符集

    ? GB2312又稱為GB2312-80字符集,全稱為《信息交換用漢字編碼字符集·基本集》,由原中國國家標準總局發布,1981年5月1日實施,是中國國家標準的簡體中文字符集。它所收錄的漢字已經覆蓋99.75%的使用頻率,基本滿足了漢字的計算機處理需要。在中國大陸和新加坡獲廣泛使用。
    ?
    ? GB2312收錄簡化漢字及一般符號、序號、數字、拉丁字母、日文假名、希臘字母、俄文字母、漢語拼音符號、漢語注音字母,共 7445 個圖形字符。其中包括6763個漢字,其中一級漢字3755個,二級漢字3008個;包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母在內的682個全角字符。

    ? GB2312中對所收漢字進行了“分區”處理,每區含有94個漢字/符號。這種表示方式也稱為區位碼。

    它是用雙字節表示的,兩個字節中前面的字節為第一字節,后面的字節為第二字節。習慣上稱第一字節為“高字節” ,而稱第二字節為“低字節”。“高位字節”使用了0xA1-0xF7(把01-87區的區號加上0xA0),“低位字節”使用了0xA1-0xFE(把01-94加上0xA0)。

    ? 以GB2312字符集的第一個漢字“啊”字為例,它的區號16,位號01,則區位碼是1601,在大多數計算機程序中,高字節和低字節分別加0xA0得到程序的漢字處理編碼0xB0A1。計算公式是:0xB0=0xA0+16, 0xA1=0xA0+1。

    GBK字符集
    ? GBK字符集是GB2312的擴展(K),GBK1.0收錄了21886個符號,它分為漢字區和圖形符號區,漢字區包括21003個字符。GBK字符集主要擴展了繁體中文字的支持。


    BIG5 字符集

    ? BIG5又稱大五碼或五大碼,1984年由臺灣財團法人信息工業策進會和五間軟件公司宏碁 (Acer)、神通 (MiTAC)、佳佳、零壹 (Zero One)、大眾 (FIC)創立,故稱大五碼。Big5碼的產生,是因為當時臺灣不同廠商各自推出不同的編碼,如倚天碼、IBM PS55、王安碼等,彼此不能兼容;另一方面,臺灣政府當時尚未推出官方的漢字編碼,而中國大陸的GB2312編碼亦未有收錄繁體中文字。

    ? Big5字符集共收錄13,053個中文字,該字符集在中國臺灣使用。耐人尋味的是該字符集重復地收錄了兩個相同的字:“兀”(0xA461及0xC94A)、“嗀”(0xDCD1及0xDDFC)。

    ? Big5碼使用了雙字節儲存方法,以兩個字節來編碼一個字。第一個字節稱為“高位字節”,第二個字節稱為“低位字節”。高位字節的編碼范圍0xA1-0xF9,低位字節的編碼范圍0x40-0x7E及0xA1-0xFE。

    ? 盡管Big5碼內包含一萬多個字符,但是沒有考慮社會上流通的人名、地名用字、方言用字、化學及生物科等用字,沒有包含日文平假名及片假字母。

    例如臺灣視“著”為“著”的異體字,故沒有收錄“著”字。康熙字典中的一些部首用字(如“亠”、“疒”、“辵”、“癶”等)、常見的人名用字(如“堃”、“煊”、“栢”、“喆”等) 也沒有收錄到Big5之中。


    GB18030 字符集

    GB18030的全稱是GB18030-2000《信息交換用漢字編碼字符集基本集的擴充》,是我國政府于2000年3月17日發布的新的漢字編碼國家標準,2001年8月31日后在中國市場上發布的軟件必須符合本標準。GB 18030字符集標準的出臺經過廣泛參與和論證,來自國內外知名信息技術行業的公司,信息產業部和原國家質量技術監督局聯合實施。

    GB 18030字符集標準解決漢字、日文假名、朝鮮語和中國少數民族文字組成的大字符集計算機編碼問題。該標準的字符總編碼空間超過150萬個編碼位,收錄了27484個漢字,覆蓋中文、日文、朝鮮語和中國少數民族文字。滿足中國大陸、香港、臺灣、日本和韓國等東亞地區信息交換多文種、大字量、多用途、統一編碼格式的要求。并且與Unicode 3.0版本兼容,填補Unicode擴展字符字匯“統一漢字擴展A”的內容。并且與以前的國家字符編碼標準(GB2312,GB13000.1)兼容。

    編碼方法:
    GB 18030標準采用單字節、雙字節和四字節三種方式對字符編碼。單字節部分使用0×00至0×7F碼(對應于ASCII碼的相應碼)。雙字節部分,首字節碼從0×81至0×FE,尾字節碼位分別是0×40至0×7E和0×80至0×FE。四字節部分采用GB/T 11383未采用的0×30到0×39作為對雙字節編碼擴充的后綴,這樣擴充的四字節編碼,其范圍為0×81308130到0×FE39FE39。其中第一、三個字節編碼碼位均為0×81至0×FE,第二、四個字節編碼碼位均為0×30至0×39。

    按照程序員的稱呼,GB2312、GBK到GB18030都屬于雙字節字符集 (DBCS)。

    接著是國際通用的unicode字符集

    Unicode字符集(簡稱為UCS)

    1.名稱的由來

    Unicode字符集編碼是(Universal Multiple-Octet Coded Character Set) 通用多八位編碼字符集的簡稱,支持世界上超過650種語言的國際字符集。Unicode允許在同一服務器上混合使用不同語言組的不同語言。它是由一個名為 Unicode 學術學會(Unicode Consortium)的機構制訂的字符編碼系統,支持現今世界各種不同語言的書面文本的交換、處理及顯示。該編碼于1990年開始研發,1994年正式公布,最新版本是2005年3月31日的Unicode 4.1.0。Unicode是一種在計算機上使用的字符編碼。它為每種語言中的每個字符設定了統一并且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。

    2.編碼方法

    Unicode 標準始終使用十六進制數字,而且在書寫時在前面加上前綴“U+”,例如字母“A”的編碼為 004116 。所以“A”的編碼書寫為“U+0041”。

    3.UTF-8 編碼
    UTF-8是Unicode的其中一個使用方式。 UTF是 Unicode Translation Format,即把Unicode轉做某種格式的意思。

    UTF-8便于不同的計算機之間使用網絡傳輸不同語言和編碼的文字,使得雙字節的Unicode能夠在現存的處理單字節的系統上正確傳輸。

    UTF-8使用可變長度字節來儲存 Unicode字符,例如ASCII字母繼續使用1字節儲存,重音文字、希臘字母或西里爾字母等使用2字節來儲存,而常用的漢字就要使用3字節。輔助平面字符則使用4字節。

    4.UTF-16 和 UTF-32 編碼
    UTF-32、UTF-16 和 UTF-8 是 Unicode 標準的編碼字符集的字符編碼方案,UTF-16 使用一個或兩個未分配的 16 位代碼單元的序列對 Unicode 代碼點進行編碼;UTF-32 即將每一個 Unicode 代碼點表示為相同值的 32 位整數

    通過一個問題了解unicode編碼
    ?
    問題:使用Windows記事本的“另存為”,可以在ANSI、GBK、Unicode、Unicode big endian和UTF-8這幾種編碼方式間相互轉換。同樣是txt文件,Windows怎樣識別編碼方式的呢?
    我很早前就發現Unicode、Unicode big endian和UTF-8編碼的txt文件的開頭會多出幾個字節,分別是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但這些標記是基于什么標準呢?

    答案:

    ANSI字符集定義:ASCII字符集,以及由此派生并兼容的字符集,如:GB2312,正式的名稱為MBCS(Multi-Byte Chactacter System,多字節字符系統),通常也稱為ANSI字符集。

    UNICODE 與 UTF8、UTF16

    ? 由于每種語言都制定了自己的字符集,導致最后存在的各種字符集實在太多,在國際交流中要經常轉換字符集非常不便。因此,產生了Unicode字符集,它固定使用16 bits(兩個字節)來表示一個字符,共可以表示65536個字符
    ? 標準的 Unicode 稱為UTF-16(UTF:UCS Transformation Format )。后來為了雙字節的Unicode能夠在現存的處理單字節的系統上正確傳輸,出現了UTF-8,使用類似MBCS的方式對Unicode進行編碼。(Unicode字符集有多種編碼形式)
    例如"連通"兩個字的Unicode標準編碼UTF-16 (big endian)為:DE 8F 1A 90?
    而其UTF-8編碼為:E8 BF 9E E9 80 9A

    當一個軟件打開一個文本時,它要做的第一件事是決定這個文本究竟是使用哪種字符集的哪種編碼保存的。軟件一般采用三種方式來決定文本的字符集和編碼:
    檢測文件頭標識,提示用戶選擇,根據一定的規則猜測
    最標準的途徑是檢測文本最開頭的幾個字節,開頭字節 Charset/encoding,如下表:
    EF BB BF????? UTF-8
    FE FF?????????? UTF-16/UCS-2, little endian
    FF FE?????????? UTF-16/UCS-2, big endian
    FF FE 00 00? UTF-32/UCS-4, little endian.
    00 00 FE FF? UTF-32/UCS-4, big-endian.?


    1、big endian和little endian
    big endian和little endian是CPU處理多字節數的不同方式。例如“漢”字的Unicode編碼是6C49。那么寫到文件里時,究竟是將6C寫在前面,還是將49寫在前面?如果將6C寫在前面,就是big endian。還是將49寫在前面,就是little endian。
    “endian”這個詞出自《格列佛游記》。小人國的內戰就源于吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位。
    我們一般將endian翻譯成“字節序”,將big endian和little endian稱作“大尾”和“小尾”。

    2、字符編碼、內碼,順帶介紹漢字編碼
    ? 字符必須編碼后才能被計算機處理。計算機使用的缺省編碼方式就是計算機的內碼。早期的計算機使用7位的ASCII編碼,為了處理漢字,程序員設計了用于簡體中文的GB2312和用于繁體中文的big5。
    GB2312(1980年)一共收錄了7445個字符,包括6763個漢字和682個其它符號。漢字區的內碼范圍高字節從B0-F7,低字節從A1-FE,占用的碼位是72*94=6768。其中有5個空位是D7FA-D7FE。
    GB2312支持的漢字太少。1995年的漢字擴展規范GBK1.0收錄了21886個符號,它分為漢字區和圖形符號區。漢字區包括21003個字符。2000年的GB18030是取代GBK1.0的正式國家標準。該標準收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。現在的PC平臺必須支持GB18030,對嵌入式產品暫不作要求。所以手機、MP3一般只支持GB2312。
    從ASCII、GB2312、GBK到GB18030,這些編碼方法是向下兼容的,即同一個字符在這些方案中總是有相同的編碼,后面的標準支持更多的字符。在這些編碼中,英文和中文可以統一地處理。區分中文編碼的方法是高字節的最高位不為0。按照程序員的稱呼,GB2312、GBK到GB18030都屬于雙字節字符集 (DBCS)。
    有的中文Windows的缺省內碼還是GBK,可以通過GB18030升級包升級到GB18030。不過GB18030相對GBK增加的字符,普通人是很難用到的,通常我們還是用GBK指代中文Windows內碼。
    這里還有一些細節:
    GB2312的原文還是區位碼,從區位碼到內碼,需要在高字節和低字節上分別加上A0。
    在DBCS中,GB內碼的存儲格式始終是big endian,即高位在前。
    GB2312的兩個字節的最高位都是1。但符合這個條件的碼位只有128*128=16384個。所以GBK和GB18030的低字節最高位都可能不是1。不過這不影響DBCS字符流的解析:在讀取DBCS字符流時,只要遇到高位為1的字節,就可以將下兩個字節作為一個雙字節編碼,而不用管低字節的高位是什么。

    3、Unicode、UCS和UTF(UCS Transformation Format)
    前面提到從ASCII、GB2312、GBK到GB18030的編碼方法是向下兼容的。而Unicode只與ASCII兼容(更準確地說,是與ISO-8859-1兼容),與GB碼不兼容。例如“漢”字的Unicode編碼是6C49,而GB碼是BABA。

    ? UCS規定了怎么用多個字節表示各種文字。而怎樣傳輸這些編碼,是由UTF(UCS Transformation Format)規范規定的!常見的UTF規范包括UTF-8、UTF-7、UTF-16。

    4、UTF的字節序和BOM
    UTF-8以字節為編碼單元,沒有字節序的問題。UTF-16以兩個字節為編碼單元,在解釋一個UTF-16文本前,首先要弄清楚每個編碼單元的字節序。例如收到一個“奎”的Unicode編碼是594E,“乙”的Unicode編碼是4E59。如果我們收到UTF-16字節流“594E”,那么這是“奎”還是“乙”?
    Unicode規范中推薦的標記字節順序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一個有點小聰明的想法:
    在UCS編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的編碼是FEFF。而FFFE在UCS中是不存在的字符,所以不應該出現在實際傳輸中。UCS規范建議我們在傳輸字節流前,先傳輸字符"ZERO WIDTH NO-BREAK SPACE"。
    這樣如果接收者收到FEFF,就表明這個字節流是Big-Endian的;如果收到FFFE,就表明這個字節流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。
    UTF-8不需要BOM來表明字節順序,但可以用BOM來表明編碼方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8編碼是EF BB BF(讀者可以用我們前面介紹的編碼方法驗證一下)。所以如果接收者收到以EF BB BF開頭的字節流,就知道這是UTF-8編碼了。
    Windows就是使用BOM來標記文本文件的編碼方式的。


    ? 寫到這里對編碼有了大致的了解了,就可以理解網上一些文章的話了,比如有一篇很流行的文章《URL編碼與SQL注射》里面有一段是這么說的:

    其實url編碼就是一個字符ascii碼的十六進制。不過稍微有些變動,需要在前面加上“%”。比如“\”,它的ascii碼是92,92的十六進制是5c,所以“\”的url編碼就是%5c。那么漢字的url編碼呢?很簡單,看例子:“胡”的ascii碼是-17670,十六進制是BAFA,url編碼是“%BA%FA”。呵呵,知道怎么轉換的了吧。


    ?? 這得從ASCII說起,擴展的ASCII字符集采用8bit255個字符顯然不夠用,于是各個國家紛紛制定了自己的文字編碼規范,其中中文的文字編碼規范叫做“GB2312-80”(就是GB2312),它是和ASCII兼容的一種編碼規范,其實就是用擴展ASCII沒有真正標準化這一點,把一個中文字符用兩個擴展ASCII字符來表示。文中說的的中文ASCII碼實際上就是簡體中文的編碼2312GB!它把ASCII又擴充了一個字節,由于高位的第一位是0,所以會出現負數的形式,url編碼就是將漢字的這個GB2312編碼轉化成UTF-8的編碼并且每8位即一個字節前面加上%符號表示。

    那為何UTF-8是進行網絡的規范傳輸編碼呢?

    在Unicode里,所有的字符被一視同仁。漢字不再使用“兩個擴展ASCII”,而是使用“1個Unicode”,注意,現在的漢字是“一個字符”了,于是,拆字、統計字數這些問題也就自然而然的解決了。但是,這個世界不是理想的,不可能在一夜之間所有的系統都使用Unicode來處理字符,所以Unicode在誕生之日,就必須考慮一個嚴峻的問題:和ASCII字符集之間的不兼容問題。

    我們知道,ASCII字符是單個字節的,比如“A”的ASCII是65。而Unicode是雙字節的,比如“A”的Unicode是0065,這就造成了一個非常大的問題:以前處理ASCII的那套機制不能被用來處理Unicode了

    另一個更加嚴重的問題是,C語言使用'\0'作為字符串結尾,而Unicode里恰恰有很多字符都有一個字節為0,這樣一來,C語言的字符串函數將無法正常處理Unicode,除非把世界上所有用C寫的程序以及他們所用的函數庫全部換掉

    于是,比Unicode更偉大的東東誕生了,之所以說它更偉大是因為它讓Unicode不再存在于紙上,而是真實的存在于我們大家的電腦中。那就是:UTF

    UTF= UCS Transformation Format UCS轉換格式,它是將Unicode編碼規則和計算機的實際編碼對應起來的一個規則。現在流行的UTF有2種:UTF-8和UTF-16

    其中UTF-16和上面提到的Unicode本身的編碼規范是一致的,這里不多說了。而UTF-8不同,它定義了一種“區間規則”,這種規則可以和ASCII編碼保持最大程度的兼容,這樣做的好處是壓縮了字符在西歐一些國家的內存消耗,減少了不必要的資源浪費,這在實際應用中是非常有必要的。

    UTF-8有點類似于Haffman編碼,它將Unicode編碼為:
    00000000-0000007F的字符,用單個字節來表示;

    00000080-000007FF的字符用兩個字節表示? (中文的編碼范圍)

    00000800-0000FFFF的字符用3字節表示

    因為目前為止Unicode-16規范沒有指定FFFF以上的字符,所以UTF-8最多是使用3個字節來表示一個字符。但理論上來說,UTF-8最多需要用6字節表示一個字符。

    在UTF-8里,英文字符仍然跟ASCII編碼一樣,因此原先的函數庫可以繼續使用。而中文的編碼范圍是在0080-07FF之間,因此是2個字節表示(但這兩個字節和GB編碼的兩個字節是不同的)。


    看看編碼之多:ANSI,AscII,GB2312,GBK,BIG5,GB18030,Unicode,UCS(就是unicode)Utf-8,utf-16,utf-32 整整10種編碼~,算是夠復雜了
    可是這還僅僅是個開始,應用方面變化無窮,不過現在看到這些東西起碼再不會頭大了!呼呼~


    哦,漏了一個加密的base64編碼。

    什么是Base64?

    按照RFC2045的定義,Base64被定義為:Base64內容傳送編碼被設計用來把任意序列的8位字節描述為一種不易被人直接識別的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)

    為什么要使用Base64?

    在設計這個編碼的時候,我想設計人員最主要考慮了3個問題:
    1.是否加密?
    2.加密算法復雜程度和效率
    3.如何處理傳輸?

    ??? 加密是肯定的,但是加密的目的不是讓用戶發送非常安全的Email。這種加密方式主要就是“防君子不防小人”。即達到一眼望去完全看不出內容即可。
    基于這個目的加密算法的復雜程度和效率也就不能太大和太低。和上一個理由類似,MIME協議等用于發送Email的協議解決的是如何收發Email,而并不是如何安全的收發Email。因此算法的復雜程度要小,效率要高,否則因為發送Email而大量占用資源,路就有點走歪了。

    ??? 但是,如果是基于以上兩點,那么我們使用最簡單的愷撒法即可,為什么Base64看起來要比愷撒法復雜呢?這是因為在Email的傳送過程中,由于歷史原因,Email只被允許傳送ASCII字符,即一個8位字節的低7位。因此,如果您發送了一封帶有非ASCII字符(即字節的最高位是1)的Email通過有“歷史問題”的網關時就可能會出現問題。網關可能會把最高位置為0!很明顯,問題就這樣產生了!因此,為了能夠正常的傳送Email,這個問題就必須考慮!所以,單單靠改變字母的位置的愷撒之類的方案也就不行了。關于這一點可以參考RFC2046。
    基于以上的一些主要原因產生了Base64編碼。

    鑒于算法比較讓人頭大,想看的人自然會有看到的辦法拉,俺是頭大得很,就不放上來了。

    posted @ 2006-12-12 11:27 保爾任 閱讀(371) | 評論 (0)編輯 收藏

    一、正則表達式基礎知識:(此文講的是符合perl的正則表達式匹配方法,與jdk1.4上的不一樣,但講的很清晰,可作為基礎知識講解看)

    如果你曾經用過Perl或任何其他內建正則表達式支持的語言,你一定知道用正則表達式處理文本和匹配模式是多么簡單。如果你不熟悉這個術語,那么“正則表達式”(Regular Expression)就是一個字符構成的串,它定義了一個用來搜索匹配字符串的模式。
    許多語言,包括Perl、PHP、Python、JavaScript和JScript,都支持用正則表達式處理文本,一些文本編輯器用正則表達式實現高級“搜索-替換”功能。那么Java又怎樣呢?本文寫作時,一個包含了用正則表達式進行文本處理的Java規范需求(Specification Request)已經得到認可,你可以期待在JDK的下一版本中看到它。
    然而,如果現在就需要使用正則表達式,又該怎么辦呢?你可以從Apache.org下載源代碼開放的Jakarta-ORO庫。本文接下來的內容先簡要地介紹正則表達式的入門知識,然后以Jakarta-ORO API為例介紹如何使用正則表達式。
    一、正則表達式基礎知識
    我們先從簡單的開始。假設你要搜索一個包含字符“cat”的字符串,搜索用的正則表達式就是“cat”。如果搜索對大小寫不敏感,單詞“catalog”、“Catherine”、“sophisticated”都可以匹配。也就是說:
    1.1 句點符號
    假設你在玩英文拼字游戲,想要找出三個字母的單詞,而且這些單詞必須以“t”字母開頭,以“n”字母結束。另外,假設有一本英文字典,你可以用正則表達式搜索它的全部內容。要構造出這個正則表達式,你可以使用一個通配符——句點符號“.”。這樣,完整的表達式就是“t.n”,它匹配“tan”、“ten”、“tin”和“ton”,還匹配“t#n”、“tpn”甚至“t n”,還有其他許多無意義的組合。這是因為句點符號匹配所有字符,包括空格、Tab字符甚至換行符:
    1.2 方括號符號
    為了解決句點符號匹配范圍過于廣泛這一問題,你可以在方括號(“[]”)里面指定看來有意義的字符。此時,只有方括號里面指定的字符才參與匹配。也就是說,正則表達式“t[aeio]n”只匹配“tan”、“Ten”、“tin”和“ton”。但“Toon”不匹配,因為在方括號之內你只能匹配單個字符:
    1.3 “或”符號
    如果除了上面匹配的所有單詞之外,你還想要匹配“toon”,那么,你可以使用“|”操作符。“|”操作符的基本意義就是“或”運算。要匹配“toon”,使用“t(a|e|i|o|oo)n”正則表達式。這里不能使用方擴號,因為方括號只允許匹配單個字符;這里必須使用圓括號“()”。圓括號還可以用來分組,具體請參見后面介紹。
    1.4 表示匹配次數的符號
    表一顯示了表示匹配次數的符號,這些符號用來確定緊靠該符號左邊的符號出現的次數:

    假設我們要在文本文件中搜索美國的社會安全號碼。這個號碼的格式是999-99-9999。用來匹配它的正則表達式如圖一所示。在正則表達式中,連字符(“-”)有著特殊的意義,它表示一個范圍,比如從0到9。因此,匹配社會安全號碼中的連字符號時,它的前面要加上一個轉義字符“\”。

    圖一:匹配所有123-12-1234形式的社會安全號碼

    假設進行搜索的時候,你希望連字符號可以出現,也可以不出現——即,999-99-9999和999999999都屬于正確的格式。這時,你可以在連字符號后面加上“?”數量限定符號,如圖二所示:

    圖二:匹配所有123-12-1234和123121234形式的社會安全號碼

    下面我們再來看另外一個例子。美國汽車牌照的一種格式是四個數字加上二個字母。它的正則表達式前面是數字部分“[0-9]{4}”,再加上字母部分“[A-Z]{2}”。圖三顯示了完整的正則表達式。

    圖三:匹配典型的美國汽車牌照號碼,如8836KV

    1.5 “否”符號
    “^”符號稱為“否”符號。如果用在方括號內,“^”表示不想要匹配的字符。例如,圖四的正則表達式匹配所有單詞,但以“X”字母開頭的單詞除外。

    圖四:匹配所有單詞,但“X”開頭的除外

    1.6 圓括號和空白符號
    假設要從格式為“June 26, 1951”的生日日期中提取出月份部分,用來匹配該日期的正則表達式可以如圖五所示:

    圖五:匹配所有Moth DD,YYYY格式的日期

    新出現的“\s”符號是空白符號,匹配所有的空白字符,包括Tab字符。如果字符串正確匹配,接下來如何提取出月份部分呢?只需在月份周圍加上一個圓括號創建一個組,然后用ORO API(本文后面詳細討論)提取出它的值。修改后的正則表達式如圖六所示:

    圖六:匹配所有Month DD,YYYY格式的日期,定義月份值為第一個組

    1.7 其它符號
    為簡便起見,你可以使用一些為常見正則表達式創建的快捷符號。如表二所示:
    表二:常用符號

    例如,在前面社會安全號碼的例子中,所有出現“[0-9]”的地方我們都可以使用“\d”。修改后的正則表達式如圖七所示:

    圖七:匹配所有123-12-1234格式的社會安全號碼

    -------------------------
    二、正則表達式在java中
    應用
    (java編程思想第三版P565頁有講解)
    簡介:

    java.util.regex是一個用正則表達式所訂制的模式來對字符串進行匹配工作的類庫包。

    它包括兩個類: PatternMatcher

    Pattern一個Pattern是一個正則表達式經編譯后的表現模式。
    Matcher一個Matcher對象是一個狀態機器,它依據Pattern對象做為匹配模式對字符串展開匹配檢查。

    首先一個Pattern實例訂制了一個所用語法與PERL的類似的正則表達式經編譯后的模式,然后一個Matcher實例在這個給定的Pattern實例的模式控制下進行字符串的匹配工作。

    以下我們就分別來看看這兩個類:



    Pattern類:

    Pattern的方法如下:

    static Patterncompile(String regex)
    將給定的正則表達式編譯并賦予給Pattern類
    static Patterncompile(String regex, int flags)
    同上,但增加flag參數的指定,可選的flag參數包括:CASE INSENSITIVE,MULTILINE,DOTALL,UNICODE CASE, CANON EQ
    intflags()
    返回當前Pattern的匹配flag參數.
    Matchermatcher(CharSequence input)
    生成一個給定命名的Matcher對象
    static booleanmatches(String regex, CharSequence input)
    編譯給定的正則表達式并且對輸入的字串以該正則表達式為模開展匹配,該方法適合于該正則表達式只會使用一次的情況,也就是只進行一次匹配工作,因為這種情況下并不需要生成一個Matcher實例。
    Stringpattern()
    返回該Patter對象所編譯的正則表達式。
    String[]split(CharSequence input)
    將目標字符串按照Pattern里所包含的正則表達式為模進行分割。
    String[]split(CharSequence input, int limit)
    作用同上,增加參數limit目的在于要指定分割的段數,如將limi設為2,那么目標字符串將根據正則表達式分為割為兩段。

    一個正則表達式,也就是一串有特定意義的字符,必須首先要編譯成為一個Pattern類的實例,這個Pattern對象將會使用 matcher()方法來生成一個Matcher實例,接著便可以使用該 Matcher實例以編譯的正則表達式為基礎對目標字符串進行匹配工作,多個Matcher是可以共用一個Pattern對象的。

    現在我們先來看一個簡單的例子,再通過分析它來了解怎樣生成一個Pattern對象并且編譯一個正則表達式,最后根據這個正則表達式將目標字符串進行分割:

    import java.util.regex.*;
    public class Replacement{ public static void main(String[] args) throws Exception { // 生成一個Pattern,同時編譯一個正則表達式 Pattern p = Pattern.compile("[/]+"); //用Pattern的split()方法把字符串按"/"分割 String[] result = p.split( "Kevin has seen《LEON》seveal times,because it is a good film." +"/ 凱文已經看過《這個殺手不太冷》幾次了,因為它是一部" +"好電影。/名詞:凱文。"); for (int i=0; i<result.length; i++) System.out.println(result[i]); } }

    輸出結果為:

    Kevin has seen《LEON》seveal times,because it is a good film.
    凱文已經看過《這個殺手不太冷》幾次了,因為它是一部好電影。
    名詞:凱文。
    

    很明顯,該程序將字符串按"/"進行了分段,我們以下再使用 split(CharSequence input, int limit)方法來指定分段的段數,程序改動為:
    tring[] result = p.split("Kevin has seen《LEON》seveal times,because it is a good film./ 凱文已經看過《這個殺手不太冷》幾次了,因為它是一部好電影。/名詞:凱文。",2);

    這里面的參數"2"表明將目標語句分為兩段。

    輸出結果則為:

    Kevin has seen《LEON》seveal times,because it is a good film.
    凱文已經看過《這個殺手不太冷》幾次了,因為它是一部好電影。/名詞:凱文。

    由上面的例子,我們可以比較出java.util.regex包在構造Pattern對象以及編譯指定的正則表達式的實現手法與我們在上一篇中所介紹的Jakarta-ORO 包在完成同樣工作時的差別,Jakarta-ORO 包要先構造一個PatternCompiler類對象接著生成一個Pattern對象,再將正則表達式用該PatternCompiler類的compile()方法來將所需的正則表達式編譯賦予Pattern類:

    PatternCompiler orocom=new Perl5Compiler();

    Pattern pattern=orocom.compile("REGULAR EXPRESSIONS");

    PatternMatcher matcher=new Perl5Matcher();

    但是在java.util.regex包里,我們僅需生成一個Pattern類,直接使用它的compile()方法就可以達到同樣的效果: Pattern p = Pattern.compile("[/]+");

    因此似乎java.util.regex的構造法比Jakarta-ORO更為簡潔并容易理解。





    回頁首


    Matcher類:

    Matcher方法如下:

    MatcherappendReplacement(StringBuffer sb, String replacement)
    將當前匹配子串替換為指定字符串,并且將替換后的子串以及其之前到上次匹配子串之后的字符串段添加到一個StringBuffer對象里。
    StringBufferappendTail(StringBuffer sb)
    將最后一次匹配工作后剩余的字符串添加到一個StringBuffer對象里。
    intend()
    返回當前匹配的子串的最后一個字符在原目標字符串中的索引位置 。
    intend(int group)
    返回與匹配模式里指定的組相匹配的子串最后一個字符的位置。
    booleanfind()
    嘗試在目標字符串里查找下一個匹配子串。
    booleanfind(int start)
    重設Matcher對象,并且嘗試在目標字符串里從指定的位置開始查找下一個匹配的子串。
    Stringgroup()
    返回當前查找而獲得的與組匹配的所有子串內容
    Stringgroup(int group)
    返回當前查找而獲得的與指定的組匹配的子串內容
    intgroupCount()
    返回當前查找所獲得的匹配組的數量。
    booleanlookingAt()
    檢測目標字符串是否以匹配的子串起始。
    booleanmatches()
    嘗試對整個目標字符展開匹配檢測,也就是只有整個目標字符串完全匹配時才返回真值。
    Patternpattern()
    返回該Matcher對象的現有匹配模式,也就是對應的Pattern 對象。
    StringreplaceAll(String replacement)
    將目標字符串里與既有模式相匹配的子串全部替換為指定的字符串。
    StringreplaceFirst(String replacement)
    將目標字符串里第一個與既有模式相匹配的子串替換為指定的字符串。
    Matcherreset()
    重設該Matcher對象。
    Matcherreset(CharSequence input)
    重設該Matcher對象并且指定一個新的目標字符串。
    intstart()
    返回當前查找所獲子串的開始字符在原目標字符串中的位置。
    intstart(int group)
    返回當前查找所獲得的和指定組匹配的子串的第一個字符在原目標字符串中的位置。

    (光看方法的解釋是不是很不好理解?不要急,待會結合例子就比較容易明白了)

    一個Matcher實例是被用來對目標字符串進行基于既有模式(也就是一個給定的Pattern所編譯的正則表達式)進行匹配查找的,所有往Matcher的輸入都是通過CharSequence接口提供的,這樣做的目的在于可以支持對從多元化的數據源所提供的數據進行匹配工作。

    我們分別來看看各方法的使用:

    ★matches()/lookingAt ()/find():
    一個Matcher對象是由一個Pattern對象調用其matcher()方法而生成的,一旦該Matcher對象生成,它就可以進行三種不同的匹配查找操作:

    1. matches()方法嘗試對整個目標字符展開匹配檢測,也就是只有整個目標字符串完全匹配時才返回真值。
    2. lookingAt ()方法將檢測目標字符串是否以匹配的子串起始。
    3. find()方法嘗試在目標字符串里查找下一個匹配子串。

    以上三個方法都將返回一個布爾值來表明成功與否。

    ★replaceAll ()/appendReplacement()/appendTail():
    Matcher類同時提供了四個將匹配子串替換成指定字符串的方法:

    1. replaceAll()
    2. replaceFirst()
    3. appendReplacement()
    4. appendTail()

    replaceAll()與replaceFirst()的用法都比較簡單,請看上面方法的解釋。我們主要重點了解一下appendReplacement()和appendTail()方法。

    appendReplacement(StringBuffer sb, String replacement) 將當前匹配子串替換為指定字符串,并且將替換后的子串以及其之前到上次匹配子串之后的字符串段添加到一個StringBuffer對象里,而appendTail(StringBuffer sb) 方法則將最后一次匹配工作后剩余的字符串添加到一個StringBuffer對象里。

    例如,有字符串fatcatfatcatfat,假設既有正則表達式模式為"cat",第一次匹配后調用appendReplacement(sb,"dog"),那么這時StringBuffer sb的內容為fatdog,也就是fatcat中的cat被替換為dog并且與匹配子串前的內容加到sb里,而第二次匹配后調用appendReplacement(sb,"dog"),那么sb的內容就變為fatdogfatdog,如果最后再調用一次appendTail(sb),那么sb最終的內容將是fatdogfatdogfat。

    還是有點模糊?那么我們來看個簡單的程序:

    //該例將把句子里的"Kelvin"改為"Kevin"
    import java.util.regex.*;
    public class MatcherTest{
        public static void main(String[] args) 
                             throws Exception {
            //生成Pattern對象并且編譯一個簡單的正則表達式"Kelvin"
            Pattern p = Pattern.compile("Kevin");
            //用Pattern類的matcher()方法生成一個Matcher對象
            Matcher m = p.matcher("Kelvin Li and Kelvin Chan are both working 
    in Kelvin Chen's KelvinSoftShop company"); StringBuffer sb = new StringBuffer(); int i=0; //使用find()方法查找第一個匹配的對象 boolean result = m.find(); //使用循環將句子里所有的kelvin找出并替換再將內容加到sb里 while(result) { i++; m.appendReplacement(sb, "Kevin"); System.out.println("第"+i+"次匹配后sb的內容是:"+sb); //繼續查找下一個匹配對象 result = m.find(); } //最后調用appendTail()方法將最后一次匹配后的剩余字符串加到sb里; m.appendTail(sb); System.out.println("調用m.appendTail(sb)后sb的最終內容是:"+ sb.toString()); } }

    最終輸出結果為:
    第1次匹配后sb的內容是:Kevin
    第2次匹配后sb的內容是:Kevin Li and Kevin
    第3次匹配后sb的內容是:Kevin Li and Kevin Chan are both working in Kevin
    第4次匹配后sb的內容是:Kevin Li and Kevin Chan are both working in Kevin Chen's Kevin
    調用m.appendTail(sb)后sb的最終內容是:Kevin Li and Kevin Chan are both working in Kevin Chen's KevinSoftShop company.

    看了上面這個例程是否對appendReplacement(),appendTail()兩個方法的使用更清楚呢,如果還是不太肯定最好自己動手寫幾行代碼測試一下。

    ★group()/group(int group)/groupCount():
    該系列方法與我們在上篇介紹的Jakarta-ORO中的MatchResult .group()方法類似(有關Jakarta-ORO請參考上篇的內容),都是要返回與組匹配的子串內容,下面代碼將很好解釋其用法:

    import java.util.regex.*;
    
    public class GroupTest{
        public static void main(String[] args) 
                             throws Exception {
            Pattern p = Pattern.compile("(ca)(t)");        
            Matcher m = p.matcher("one cat,two cats in the yard");
            StringBuffer sb = new StringBuffer();
            boolean result = m.find();
            System.out.println("該次查找獲得匹配組的數量為:"+m.groupCount());
            for(int i=1;i<=m.groupCount();i++){
             System.out.println("第"+i+"組的子串內容為: "+m.group(i));
            }
        }
    }

    輸出為:
    該次查找獲得匹配組的數量為:2
    第1組的子串內容為:ca
    第2組的子串內容為:t

    Matcher對象的其他方法因比較好理解且由于篇幅有限,請讀者自己編程驗證。





    回頁首


    一個檢驗Email地址的小程序:

    最后我們來看一個檢驗Email地址的例程,該程序是用來檢驗一個輸入的EMAIL地址里所包含的字符是否合法,雖然這不是一個完整的EMAIL地址檢驗程序,它不能檢驗所有可能出現的情況,但在必要時您可以在其基礎上增加所需功能。

    import java.util.regex.*;
    public class Email {
       public static void main(String[] args) throws Exception {
          String input = args[0];
          //檢測輸入的EMAIL地址是否以 非法符號"."或"@"作為起始字符      
          Pattern p = Pattern.compile("^\\.|^\\@");
          Matcher m = p.matcher(input);
          if (m.find()){
            System.err.println("EMAIL地址不能以'.'或'@'作為起始字符");
          }
          //檢測是否以"www."為起始
          p = Pattern.compile("^www\\.");
          m = p.matcher(input);
          if (m.find()) {
            System.out.println("EMAIL地址不能以'www.'起始");
          }
          //檢測是否包含非法字符
          p = Pattern.compile("[^A-Za-z0-9\\.\\@_\\-~#]+");
          m = p.matcher(input);
          StringBuffer sb = new StringBuffer();
          boolean result = m.find();
          boolean deletedIllegalChars = false;
          while(result) {
             //如果找到了非法字符那么就設下標記
             deletedIllegalChars = true;
             //如果里面包含非法字符如冒號雙引號等,那么就把他們消去,加到SB里面
             m.appendReplacement(sb, "");
             result = m.find();
          }
          m.appendTail(sb);
          input = sb.toString();
          if (deletedIllegalChars) {
              System.out.println("輸入的EMAIL地址里包含有冒號、逗號等非法字符,請修改");
              System.out.println("您現在的輸入為: "+args[0]);
              System.out.println("修改后合法的地址應類似: "+input);
         }
       }
    }

    例如,我們在命令行輸入:java Email www.kevin@163.net

    那么輸出結果將會是:EMAIL地址不能以'www.'起始

    如果輸入的EMAIL為@kevin@163.net

    則輸出為:EMAIL地址不能以'.'或'@'作為起始字符

    當輸入為:cgjmail#$%@163.net

    那么輸出就是:

    輸入的EMAIL地址里包含有冒號、逗號等非法字符,請修改
    您現在的輸入為: cgjmail#$%@163.net
    修改后合法的地址應類似: cgjmail@163.net
    posted @ 2006-12-12 11:26 保爾任 閱讀(313) | 評論 (0)編輯 收藏
    當年,國際巨星成龍的「龍種」曝光,眾人指責他對不起嬌妻林鳳嬌,逼得他出面召開記者會,向世人自白他犯了「全世界所有男人都會犯的錯誤」。從來沒犯過這種錯誤的我,也因此常常認為自己不是個男人。
    雖然沒犯過「全世界所有男人都會犯的錯誤」,但是我倒是曾經犯了「全世界所有程序員都會犯的錯誤」。不管使用何種語言,全世界所有程序員都一定犯過這種錯誤,那就是:太依賴編譯器,卻不知道編譯器做了哪些事。
    一般來說,越高階的程序語言,會提供越多語法上的便利,以方便程序撰寫,這就俗稱為syntactic sugar,我稱其為「語法上的甜頭」。雖說是甜頭,但是如果你未能了解該語法的實質內涵,很可能會未嘗甜頭,卻吃盡苦頭。
    不久前,我收到一個電子郵件,讀者列出下面的Java程序,向我求救。看過這個程序之后,我確定這又是一個「全世界所有程序員都會犯的錯誤」。
    // 程序1
    class Singleton {
    private static Singleton obj = new Singleton();
    public static int counter1;
    public static int counter2 = 0;
    private Singleton() {
    counter1++;
    counter2++;
    }
    public static Singleton getInstance() {
    return obj;
    }
    }
    // 程序2
    public class MyMain {
    public static void main(String[] args) {
    Singleton obj = Singleton.getInstance();
    System.out.println("obj.counter1=="+obj.counter1);
    System.out.println("obj.counter2=="+obj.counter2);
    }
    }
    執行結果是:
    obj.counter1==1
    obj.counter2==0
    你有沒有被此結果嚇一跳?乍看程序代碼,你很可能會認為counter1和counter2的值一定會相等,但執行結果顯然不是如此。其實,程序1被編譯后的程序應該等同于下面的程序3:
    // 程序3
    class Singleton {
    private static Singleton obj;
    public static int counter1;
    public static int counter2;
    static { // 這就是class constructor
    // 在進入此class constructor之前,class已經被JVM
    // 配置好內存,所有的static field都會被先設定為0,
    // 所以此時counter1和counter2都已經是0,且singleton為null
    obj = new Singleton(); // 問題皆由此行程序產生
    // counter1不會在此被設定為0
    counter2 = 0; // counter2再被設定一次0(其實是多此一舉)
    }
    private Singleton() { // 這是instance constructor
    counter1++;
    counter2++;
    }
    public static Singleton getInstance() {
    return obj;
    }
    }
    這是因為:當class具有static field,且直接在宣告處透過「=...」的方式設定其值時,編譯器會自動將這些敘述依序搬到class constructor內。同樣地,當class具有instance field,且直接在宣告處透過「=...」的方式設定其值時,編譯器會自動將這些敘述依序搬到instance constructor內。
    此程序在class constructor內,還未將static field初始化時(這時候,counter1和counter2都是0),就呼叫instance constructor,而instance constructor竟然還會去更動static field的值,使得counter1和counter2都變成1。然后instance constructor執行完,回到class constructor,再把counter2的值設為0(但是
    counter1維持不變)。最后的結果:counter1等于1,counter2等于0。
    欲改正程序1,方法有三:
    -方法一:將singleton field的宣告調到counter1與counter2 field之后。
    這是最好的作法。
    -方法二:將counter2=0的宣告中,「=0」的部分刪除。這種作法只有在希望
    -方法三:將初始化的動作搬到class constructors內,自行撰寫,而不依賴
    編譯器產生。這是最保險的作法。
    如何避免犯下「全世界所有程序員都會犯的錯誤」,我給各位Java程序員
    的建議是:
    -熟讀Java Language Specification
    -在有疑問時,使用J2SDK所提供的javap來反組譯Java Bytecode,直接觀察
    編譯后的結果。
    下面是我用javap來反組譯程序1的示范:
    C:\>javap -c -classpath . Singleton
    Compiled from MyMain.java
    class Singleton extends java.lang.Object {
    public static int counter1;
    public static int counter2;
    public static Singleton getInstance();
    static {};
    }
    Method Singleton()
    0 aload_0
    1 invokespecial #1 <Method java.lang.Object()>
    4 getstatic #2 <Field int counter1>
    7 iconst_1
    8 iadd
    9 putstatic #2 <Field int counter1>
    12 getstatic #3 <Field int counter2>
    15 iconst_1
    16 iadd
    17 putstatic #3 <Field int counter2>
    20 return
    Method Singleton getInstance()
    0 getstatic #4 <Field Singleton obj>
    3 areturn
    Method static {}
    0 new #5 <Class Singleton>
    3 dup
    4 invokespecial #6 <Method Singleton()>
    7 putstatic #4 <Field Singleton obj>
    10 iconst_0
    11 putstatic #3 <Field int counter2>
    14 return
    其實Java的syntactic sugar并不算多,C#的syntactic sugar才真的是無所不在,
    也因此C#的初學者更容易犯了「全世界所有程序員都會犯的錯誤」。許多C#的書都會一邊介紹C#語法,一邊介紹編譯之后MSIL(.NET的中間語言,類似Java的Bytecode)的結果,然而Java的書卻鮮少這么做。
    雖說是「全世界所有程序員都會犯的錯誤」,但是這不代表你犯了此錯誤之后,仍可以同愛借錢的曹啟泰一般地「抬頭挺胸、理直氣壯」。只要有心,其實這一類的錯誤仍是可以避免的。
    posted @ 2006-12-12 11:25 保爾任 閱讀(217) | 評論 (0)編輯 收藏

    轉自:http://china.manufacturer.com/article/study_for_character_encoding_java.htm

    問題研究

    --字符集編碼

    1. 概述

    本文主要包括以下幾個方面:編碼基本知識,java,系統軟件,url,工具軟件等。

    在下面的描述中,將以"中文"兩個字為例,經查表可以知道其GB2312編碼是"d6d0 cec4",Unicode編碼為"4e2d 6587",UTF編碼就是"e4b8ad e69687"。注意,這兩個字沒有iso8859-1編碼,但可以用iso8859-1編碼來"表示"。

    2. 編碼基本知識

    最早的編碼是iso8859-1,和ascii編碼相似。但為了方便表示各種各樣的語言,逐漸出現了很多標準編碼,重要的有如下幾個。

    2.1. iso8859-1

    屬于單字節編碼,最多能表示的字符范圍是0-255,應用于英文系列。比如,字母'a'的編碼為0x61=97。

    很明顯,iso8859-1編碼表示的字符范圍很窄,無法表示中文字符。但是,由于是單字節編碼,和計算機最基礎的表示單位一致,所以很多時候,仍舊使用iso8859-1編碼來表示。而且在很多協議上,默認使用該編碼。比如,雖然"中文"兩個字不存在iso8859-1編碼,以gb2312編碼為例,應該是"d6d0 cec4"兩個字符,使用iso8859-1編碼的時候則將它拆開為4個字節來表示:"d6 d0 ce c4"(事實上,在進行存儲的時候,也是以字節為單位處理的)。而如果是UTF編碼,則是6個字節"e4 b8 ad e6 96 87"。很明顯,這種表示方法還需要以另一種編碼為基礎。

    2.2. GB2312/GBK

    這就是漢子的國標碼,專門用來表示漢字,是雙字節編碼,而英文字母和iso8859-1一致(兼容iso8859-1編碼)。其中gbk編碼能夠用來同時表示繁體字和簡體字,而gb2312只能表示簡體字,gbk是兼容gb2312編碼的。

    2.3. unicode

    這是最統一的編碼,可以用來表示所有語言的字符,而且是定長雙字節(也有四字節的)編碼,包括英文字母在內。所以可以說它是不兼容iso8859-1編碼的,也不兼容任何編碼。不過,相對于iso8859-1編碼來說,uniocode編碼只是在前面增加了一個0字節,比如字母'a'為"00 61"。

    需要說明的是,定長編碼便于計算機處理(注意GB2312/GBK不是定長編碼),而unicode又可以用來表示所有字符,所以在很多軟件內部是使用unicode編碼來處理的,比如java。

    2.4. UTF

    考慮到unicode編碼不兼容iso8859-1編碼,而且容易占用更多的空間:因為對于英文字母,unicode也需要兩個字節來表示。所以unicode不便于傳輸和存儲。因此而產生了utf編碼,utf編碼兼容iso8859-1編碼,同時也可以用來表示所有語言的字符,不過,utf編碼是不定長編碼,每一個字符的長度從1-6個字節不等。另外,utf編碼自帶簡單的校驗功能。一般來講,英文字母都是用一個字節表示,而漢字使用三個字節。

    注意,雖然說utf是為了使用更少的空間而使用的,但那只是相對于unicode編碼來說,如果已經知道是漢字,則使用GB2312/GBK無疑是最節省的。不過另一方面,值得說明的是,雖然utf編碼對漢字使用3個字節,但即使對于漢字網頁,utf編碼也會比unicode編碼節省,因為網頁中包含了很多的英文字符。

    3. java對字符的處理

    在java應用軟件中,會有多處涉及到字符集編碼,有些地方需要進行正確的設置,有些地方需要進行一定程度的處理。

    3.1. getBytes(charset)

    這是java字符串處理的一個標準函數,其作用是將字符串所表示的字符按照charset編碼,并以字節方式表示。注意字符串在java內存中總是按unicode編碼存儲的。比如"中文",正常情況下(即沒有錯誤的時候)存儲為"4e2d 6587",如果charset為"gbk",則被編碼為"d6d0 cec4",然后返回字節"d6 d0 ce c4"。如果charset為"utf8"則最后是"e4 b8 ad e6 96 87"。如果是"iso8859-1",則由于無法編碼,最后返回 "3f 3f"(兩個問號)。

    3.2. new String(charset)

    這是java字符串處理的另一個標準函數,和上一個函數的作用相反,將字節數組按照charset編碼進行組合識別,最后轉換為unicode存儲。參考上述getBytes的例子,"gbk" 和"utf8"都可以得出正確的結果"4e2d 6587",但iso8859-1最后變成了"003f 003f"(兩個問號)。

    因為utf8可以用來表示/編碼所有字符,所以new String( str.getBytes( "utf8" ), "utf8" ) === str,即完全可逆。

    3.3. setCharacterEncoding()

    該函數用來設置http請求或者相應的編碼。

    對于request,是指提交內容的編碼,指定后可以通過getParameter()則直接獲得正確的字符串,如果不指定,則默認使用iso8859-1編碼,需要進一步處理。參見下述"表單輸入"。值得注意的是在執行setCharacterEncoding()之前,不能執行任何getParameter()。java doc上說明:This method must be called prior to reading request parameters or reading input using getReader()。而且,該指定只對POST方法有效,對GET方法無效。分析原因,應該是在執行第一個getParameter()的時候,java將會按照編碼分析所有的提交內容,而后續的getParameter()不再進行分析,所以setCharacterEncoding()無效。而對于GET方法提交表單是,提交的內容在URL中,一開始就已經按照編碼分析所有的提交內容,setCharacterEncoding()自然就無效。

    對于response,則是指定輸出內容的編碼,同時,該設置會傳遞給瀏覽器,告訴瀏覽器輸出內容所采用的編碼。

    3.4. 處理過程

    下面分析兩個有代表性的例子,說明java對編碼有關問題的處理方法。

    3.4.1. 表單輸入

    User input  *(gbk:d6d0 cec4)  browser  *(gbk:d6d0 cec4)  web server  iso8859-1(00d6 00d 000ce 00c4)  class,需要在class中進行處理:getbytes("iso8859-1")為d6 d0 ce c4,new String("gbk")為d6d0 cec4,內存中以unicode編碼則為4e2d 6587

    l 用戶輸入的編碼方式和頁面指定的編碼有關,也和用戶的操作系統有關,所以是不確定的,上例以gbk為例。

    l 從browser到web server,可以在表單中指定提交內容時使用的字符集,否則會使用頁面指定的編碼。而如果在url中直接用?的方式輸入參數,則其編碼往往是操作系統本身的編碼,因為這時和頁面無關。上述仍舊以gbk編碼為例。

    l Web server接收到的是字節流,默認時(getParameter)會以iso8859-1編碼處理之,結果是不正確的,所以需要進行處理。但如果預先設置了編碼(通過request. setCharacterEncoding ()),則能夠直接獲取到正確的結果。

    l 在頁面中指定編碼是個好習慣,否則可能失去控制,無法指定正確的編碼。

    3.4.2. 文件編譯

    假設文件是gbk編碼保存的,而編譯有兩種編碼選擇:gbk或者iso8859-1,前者是中文windows的默認編碼,后者是linux的默認編碼,當然也可以在編譯時指定編碼。

    Jsp  *(gbk:d6d0 cec4)  java file  *(gbk:d6d0 cec4)  compiler read  uincode(gbk: 4e2d 6587; iso8859-1: 00d6 00d 000ce 00c4)  compiler write  utf(gbk: e4b8ad e69687; iso8859-1: *)  compiled file  unicode(gbk: 4e2d 6587; iso8859-1: 00d6 00d 000ce 00c4)  class。所以用gbk編碼保存,而用iso8859-1編譯的結果是不正確的。

    class  unicode(4e2d 6587)  system.out / jsp.out  gbk(d6d0 cec4)  os console / browser。

    l 文件可以以多種編碼方式保存,中文windows下,默認為ansi/gbk。

    l 編譯器讀取文件時,需要得到文件的編碼,如果未指定,則使用系統默認編碼。一般class文件,是以系統默認編碼保存的,所以編譯不會出問題,但對于jsp文件,如果在中文windows下編輯保存,而部署在英文linux下運行/編譯,則會出現問題。所以需要在jsp文件中用pageEncoding指定編碼。

    l Java編譯的時候會轉換成統一的unicode編碼處理,最后保存的時候再轉換為utf編碼。

    l 當系統輸出字符的時候,會按指定編碼輸出,對于中文windows下,System.out將使用gbk編碼,而對于response(瀏覽器),則使用jsp文件頭指定的contentType,或者可以直接為response指定編碼。同時,會告訴browser網頁的編碼。如果未指定,則會使用iso8859-1編碼。對于中文,應該為browser指定輸出字符串的編碼。

    l browser顯示網頁的時候,首先使用response中指定的編碼(jsp文件頭指定的contentType最終也反映在response上),如果未指定,則會使用網頁中meta項指定中的contentType。

    3.5. 幾處設置

    對于web應用程序,和編碼有關的設置或者函數如下。

    3.5.1. jsp編譯

    指定文件的存儲編碼,很明顯,該設置應該置于文件的開頭。例如:<%@page pageEncoding="GBK"%>。另外,對于一般class文件,可以在編譯的時候指定編碼。

    3.5.2. jsp輸出

    指定文件輸出到browser是使用的編碼,該設置也應該置于文件的開頭。例如:<%@ page contentType="text/html; charset= GBK" %>。該設置和response.setCharacterEncoding("GBK")等效。

    3.5.3. meta設置

    指定網頁使用的編碼,該設置對靜態網頁尤其有作用。因為靜態網頁無法采用jsp的設置,而且也無法執行response.setCharacterEncoding()。例如:<META http-equiv="Content-Type" content="text/html; charset=GBK" />

    如果同時采用了jsp輸出和meta設置兩種編碼指定方式,則jsp指定的優先。因為jsp指定的直接體現在response中。

    需要注意的是,apache有一個設置可以給無編碼指定的網頁指定編碼,該指定等同于jsp的編碼指定方式,所以會覆蓋靜態網頁中的meta指定。所以有人建議關閉該設置。

    3.5.4. form設置

    當瀏覽器提交表單的時候,可以指定相應的編碼。例如:<form accept-charset= "gb2312">。一般不必不使用該設置,瀏覽器會直接使用網頁的編碼。

    4. 系統軟件

    下面討論幾個相關的系統軟件。

    4.1. mysql數據庫

    很明顯,要支持多語言,應該將數據庫的編碼設置成utf或者unicode,而utf更適合與存儲。但是,如果中文數據中包含的英文字母很少,其實unicode更為適合。

    數據庫的編碼可以通過mysql的配置文件設置,例如default-character-set=utf8。還可以在數據庫鏈接URL中設置,例如: useUnicode=true&characterEncoding=UTF-8。注意這兩者應該保持一致,在新的sql版本里,在數據庫鏈接URL里可以不進行設置,但也不能是錯誤的設置。

    4.2. apache

    appache和編碼有關的配置在httpd.conf中,例如AddDefaultCharset UTF-8。如前所述,該功能會將所有靜態頁面的編碼設置為UTF-8,最好關閉該功能。

    另外,apache還有單獨的模塊來處理網頁響應頭,其中也可能對編碼進行設置。

    4.3. linux默認編碼

    這里所說的linux默認編碼,是指運行時的環境變量。兩個重要的環境變量是LC_ALL和LANG,默認編碼會影響到java URLEncode的行為,下面有描述。

    建議都設置為"zh_CN.UTF-8"。

    4.4. 其它

    為了支持中文文件名,linux在加載磁盤時應該指定字符集,例如:mount /dev/hda5 /mnt/hda5/ -t ntfs -o iocharset=gb2312。

    另外,如前所述,使用GET方法提交的信息不支持request.setCharacterEncoding(),但可以通過tomcat的配置文件指定字符集,在tomcat的server.xml文件中,形如:<Connector ... URIEncoding="GBK"/>。這種方法將統一設置所有請求,而不能針對具體頁面進行設置,也不一定和browser使用的編碼相同,所以有時候并不是所期望的。

    5. URL地址

    URL地址中含有中文字符是很麻煩的,前面描述過使用GET方法提交表單的情況,使用GET方法時,參數就是包含在URL中。

    5.1. URL編碼

    對于URL中的一些特殊字符,瀏覽器會自動進行編碼。這些字符除了"/?&"等外,還包括unicode字符,比如漢子。這時的編碼比較特殊。

    IE有一個選項"總是使用UTF-8發送URL",當該選項有效時,IE將會對特殊字符進行UTF-8編碼,同時進行URL編碼。如果改選項無效,則使用默認編碼"GBK",并且不進行URL編碼。但是,對于URL后面的參數,則總是不進行編碼,相當于UTF-8選項無效。比如"中文.html?a=中文",當UTF-8選項有效時,將發送鏈接"%e4%b8%ad%e6%96%87.html?a=\x4e\x2d\x65\x87";而UTF-8選項無效時,將發送鏈接"\x4e\x2d\x65\x87.html?a=\x4e\x2d\x65\x87"。注意后者前面的"中文"兩個字只有4個字節,而前者卻有18個字節,這主要時URL編碼的原因。

    當web server(tomcat)接收到該鏈接時,將會進行URL解碼,即去掉"%",同時按照ISO8859-1編碼(上面已經描述,可以使用URLEncoding來設置成其它編碼)識別。上述例子的結果分別是"\ue4\ub8\uad\ue6\u96\u87.html?a=\u4e\u2d\u65\u87"和"\u4e\u2d\u65\u87.html?a=\u4e\u2d\u65\u87",注意前者前面的"中文"兩個字恢復成了6個字符。這里用"\u",表示是unicode。

    所以,由于客戶端設置的不同,相同的鏈接,在服務器上得到了不同結果。這個問題不少人都遇到,卻沒有很好的解決辦法。所以有的網站會建議用戶嘗試關閉UTF-8選項。不過,下面會描述一個更好的處理辦法。

    5.2. rewrite

    熟悉的人都知道,apache有一個功能強大的rewrite模塊,這里不描述其功能。需要說明的是該模塊會自動將URL解碼(去除%),即完成上述web server(tomcat)的部分功能。有相關文檔介紹說可以使用[NE]參數來關閉該功能,但我試驗并未成功,可能是因為版本(我使用的是apache 2.0.54)問題。另外,當參數中含有"?& "等符號的時候,該功能將導致系統得不到正常結果。

    rewrite本身似乎完全是采用字節處理的方式,而不考慮字符串的編碼,所以不會帶來編碼問題。

    5.3. URLEncode.encode()

    這是Java本身提供對的URL編碼函數,完成的工作和上述UTF-8選項有效時瀏覽器所做的工作相似。值得說明的是,java已經不贊成不指定編碼來使用該方法(deprecated)。應該在使用的時候增加編碼指定。

    當不指定編碼的時候,該方法使用系統默認編碼,這會導致軟件運行結果得不確定。比如對于"中文",當系統默認編碼為"gb2312"時,結果是"%4e%2d%65%87",而默認編碼為"UTF-8",結果卻是"%e4%b8%ad%e6%96%87",后續程序將難以處理。另外,這兒說的系統默認編碼是由運行tomcat時的環境變量LC_ALL和LANG等決定的,曾經出現過tomcat重啟后就出現亂碼的問題,最后才郁悶的發現是因為修改修改了這兩個環境變量。

    建議統一指定為"UTF-8"編碼,可能需要修改相應的程序。

    5.4. 一個解決方案

    上面說起過,因為瀏覽器設置的不同,對于同一個鏈接,web server收到的是不同內容,而軟件系統有無法知道這中間的區別,所以這一協議目前還存在缺陷。

    針對具體問題,不應該僥幸認為所有客戶的IE設置都是UTF-8有效的,也不應該粗暴的建議用戶修改IE設置,要知道,用戶不可能去記住每一個web server的設置。所以,接下來的解決辦法就只能是讓自己的程序多一點智能:根據內容來分析編碼是否UTF-8。

    比較幸運的是UTF-8編碼相當有規律,所以可以通過分析傳輸過來的鏈接內容,來判斷是否是正確的UTF-8字符,如果是,則以UTF-8處理之,如果不是,則使用客戶默認編碼(比如"GBK"),下面是一個判斷是否UTF-8的例子,如果你了解相應規律,就容易理解。

    public static boolean isValidUtf8(byte[] b,int aMaxCount){

           int lLen=b.length,lCharCount=0;

           for(int i=0;i<lLen && lCharCount<aMaxCount;++lCharCount){

                  byte lByte=b[i++];//to fast operation, ++ now, ready for the following for(;;)

                  if(lByte>=0) continue;//>=0 is normal ascii

                  if(lByte<(byte)0xc0 || lByte>(byte)0xfd) return false;

                  int lCount=lByte>(byte)0xfc?5:lByte>(byte)0xf8?4

                         :lByte>(byte)0xf0?3:lByte>(byte)0xe0?2:1;

                  if(i+lCount>lLen) return false;

                  for(int j=0;j<lCount;++j,++i) if(b[i]>=(byte)0xc0) return false;

           }

           return true;

    }

    相應地,一個使用上述方法的例子如下:

    public static String getUrlParam(String aStr,String aDefaultCharset)

    throws UnsupportedEncodingException{

           if(aStr==null) return null;

           byte[] lBytes=aStr.getBytes("ISO-8859-1");

           return new String(lBytes,StringUtil.isValidUtf8(lBytes)?"utf8":aDefaultCharset);

    }

    不過,該方法也存在缺陷,如下兩方面:

    l 沒有包括對用戶默認編碼的識別,這可以根據請求信息的語言來判斷,但不一定正確,因為我們有時候也會輸入一些韓文,或者其他文字。

    l 可能會錯誤判斷UTF-8字符,一個例子是"學習"兩個字,其GBK編碼是" \xd1\xa7\xcf\xb0",如果使用上述isValidUtf8方法判斷,將返回true。可以考慮使用更嚴格的判斷方法,不過估計效果不大。

    有一個例子可以證明google也遇到了上述問題,而且也采用了和上述相似的處理方法,比如,如果在地址欄中輸入"

    最后,應該補充說明一下,如果不使用rewrite規則,或者通過表單提交數據,其實并不一定會遇到上述問題,因為這時可以在提交數據時指定希望的編碼。另外,中文文件名確實會帶來問題,應該謹慎使用。

    6. 其它

    下面描述一些和編碼有關的其他問題。

    6.1. SecureCRT

    除了瀏覽器和控制臺與編碼有關外,一些客戶端也很有關系。比如在使用SecureCRT連接linux時,應該讓SecureCRT的顯示編碼(不同的session,可以有不同的編碼設置)和linux的編碼環境變量保持一致。否則看到的一些幫助信息,就可能是亂碼。

    另外,mysql有自己的編碼設置,也應該保持和SecureCRT的顯示編碼一致。否則通過SecureCRT執行sql語句的時候,可能無法處理中文字符,查詢結果也會出現亂碼。

    對于Utf-8文件,很多編輯器(比如記事本)會在文件開頭增加三個不可見的標志字節,如果作為mysql的輸入文件,則必須要去掉這三個字符。(用linux的vi保存可以去掉這三個字符)。一個有趣的現象是,在中文windows下,創建一個新txt文件,用記事本打開,輸入"連通"兩個字,保存,再打開,你會發現兩個字沒了,只留下一個小黑點。

    6.2. 過濾器

    如果需要統一設置編碼,則通過filter進行設置是個不錯的選擇。在filter class中,可以統一為需要的請求或者回應設置編碼。參加上述setCharacterEncoding()。這個類apache已經給出了可以直接使用的例子SetCharacterEncodingFilter。

    6.3. POST和GET

    很明顯,以POST提交信息時,URL有更好的可讀性,而且可以方便的使用setCharacterEncoding()來處理字符集問題。但GET方法形成的URL能夠更容易表達網頁的實際內容,也能夠用于收藏。

    從統一的角度考慮問題,建議采用GET方法,這要求在程序中獲得參數是進行特殊處理,而無法使用setCharacterEncoding()的便利,如果不考慮rewrite,就不存在IE的UTF-8問題,可以考慮通過設置URIEncoding來方便獲取URL中的參數。

    6.4. 簡繁體編碼轉換

    GBK同時包含簡體和繁體編碼,也就是說同一個字,由于編碼不同,在GBK編碼下屬于兩個字。有時候,為了正確取得完整的結果,應該將繁體和簡體進行統一。可以考慮將UTF、GBK中的所有繁體字,轉換為相應的簡體字,BIG5編碼的數據,也應該轉化成相應的簡體字。當然,仍舊以UTF編碼存儲。

    例如,對于"語言 語言",用UTF表示為"\xE8\xAF\xAD\xE8\xA8\x80 \xE8\xAA\x9E\xE8\xA8\x80",進行簡繁體編碼轉換后應該是兩個相同的 "\xE8\xAF\xAD\xE8\xA8\x80>"。

     

    Eceel東西在線 劉科垠

    2006-3-8

    轉自:http://china.eceel.com/article/study_for_character_encoding_java.htm

    posted @ 2006-12-12 11:23 保爾任 閱讀(215) | 評論 (0)編輯 收藏

    這是一篇程序員寫給程序員的趣味讀物。所謂趣味是指可以比較輕松地了解一些原來不清楚的概念,增進知識,類似于打RPG游戲的升級。整理這篇文章的動機是兩個問題:

      問題一:

      使用Windows記事本的“另存為”,可以在GBK、Unicode、Unicode big endian和UTF-8這幾種編碼方式間相互轉換。同樣是txt文件,Windows是怎樣識別編碼方式的呢?

      我很早前就發現Unicode、Unicode big endian和UTF-8編碼的txt文件的開頭會多出幾個字節,分別是FF、FE(Unicode),FE、FF(Unicode big endian),EF、BB、BF(UTF-8)。但這些標記是基于什么標準呢?

      問題二:

      最近在網上看到一個ConvertUTF.c,實現了UTF-32、UTF-16和UTF-8這三種編碼方式的相互轉換。對于Unicode(UCS2)、GBK、UTF-8這些編碼方式,我原來就了解。但這個程序讓我有些糊涂,想不起來UTF-16和UCS2有什么關系。

      查了查相關資料,總算將這些問題弄清楚了,順帶也了解了一些Unicode的細節。寫成一篇文章,送給有過類似疑問的朋友。本文在寫作時盡量做到通俗易懂,但要求讀者知道什么是字節,什么是十六進制。

    0、big endian和little endian

      big endian和little endian是CPU處理多字節數的不同方式。例如“漢”字的Unicode編碼是6C49。那么寫到文件里時,究竟是將6C寫在前面,還是將49寫在前面?如果將6C寫在前面,就是big endian。還是將49寫在前面,就是little endian。

      “endian”這個詞出自《格列佛游記》。小人國的內戰就源于吃雞蛋時是究竟從大頭(Big-Endian)敲開還是從小頭(Little-Endian)敲開,由此曾發生過六次叛亂,其中一個皇帝送了命,另一個丟了王位。

      我們一般將endian翻譯成“字節序”,將big endian和little endian稱作“大尾”和“小尾”。

    1、字符編碼、內碼,順帶介紹漢字編碼

      字符必須編碼后才能被計算機處理。計算機使用的缺省編碼方式就是計算機的內碼。早期的計算機使用7位的ASCII編碼,為了處理漢字,程序員設計了用于簡體中文的GB2312和用于繁體中文的big5。

      GB2312(1980年)一共收錄了7445個字符,包括6763個漢字和682個其它符號。漢字區的內碼范圍高字節從B0-F7,低字節從A1-FE,占用的碼位是72*94=6768。其中有5個空位是D7FA-D7FE。

      GB2312支持的漢字太少。1995年的漢字擴展規范GBK1.0收錄了21886個符號,它分為漢字區和圖形符號區。漢字區包括21003個字符。2000年的GB18030是取代GBK1.0的正式國家標準。該標準收錄了27484個漢字,同時還收錄了藏文、蒙文、維吾爾文等主要的少數民族文字。現在的PC平臺必須支持GB18030,對嵌入式產品暫不作要求。所以手機、MP3一般只支持GB2312。

      從ASCII、GB2312、GBK到GB18030,這些編碼方法是向下兼容的,即同一個字符在這些方案中總是有相同的編碼,后面的標準支持更多的字符。在這些編碼中,英文和中文可以統一地處理。區分中文編碼的方法是高字節的最高位不為0。按照程序員的稱呼,GB2312、GBK到GB18030都屬于雙字節字符集 (DBCS)。

      有的中文Windows的缺省內碼還是GBK,可以通過GB18030升級包升級到GB18030。不過GB18030相對GBK增加的字符,普通人是很難用到的,通常我們還是用GBK指代中文Windows內碼。

      這里還有一些細節:

      GB2312的原文還是區位碼,從區位碼到內碼,需要在高字節和低字節上分別加上A0。

      在DBCS中,GB內碼的存儲格式始終是big endian,即高位在前。

      GB2312的兩個字節的最高位都是1。但符合這個條件的碼位只有128*128=16384個。所以GBK和GB18030的低字節最高位都可能不是1。不過這不影響DBCS字符流的解析:在讀取DBCS字符流時,只要遇到高位為1的字節,就可以將下兩個字節作為一個雙字節編碼,而不用管低字節的高位是什么。

    2、Unicode、UCS和UTF

      前面提到從ASCII、GB2312、GBK到GB18030的編碼方法是向下兼容的。而Unicode只與ASCII兼容(更準確地說,是與ISO-8859-1兼容),與GB碼不兼容。例如“漢”字的Unicode編碼是6C49,而GB碼是BABA。

      Unicode也是一種字符編碼方法,不過它是由國際組織設計,可以容納全世界所有語言文字的編碼方案。Unicode的學名是"Universal Multiple-Octet Coded Character Set",簡稱為UCS。UCS可以看作是"Unicode Character Set"的縮寫。

      根據維基百科全書(http://zh.wikipedia.org/wiki/)的記載:歷史上存在兩個試圖獨立設計Unicode的組織,即國際標準化組織(ISO)和一個軟件制造商的協會(unicode.org)。ISO開發了ISO 10646項目,Unicode協會開發了Unicode項目。

      在1991年前后,雙方都認識到世界不需要兩個不兼容的字符集。于是它們開始合并雙方的工作成果,并為創立一個單一編碼表而協同工作。從Unicode2.0開始,Unicode項目采用了與ISO 10646-1相同的字庫和字碼。

      目前兩個項目仍都存在,并獨立地公布各自的標準。Unicode協會現在的最新版本是2005年的Unicode 4.1.0。ISO的最新標準是10646-3:2003。

      UCS規定了怎么用多個字節表示各種文字。怎樣傳輸這些編碼,是由UTF(UCS Transformation Format)規范規定的,常見的UTF規范包括UTF-8、UTF-7、UTF-16。

      IETF的RFC2781和RFC3629以RFC的一貫風格,清晰、明快又不失嚴謹地描述了UTF-16和UTF-8的編碼方法。我總是記不得IETF是Internet Engineering Task Force的縮寫。但IETF負責維護的RFC是Internet上一切規范的基礎。

    3、UCS-2、UCS-4、BMP

      UCS有兩種格式:UCS-2和UCS-4。顧名思義,UCS-2就是用兩個字節編碼,UCS-4就是用4個字節(實際上只用了31位,最高位必須為0)編碼。下面讓我們做一些簡單的數學游戲:

      UCS-2有2^16=65536個碼位,UCS-4有2^31=2147483648個碼位。

      UCS-4根據最高位為0的最高字節分成2^7=128個group。每個group再根據次高字節分為256個plane。每個plane根據第3個字節分為256行 (rows),每行包含256個cells。當然同一行的cells只是最后一個字節不同,其余都相同。

      group 0的plane 0被稱作Basic Multilingual Plane, 即BMP。或者說UCS-4中,高兩個字節為0的碼位被稱作BMP。

      將UCS-4的BMP去掉前面的兩個零字節就得到了UCS-2。在UCS-2的兩個字節前加上兩個零字節,就得到了UCS-4的BMP。而目前的UCS-4規范中還沒有任何字符被分配在BMP之外。

    4、UTF編碼

      UTF-8就是以8位為單元對UCS進行編碼。從UCS-2到UTF-8的編碼方式如下:

    UCS-2編碼(16進制) UTF-8 字節流(二進制)
    0000 - 007F 0xxxxxxx
    0080 - 07FF 110xxxxx 10xxxxxx
    0800 - FFFF 1110xxxx 10xxxxxx 10xxxxxx


      例如“漢”字的Unicode編碼是6C49。6C49在0800-FFFF之間,所以肯定要用3字節模板了:1110xxxx 10xxxxxx 10xxxxxx。將6C49寫成二進制是:0110 110001 001001, 用這個比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

      讀者可以用記事本測試一下我們的編碼是否正確。

      UTF-16以16位為單元對UCS進行編碼。對于小于0x10000的UCS碼,UTF-16編碼就等于UCS碼對應的16位無符號整數。對于不小于0x10000的UCS碼,定義了一個算法。不過由于實際使用的UCS2,或者UCS4的BMP必然小于0x10000,所以就目前而言,可以認為UTF-16和UCS-2基本相同。但UCS-2只是一個編碼方案,UTF-16卻要用于實際的傳輸,所以就不得不考慮字節序的問題。

    5、UTF的字節序和BOM

      UTF-8以字節為編碼單元,沒有字節序的問題。UTF-16以兩個字節為編碼單元,在解釋一個UTF-16文本前,首先要弄清楚每個編碼單元的字節序。例如收到一個“奎”的Unicode編碼是594E,“乙”的Unicode編碼是4E59。如果我們收到UTF-16字節流“594E”,那么這是“奎”還是“乙”?

      Unicode規范中推薦的標記字節順序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一個有點小聰明的想法:

      在UCS編碼中有一個叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的編碼是FEFF。而FFFE在UCS中是不存在的字符,所以不應該出現在實際傳輸中。UCS規范建議我們在傳輸字節流前,先傳輸字符"ZERO WIDTH NO-BREAK SPACE"。

      這樣如果接收者收到FEFF,就表明這個字節流是Big-Endian的;如果收到FFFE,就表明這個字節流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被稱作BOM。

      UTF-8不需要BOM來表明字節順序,但可以用BOM來表明編碼方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8編碼是EF BB BF(讀者可以用我們前面介紹的編碼方法驗證一下)。所以如果接收者收到以EF BB BF開頭的字節流,就知道這是UTF-8編碼了。

      Windows就是使用BOM來標記文本文件的編碼方式的。

    6、進一步的參考資料

      本文主要參考的資料是 "Short overview of ISO-IEC 10646 and Unicode" (http://www.nada.kth.se/i18n/ucs/unicode-iso10646-oview.html)。

      我還找了兩篇看上去不錯的資料,不過因為我開始的疑問都找到了答案,所以就沒有看:

    "Understanding Unicode A general introduction to the Unicode Standard" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter04a)
    "Character set encoding basics Understanding character set encodings and legacy encodings" (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&item_id=IWS-Chapter03)

      我寫過UTF-8、UCS-2、GBK相互轉換的軟件包,包括使用Windows API和不使用Windows API的版本。以后有時間的話,我會整理一下放到我的個人主頁上(http://fmddlmyy.home4u.china.com)。

      我是想清楚所有問題后才開始寫這篇文章的,原以為一會兒就能寫好。沒想到考慮措辭和查證細節花費了很長時間,竟然從下午1:30寫到9:00。希望有讀者能從中受益。

    posted @ 2006-12-12 11:22 保爾任 閱讀(298) | 評論 (0)編輯 收藏

    鏈接都已失效,可以在網上搜索。

    高效解析XML

    詳細解析XML與J2EE組合

    初學者入門 JAVA 的 XML 編程實例解析

    Java中四種XML解析技術之不完全測試

    XML認證教程,第 7 部分

    詳細解析XML與J2EE組合技術的精髓

    Java高手解析XML配置文件的讀取操作

    使用dom4j解析xml??? http://www-128.ibm.com/developerworks/cn/xml/x-dom4j.html

    初學者入門 JAVA 的 XML 編程實例解析 http://tech.ccidnet.com/art/1077/20050307/219781_1.html

    posted @ 2006-12-12 11:21 保爾任 閱讀(810) | 評論 (0)編輯 收藏
    Calendar與Date、long的轉換:
    ? Calendar ca = Calendar.getInstance();
    ??Date d = ca.getTime();
    ??long l = ca.getTimeInMillis();
    ??ca.setTime(d);
    ??ca.setTimeInMillis(l);

    Date和long間的轉換:
    ??Date d = new Date();
    ??long l = d.getTime();
    ??d.setTime(l);
    ??d = new Date(l);
    ————————————————————————————————
    ? 當前年月實際的總天數:???
    ? Calendar ? cal ? = ? new ? GregorianCalendar(); ?
    ? ?
    ? int ? year_days ? ? = ? cal.getActualMaximum(Calendar.DAY_OF_YEAR ? ); ?
    ? int ? month_days ? = ? cal.getActualMaximum(Calendar.DAY_OF_MONTH);???
    ???
    ? 可能出現的最大天數:???
    ????
    ? int ? month_days ? = ? cal.getMaximum(Calendar.DAY_OF_MONTH); ?
    ? // ? 這種方式不隨當前日期的影響,如果取 ? 2 ? 月份,總是的到 ? 29 ?
    ? ?
    ? ?
    ? 在給 ? Calendar ? 指定月份時要注意:?????
    ????
    ? Java ? 中的月份,0 ? - ? 表示1月份, ? ..... ? ? 11 ? - ? 表示12月份,不要搞錯了喲???保險的方式是,使用常量:Calendar.JANUARY ? ...??

    ————————————————————————————————


    學習日期, 日期格式, 日期的解析和日期的計算

    Java 語言的 Calendar,GregorianCalendar (日歷),Date(日期), 和DateFormat(日期格式)組成了Java標準的一個基本但是非常重要的部分. 日期是商業邏輯計算一個關鍵的部分. 所有的開發者都應該能夠計算未來的日期, 定制日期的顯示格式, 并將文本數據解析成日期對象。學習日期, 日期格式, 日期的解析和日期的計算。

    我們將討論下面的類:

    1、 具體類(和抽象類相對)java.util.Date

    2、 抽象類java.text.DateFormat 和它的一個具體子類,java.text.SimpleDateFormat

    3、 抽象類java.util.Calendar 和它的一個具體子類,java.util.GregorianCalendar

    具體類可以被實例化, 但是抽象類卻不能. 你首先必須實現抽象類的一個具體子類.

    1.?? java.util.Date及其格式化
    Date 類從Java 開發包(JDK) 1.0 就開始進化, 當時它只包含了幾個取得或者設置一個日期數據的各個部分的方法, 比如說月, 日, 和年. 這些方法現在遭到了批評并且已經被轉移到了Calendar類里去了, 我們將在本文中進一步討論它. 這種改進旨在更好的處理日期數據的國際化格式. 就象在JDK 1.1中一樣, Date 類實際上只是一個包裹類, 它包含的是一個長整型數據, 表示的是從GMT(格林尼治標準時間)1970年, 1 月 1日00:00:00這一刻之前或者是之后經歷的毫秒數.

    1.1. 創建java.util.Date
    Java統計從1970年1月1日起的毫秒的數量表示日期。也就是說,例如,1970年1月2日,是在1月1日后的86,400,000毫秒。同樣的,1969年12 月31日是在1970年1月1日前86,400,000毫秒。Java的Date類使用long類型紀錄這些毫秒值.因為long是有符號整數,所以日期可以在1970年1月1日之前,也可以在這之后。Long類型表示的最大正值和最大負值可以輕松的表示290,000,000年的時間,這適合大多數人的時間要求。

    讓我們看一個使用系統的當前日期和時間創建一個日期對象并返回一個長整數的簡單例子. 這個時間通常被稱為Java 虛擬機(JVM)主機環境的系統時間.
    import java.util.Date;

    public class DateExample1 {

    public static void main(String[] args) {

    // Get the system date/time

    Date date = new Date();

    // 打印出具體的年,月,日,小時,分鐘,秒鐘以及時區

    System.out.println(date.getTime());

    }??

    }

    在星期六, 2001年9月29日, 下午大約是6:50的樣子, 上面的例子在系統輸出設備上顯示的結果是 1001803809710. 在這個例子中,值得注意的是我們使用了Date 構造函數創建一個日期對象, 這個構造函數沒有接受任何參數. 而這個構造函數在內部使用了 System.currentTimeMillis() 方法來從系統獲取日期.

    //1年前日期

    ? java.util.Date myDate=new java.util.Date();

    ? long myTime=(myDate.getTime()/1000)-60*60*24*365;

    ? myDate.setTime(myTime*1000);

    ? String mDate=formatter.format(myDate);

    //明天日期

    ? myDate=new java.util.Date();

    ? myTime=(myDate.getTime()/1000)+60*60*24;

    ? myDate.setTime(myTime*1000);

    ? mDate=formatter.format(myDate);

    //兩個時間之間的天數

    ? SimpleDateFormat myFormatter = new SimpleDateFormat("yyyy-MM-dd");

    ? java.util.Date date= myFormatter.parse("2003-05-1");

    ? java.util.Date mydate= myFormatter.parse("1899-12-30");

    ? long day=(date.getTime()-mydate.getTime())/(24*60*60*1000);

    //加半小時

    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    java.util.Date date1 = format.parse("2002-02-28 23:16:00");

    long Time=(date1.getTime()/1000)+60*30;

    date1.setTime(Time*1000);

    String mydate1=formatter.format(date1);

    //年月周求日期

    SimpleDateFormat formatter2 = new SimpleDateFormat("yyyy-MM F E");

    java.util.Date date2= formatter2.parse("2003-05 5 星期五");

    SimpleDateFormat formatter3 = new SimpleDateFormat("yyyy-MM-dd");

    String mydate2=formatter3.format(date2);

    //求是星期幾

    mydate= myFormatter.parse("2001-1-1");

    SimpleDateFormat formatter4 = new SimpleDateFormat("E");

    String mydate3=formatter4.format(mydate);

    ?

    ?


    1.2. Date格式化
    能以一種用戶明白的格式來顯示這個日期呢? 在這里類java.text.SimpleDateFormat 和它的抽象基類 java.text.DateFormat。那么, 現在我們已經知道了如何獲取從1970年1月1日開始經歷的毫秒數了. 我們如何才format 就派得上用場了.

    // 我們能不能用下面的代碼構件出 2001/8/8 8:8
    ? import java.io.*;
    ? import java.util.*;

    ? public class WhatIsDate
    ? {
    ??? public static void main(String[] args) {
    ??????? Date date = new Date(2001, 8, 8, 8, 8, 8);
    ??????? System.out.println(date);
    ??? }
    ? }


    Java 的編譯器竟然報如下信息 (Sun JDK1.3, Windows 2000 中文下)

    注意:
    WhatIsDate.java 使用或覆蓋一個不鼓勵使用的API。
    注意:
    使用-deprecation重新編譯,以得到詳細信息。!


    那么 Date 對象究竟是為了滿足哪個需求呢?看來它不是用來實現基于年/月/日小時:分鐘 的時間表述。我們查看 Java 的文檔,我們看到有 getTime() 方法,它返回的竟然是一個 long 值。

    文檔進一步又告訴我們這個值代表了當前系統的時間離1970/1/1 0:0 的毫秒差,而且是在 GMT 時區下(也被稱為 EPOC)。如果我們指定的時間是在此之前的,那它將返回一個負數值。

    這個發現讓我們對 Date 對象有了一個全新的認識-Date 存放的是與 EPOC 的偏差值。換而言之我們也可通過 long 類型來表示時間?對了,這個猜想是得到了 Java 的支持:

    ? // 第二種獲得當前時間的方法
    ? long dateInMilliSeconds = System.currentTimeMillis();
    ? // 這時候打印出的只是一串數字而已
    ? System.out.println(dateInMilliSeconds);


    對程序執行效率敏感的程序員可以發現這個方法只是生成一個 Java 的原始類型 (primitive type) long, 不需要實例化一個對象。因此如果我們對時間的處理只是在內部進行時,可以用 long 來代替 Date 對象。

    最典型的應用就是在一段代碼開始和結束時,分別獲得系統當前的時間,然后計算出代碼執行所需的時間(微秒級)。

    ? long start = System.currentTimeMillis();
    ? // 代碼段
    ? System.out.println("需要 "+(System.currentTimeMillis()-start)+" 微秒");


    那么當我們要把這個 long 值已更為友好的表現形式顯示處理的時候,我們可以用它來構造 Date 對象:

    Date date = new Date(dateInMilliSeconds);

    System.out.println(date);


    我們看到了在 Java 中對時間最為基本的表示,有通過對EPOC 的偏差值進行處理。Date 對象是對它的一個對象的封裝。我們同時也看到了,在現時世界中我們對時間的描述通常是通過"某年某月某日某時某分"來定義的。Date 的顯示(實際上是 toString() 方法)描述了這些信息,但 Java 并不建議我們用這種方式直接來構件 Date 對象。因此我們需要找出哪個對象可以實現這個需求。這就是我們下面就要講述的 Calendar 對象的功能。

    在我們進一步研究 Calendar 之前,請記住 Date 只是一個對 long 值(基于 GMT 時區)的對象封裝。它所表現出來的年/月/日小時:分鐘時區的時間表述,只是它的 toString() 方法所提供的。千萬不要為這個假象所迷惑。

    假如我們希望定制日期數據的格式, 比方星期六-9月-29日-2001年. 下面的例子展示了如何完成這個工作:

    import java.text.SimpleDateFormat;

    import java.util.Date;

    public class DateExample2 {

    public static void main(String[] args) {

    SimpleDateFormat bartDateFormat = new SimpleDateFormat("EEEE-MMMM-dd-yyyy"); Date date = new Date();

    System.out.println(bartDateFormat.format(date));

    }

    }


    只要通過向SimpleDateFormat 的構造函數傳遞格式字符串"EEE-MMMM-dd-yyyy", 我們就能夠指明自己想要的格式. 你應該可以看見, 格式字符串中的ASCII 字符告訴格式化函數下面顯示日期數據的哪一個部分. EEEE是星期, MMMM是月, dd是日, yyyy是年. 字符的個數決定了日期是如何格式化的.傳遞"EE-MM-dd-yy"會顯示 Sat-09-29-01. 請察看Sun 公司的Web 站點獲取日期格式化選項的完整的指示.

    1.3. 文本數據解析成日期對象
    假設我們有一個文本字符串包含了一個格式化了的日期對象, 而我們希望解析這個字符串并從文本日期數據創建一個日期對象. 我們將再次以格式化字符串"MM-dd-yyyy" 調用 SimpleDateFormat類, 但是這一次, 我們使用格式化解析而不是生成一個文本日期數據. 我們的例子, 顯示在下面, 將解析文本字符串 "9-29-2001"并創建一個值為001736000000 的日期對象.

    通過parse()方法,DateFormat能夠以一個字符串創立一個Date對象。這個方法能拋出ParseException異常,所以你必須使用適當的異常處理技術。

    例子程序:

    import java.text.SimpleDateFormat;

    import java.util.Date;

    public class DateExample3 {

    public static void main(String[] args) {

    // Create a date formatter that can parse dates of

    // the form MM-dd-yyyy.

    SimpleDateFormat bartDateFormat = new SimpleDateFormat("MM-dd-yyyy");

    // Create a string containing a text date to be parsed.

    String dateStringToParse = "9-29-2001";

    try {

    // Parse the text version of the date.

    // We have to perform the parse method in a

    // try-catch construct in case dateStringToParse

    // does not contain a date in the format we are expecting.

    Date date = bartDateFormat.parse(dateStringToParse);

    // Now send the parsed date as a long value

    // to the system output.

    System.out.println(date.getTime());

    }catch (Exception ex) {

    System.out.println(ex.getMessage());

    }

    }

    }


    1.4. 使用標準的日期格式化過程
    既然我們已經可以生成和解析定制的日期格式了, 讓我們來看一看如何使用內建的格式化過程. 方法 DateFormat.getDateTimeInstance() 讓我們得以用幾種不同的方法獲得標準的日期格式化過程. 在下面的例子中, 我們獲取了四個內建的日期格式化過程. 它們包括一個短的, 中等的, 長的, 和完整的日期格式.

    import java.text.DateFormat;

    import java.util.Date;

    public class DateExample4 {

    public static void main(String[] args) {

    Date date = new Date();

    DateFormat shortDateFormat = DateFormat.getDateTimeInstance(

    DateFormat.SHORT, DateFormat.SHORT);

    DateFormat mediumDateFormat = DateFormat.getDateTimeInstance(

    DateFormat.MEDIUM, DateFormat.MEDIUM);

    DateFormat longDateFormat = DateFormat.getDateTimeInstance(

    DateFormat.LONG, DateFormat.LONG);

    DateFormat fullDateFormat = DateFormat.getDateTimeInstance(

    DateFormat.FULL, DateFormat.FULL);

    System.out.println(shortDateFormat.format(date)); System.out.println(mediumDateFormat.format(date)); System.out.println(longDateFormat.format(date)); System.out.println(fullDateFormat.format(date));

    }

    }

    注意我們在對 getDateTimeInstance的每次調用中都傳遞了兩個值. 第一個參數是日期風格, 而第二個參數是時間風格. 它們都是基本數據類型int(整型). 考慮到可讀性, 我們使用了DateFormat 類提供的常量: SHORT, MEDIUM, LONG, 和 FULL. 要知道獲取時間和日期格式化過程的更多的方法和選項, 請看Sun 公司Web 站點上的解釋.

    運行我們的例子程序的時候, 它將向標準輸出設備輸出下面的內容:
    9/29/01 8:44 PM
    Sep 29, 2001 8:44:45 PM
    September 29, 2001 8:44:45 PM EDT
    Saturday, September 29, 2001 8:44:45 PM EDT

    2.?? Calendar 日歷類
    首先請記住 Calendar 只是一個抽象類, 也就是說你無法直接獲得它的一個實例,換而言之你可以提供一個自己開發的 Calendar 對象。

    那究竟什么是一個 Calendar 呢?中文的翻譯就是日歷,那我們立刻可以想到我們生活中有陽(公)歷、陰(農)歷之分。它們的區別在哪呢?

    比如有:

    月份的定義 - 陽`(公)歷 一年12 個月,每個月的天數各不同;陰(農)歷,每個月固定28天,每周的第一天 - 陽(公)歷星期日是第一天;陰(農)歷,星期一是第一天

    實際上,在歷史上有著許多種紀元的方法。它們的差異實在太大了,比如說一個人的生日是"八月八日" 那么一種可能是陽(公)歷的八月八日,但也可以是陰 (農)歷的日期。所以為了計時的統一,必需指定一個日歷的選擇。那現在最為普及和通用的日歷就是 "Gregorian Calendar"。也就是我們在講述年份時常用 "公元幾幾年"。Calendar 抽象類定義了足夠的方法,讓我們能夠表述日歷的規則。Java 本身提供了對 "Gregorian Calendar" 規則的實現。我們從 Calendar.getInstance() 中所獲得的實例就是一個 "GreogrianCalendar" 對象(與您通過 new GregorianCalendar() 獲得的結果一致)。

    下面的代碼可以證明這一點:

    ? import java.io.*;
    ? import java.util.*;

    ? public class WhatIsCalendar
    ? {
    ??? public static void main(String[] args) {
    ??????? Calendar calendar = Calendar.getInstance();
    ??????? if (calendar instanceof GregorianCalendar)
    ????????? System.out.println("It is an instance of GregorianCalendar");
    ??? }
    ? }

    ?


    Calendar 在 Java 中是一個抽象類(Abstract Class),GregorianCalendar 是它的一個具體實現。

    Calendar 與 Date 的轉換非常簡單:

    ? Calendar calendar = Calendar.getInstance();
    ? // 從一個 Calendar 對象中獲取 Date 對象
    ? Date date = calendar.getTime();
    ? // 將 Date 對象反應到一個 Calendar 對象中,
    ? // Calendar/GregorianCalendar 沒有構造函數可以接受 Date 對象
    ? // 所以我們必需先獲得一個實例,然后設置 Date 對象
    ? calendar.setTime(date);

    ?

    ?

    Calendar 對象在使用時,有一些值得注意的事項:

    1. Calendar 的 set() 方法

    set(int field, int value) - 是用來設置"年/月/日/小時/分鐘/秒/微秒"等值

    field 的定義在 Calendar 中

    set (int year, int month, int day, int hour, int minute, int second) 但沒有set (int year, int month, int day, int hour, int minute, int second, int millisecond) 前面 set(int,int,int,int,int,int) 方法不會自動將 MilliSecond 清為 0。

    另外,月份的起始值為0而不是1,所以要設置八月時,我們用7而不是8。

    calendar.set(Calendar.MONTH, 7);

    我們通常需要在程序邏輯中set(Calendar.MILLISECOND, 0),否則可能會出現下面的情況:
    //ObjectOutputStream和ObjectOutputStream是對象存儲的類
    //Calendar.MILLISECOND如不設為0則會是個與當前系統時間有關的數

    ? import java.io.*;
    ? import java.util.*;

    ? public class WhatIsCalendarWrite
    ? {
    ??? public static void main(String[] args) throws Exception{
    ??????? ObjectOutputStream out =
    ????????? new ObjectOutputStream(
    ??????????? new FileOutputStream("calendar.out"));
    ??????? Calendar cal1 = Calendar.getInstance();
    ??????? cal1.set(2000, 7, 1, 0, 0, 0);
    ??????? out.writeObject(cal1);
    ??????? Calendar cal2 = Calendar.getInstance();
    ??????? cal2.set(2000, 7, 1, 0, 0, 0);
    ??????? cal2.set(Calendar.MILLISECOND, 0);
    ??????? out.writeObject(cal2);
    ??????? out.close();
    ??? }
    ? }


    我們將 Calendar 保存到文件中

    ? import java.io.*;
    ? import java.util.*;

    ? public class WhatIsCalendarRead
    ? {
    ??? public static void main(String[] args) throws Exception{
    ??????? ObjectInputStream in =
    ????????? new ObjectInputStream(
    ??????????? new FileInputStream("calendar.out"));
    ??????? Calendar cal2 = (Calendar)in.readObject();
    ??????? Calendar cal1 = Calendar.getInstance();
    ??????? cal1.set(2000, 7, 1, 0, 0, 0);
    ??????? if (cal1.equals(cal2))
    ????????? System.out.println("Equals");
    ??????? else
    ????????? System.out.println("NotEqual");
    ??????? System.out.println("Old calendar "+cal2.getTime().getTime());
    ??????? System.out.println("New calendar "+cal1.getTime().getTime());
    ??????? cal1.set(Calendar.MILLISECOND, 0);
    ??????? cal2 = (Calendar)in.readObject();
    ??????? if (cal1.equals(cal2))
    ????????? System.out.println("Equals");
    ??????? else
    ????????? System.out.println("NotEqual");
    ??????? System.out.println("Processed Old calendar "+cal2.getTime().getTime());
    ??????? System.out.println("Processed New calendar "+cal1.getTime().getTime());
    ??? }
    ? }


    然后再另外一個程序中取回來(模擬對數據庫的存儲),但是執行的結果是:

    NotEqual
    Old calendar 965113200422 <------------ 最后三位的MilliSecond與當前時間有關
    New calendar 965113200059 <-----------/
    Equals
    Processed Old calendar 965113200000
    Processed New calendar 965113200000

    ?

    另外我們要注意的一點是,Calendar 為了性能原因對 set() 方法采取延緩計算的方法。在 JavaDoc 中有下面的例子來說明這個問題:

    Calendar cal1 = Calendar.getInstance();
    ? cal1.set(2000, 7, 31, 0, 0 , 0); //2000-8-31
    ? cal1.set(Calendar.MONTH, Calendar.SEPTEMBER); //應該是 2000-9-31,也就是 2000-10-1
    ? cal1.set(Calendar.DAY_OF_MONTH, 30); //如果 Calendar 轉化到 2000-10-1,那么現在的結果就該是 2000-10-30
    ? System.out.println(cal1.getTime()); //輸出的是2000-9-30,說明 Calendar 不是馬上就刷新其內部的記錄


    在 Calendar 的方法中,get() 和 add() 會讓 Calendar 立刻刷新。Set() 的這個特性會給我們的開發帶來一些意想不到的結果。我們后面會看到這個問題。

    2. Calendar 對象的容錯性,Lenient 設置

    我們知道特定的月份有不同的日期,當一個用戶給出錯誤的日期時,Calendar 如何處理的呢?

    ? import java.io.*;
    ? import java.util.*;

    ? public class WhatIsCalendar
    ? {
    ??? public static void main(String[] args) throws Exception{
    ??????? Calendar cal1 = Calendar.getInstance();
    ??????? cal1.set(2000, 1, 32, 0, 0, 0);
    ??????? System.out.println(cal1.getTime());
    ??????? cal1.setLenient(false);
    ??????? cal1.set(2000, 1, 32, 0, 0, 0);
    ??????? System.out.println(cal1.getTime());
    ??? }
    ? }


    它的執行結果是:

    ? Tue Feb 01 00:00:00 PST 2000
    ? Exception in thread "main" java.lang.IllegalArgumentException
    ??? at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:1368)
    ??? at java.util.Calendar.updateTime(Calendar.java:1508)
    ??? at java.util.Calendar.getTimeInMillis(Calendar.java:890)
    ??? at java.util.Calendar.getTime(Calendar.java:871)
    ??? at WhatIsCalendar.main(WhatIsCalendar.java:12)


    當我們設置該 Calendar 為 Lenient false 時,它會依據特定的月份檢查出錯誤的賦值。

    3. 不穩定的 Calendar

    我們知道 Calendar 是可以被 serialize 的,但是我們要注意下面的問題

    ? import java.io.*;
    ? import java.util.*;

    ? public class UnstableCalendar implements Serializable
    ? {

    ??? public static void main(String[] args) throws Exception{
    ??????? Calendar cal1 = Calendar.getInstance();
    ??????? cal1.set(2000, 7, 1, 0, 0 , 0);
    ??????? cal1.set(Calendar.MILLISECOND, 0);
    ??????? ObjectOutputStream out =
    ????????? new ObjectOutputStream(
    ????????? new FileOutputStream("newCalendar.out"));
    ??????? out.writeObject(cal1);
    ??????? out.close();
    ??????? ObjectInputStream in =
    ????????? new ObjectInputStream(
    ????????? new FileInputStream("newCalendar.out"));
    ??????? Calendar cal2 = (Calendar)in.readObject();
    ??????? cal2.set(Calendar.MILLISECOND, 0);
    ??????? System.out.println(cal2.getTime());
    ??? }
    ? }

    ?


    運行的結果竟然是: Thu Jan 01 00:00:00 PST 1970

    它被復原到 EPOC 的起始點,我們稱該 Calendar 是處于不穩定狀態。這個問題的根本原因是 Java 在 serialize GregorianCalendar 時沒有保存所有的信息,所以當它被恢復到內存中,又缺少足夠的信息時,Calendar 會被恢復到 EPOCH 的起始值。Calendar 對象由兩部分構成:字段和相對于 EPOC 的微秒時間差。字段信息是由微秒時間差計算出的,而 set() 方法不會強制 Calendar 重新計算字段。這樣字段值就不對了。

    下面的代碼可以解決這個問題:

    ? import java.io.*;
    ? import java.util.*;

    ? public class StableCalendar implements Serializable
    ? {
    ??? public static void main(String[] args) throws Exception{
    ??????? Calendar cal1 = Calendar.getInstance();
    ??????? cal1.set(2000, 7, 1, 0, 0 , 0);
    ??????? cal1.set(Calendar.MILLISECOND, 0);
    ??????? ObjectOutputStream out =
    ????????? new ObjectOutputStream(
    ????????? new FileOutputStream("newCalendar.out"));
    ??????? out.writeObject(cal1);
    ??????? out.close();
    ??????? ObjectInputStream in =
    ????????? new ObjectInputStream(
    ????????? new FileInputStream("newCalendar.out"));
    ??????? Calendar cal2 = (Calendar)in.readObject();
    ??????? cal2.get(Calendar.MILLISECOND); //先調用 get(),強制 Calendar 刷新
    ??????? cal2.set(Calendar.MILLISECOND, 0); //再設值
    ??????? System.out.println(cal2.getTime());
    ??? }
    ? }


    運行的結果是: Tue Aug 01 00:00:00 PDT 2000,這個問題主要會影響到在 EJB 編程中,參數對象中包含 Calendar 時。經過 Serialize/Deserialize 后,直接操作 Calendar 會產生不穩定的情況。

    4. add() 與 roll() 的區別

    add() 的功能非常強大,add 可以對 Calendar 的字段進行計算。如果需要減去值,那么使用負數值就可以了,如 add(field, -value)。

    add() 有兩條規則:

    當被修改的字段超出它可以的范圍時,那么比它大的字段會自動修正。如:

    Calendar cal1 = Calendar.getInstance();

    cal1.set(2000, 7, 31, 0, 0 , 0); //2000-8-31

    cal1.add(Calendar.MONTH, 1); //2000-9-31 => 2000-10-1,對嗎?System.out.println(cal1.getTime()); //結果是 2000-9-30


    另一個規則是,如果比它小的字段是不可變的(由 Calendar 的實現類決定),那么該小字段會修正到變化最小的值。

    以上面的例子,9-31 就會變成 9-30,因為變化最小。

    Roll() 的規則只有一條:當被修改的字段超出它可以的范圍時,那么比它大的字段不會被修正。如:

    Calendar cal1 = Calendar.getInstance();
    cal1.set(1999, 5, 6, 0, 0, 0); //1999-6-6, 周日
    cal1.roll(Calendar.WEEK_OF_MONTH, -1); //1999-6-1, 周二
    cal1.set(1999, 5, 6, 0, 0, 0); //1999-6-6, 周日
    cal1.add(Calendar.WEEK_OF_MONTH, -1); //1999-5-30, 周日
    WEEK_OF_MONTH 比 MONTH 字段小,所以 roll 不能修正 MONTH 字段。


    我們現在已經能夠格式化并創建一個日期對象了, 但是我們如何才能設置和獲取日期數據的特定部分呢, 比如說小時, 日, 或者分鐘? 我們又如何在日期的這些部分加上或者減去值呢? 答案是使用Calendar 類. 就如我們前面提到的那樣, Calendar 類中的方法替代了Date 類中被人唾罵的方法.

    假設你想要設置, 獲取, 和操縱一個日期對象的各個部分, 比方一個月的一天或者是一個星期的一天. 為了演示這個過程, 我們將使用具體的子類 java.util.GregorianCalendar. 考慮下面的例子, 它計算得到下面的第十個星期五是13號.

    import java.util.GregorianCalendar;

    import java.util.Date;

    import java.text.DateFormat;

    public class DateExample5 {

    public static void main(String[] args) {

    DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL);

    // Create our Gregorian Calendar.

    GregorianCalendar cal = new GregorianCalendar();

    // Set the date and time of our calendar

    // to the system&s date and time

    cal.setTime(new Date());

    System.out.println("System Date: " + dateFormat.format(cal.getTime())); // Set the day of week to FRIDAY

    cal.set(GregorianCalendar.DAY_OF_WEEK, GregorianCalendar.FRIDAY); System.out.println("After Setting Day of Week to Friday: " + dateFormat.format(cal.getTime()));

    int friday13Counter = 0;

    while (friday13Counter <= 10) {

    // Go to the next Friday by adding 7 days. cal.add(GregorianCalendar.DAY_OF_MONTH, 7);

    // If the day of month is 13 we have

    // another Friday the 13th.

    if (cal.get(GregorianCalendar.DAY_OF_MONTH) == 13) {

    friday13Counter++; System.out.println(dateFormat.format(cal.getTime()));

    }

    }

    }

    }

    在這個例子中我們作了有趣的函數調用:

    cal.set(GregorianCalendar.DAY_OF_WEEK, GregorianCalendar.FRIDAY);

    和:cal.add(GregorianCalendar.DAY_OF_MONTH, 7);

    set 方法能夠讓我們通過簡單的設置星期中的哪一天這個域來將我們的時間調整為星期五. 注意到這里我們使用了常量 DAY_OF_WEEK 和 FRIDAY 來增強代碼的可讀性. add 方法讓我們能夠在日期上加上數值. 潤年的所有復雜的計算都由這個方法自動處理.

    我們這個例子的輸出結果是:

    System Date: Saturday, September 29, 2001

    當我們將它設置成星期五以后就成了: Friday, September 28, 2001

    Friday, September 13, 2002

    Friday, December 13, 2002

    Friday, June 13, 2003

    Friday, February 13, 2004

    Friday, August 13, 2004

    Friday, May 13, 2005

    Friday, January 13, 2006

    Friday, October 13, 2006

    Friday, April 13, 2007

    Friday, July 13, 2007

    Friday, June 13, 2008

    Calendar類的基礎即有變量域的觀念。每個類元素都是域,并且這些域在Calendar類中表現為靜態變量。這些變量域,可以通過get/set類方法來獲得或者設置域值。

    // 獲得默認的Calendar實例,給它設置時間
    Calendarcal = Calendar.getInstance();
    intyear = cal.get(Calendar.YEAR);
    cal.set(Calendar.MONTH,Calendar.NOVEMBER);
    Calendar類的add和roll方法提供在日期之間轉換的能力。每個方法都由一個參數變量和一個參數值來修改,通過這個可為正數或負數的參數值來修改它。僅僅不同的是,add方法可以向高階的變量域溢出。例如,如果從九月三號向后倒退三天,將得到:

    Calendar cal = Calendar.getInstance();

    cal.add(Calendar.DATE,-3);

    // 值為: 星期六八月 31 23:43:19 EDT 2002

    然而使用roll方法向后回滾三天得出:

    Calendar cal = Calendar.getInstance();

    cal.roll(Calendar.DATE,-3);

    // 值為: 星期一九月 30 23:43:47 EDT 2002
    這就是為什么通常主要使用add方法的原因。

    還有一個隱藏在最通用的Calendar的子類中的功能性方法--isLeapYear(判斷是否為閏年)方法。

    Calendar cal = Calendar.getInstance();

    booleanleapYear = ( (GregorianCalendar)cal ).isLeapYear(2002);

    // 這個值是false

    盡管它是一個實例方法,isLeapYear方法的行為表現像靜態方法,需要提供年份的參數傳值給日歷。

    其實求幾天幾月幾年前/后的方法,應該用Calendar類比較好的(比Date)。

    Calendar cal = Calendar.getInstance();

    cal.setTime(date);

    cal.add(Calendar.MONTH,1);

    cal.add(Calendar.YEAR,2000);

    date = cal.getTime();

    通過接管日期修改的功能,java.util.Calendar類看上去更像是Data類的復雜版本。但是它還提供額外的功能,更不用說它的國際化支持,使得它值得擁有學習的難度曲線。

    3.???? 使用GregorianCalendar類
    創建一個代表任意日期的一個途徑使用GregorianCalendar類的構造函數,它包含在java.util包中:

    GregorianCalendar(int year, int month, int date)
    注意月份的表示,一月是0,二月是1,以此類推,是12月是11。因為大多數人習慣于使用單詞而不是使用數字來表示月份,這樣程序也許更易讀,父類 Calendar使用常量來表示月份:JANUARY, FEBRUARY,等等。所以,創建Wilbur 和 Orville制造第一架動力飛機的日期(December 17, 1903),你可以使用:

    GregorianCalendar firstFlight = new GregorianCalendar(1903, Calendar.DECEMBER, 17);

    出于清楚的考慮,你應該使用前面的形式。但是,你也應該學習怎樣閱讀下面的短格式。下面的例子同樣表示December 17,1903(記住,在短格式中,11表示December)

    ??? GregorianCalendar firstFlight = new GregorianCalendar(1903, 11, 17);?? 在上一節中,你學習了轉換Date對象到字符串。這里,你可以做同樣的事情;但是首先,你需要將GregorianCalendar對象轉換到Date。要做到這一點,你可以使用getTime()方法,從它得父類 Calendar繼承而來。GetTime()方法返回GregorianCalendar相應的Date對象。你能夠創建 GregorianCalendar對象,轉換到Date對象,得到和輸出相應的字符串這樣一個過程。下面是例子:

    import java.util.*;

    import java.text.*;

    public class Flight {

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

    GregorianCalendar firstFlight = new GregorianCalendar(1903, Calendar.DECEMBER, 17);??

    Date d = firstFlight.getTime();

    DateFormat df = DateFormat.getDateInstance();

    String s = df.format(d);

    System.out.println("First flight was " + s);

    }

    有時候創建一個代表當前時刻的GregorianCalendar類的實例是很有用的。你可以簡單的使用沒有參數的GregorianCalendar構造函數,象這樣:

    GregorianCalendar thisday = new GregorianCalendar();
    一個輸出今天日期的例子程序,使用GregorianCalendar對象:

    import java.util.*;
    import java.text.*;
    class Today {
    ? public static void main(String[] args) {
    GregorianCalendar thisday = new GregorianCalendar();
    Date d = thisday.getTime();
    DateFormat df = DateFormat.getDateInstance();
    String s = df.format(d);
    ??? System.out.println("Today is " + s);
    ? }
    }
    注意到,Date()構造函數和GregorianCalendar()構造函數很類似:都創建一個對象,條件簡單,代表今天。
    GregorianCalendar 類提供處理日期的方法。一個有用的方法是add().使用add()方法,你能夠增加象年,月數,天數到日期對象中。要使用add()方法,你必須提供要增加的字段,要增加的數量。一些有用的字段是DATE, MONTH, YEAR, 和 WEEK_OF_YEAR。下面的程序使用add()方法計算未來80天的一個日期。在Jules的<環球80天>是一個重要的數字,使用這個程序可以計算Phileas Fogg從出發的那一天1872 年10月2日后80天的日期:

    import java.util.*;
    import java.text.*;
    public class World {
    ? public static void main(String[] args) {
    GregorianCalendar worldTour = new GregorianCalendar(1872, Calendar.OCTOBER, 2);
    ??? worldTour.add(GregorianCalendar.DATE, 80);
    Date d = worldTour.getTime();
    DateFormat df = DateFormat.getDateInstance();
    String s = df.format(d);
    System.out.println("80 day trip will end " + s);
    ? }
    }
    add ()一個重要的副作用是它改變了原來的日期。有時候,擁有原始日期和修改后的日期很重要。不幸的是,你不能簡單的創建一個 GregorianCalendar對象,設置它和原來的相等(equal)。原因是兩個變量指向同一個Date()對象地址。如果Date對象改變,兩個變量就指向改變后的日期對象。代替這種做法,應該創建一個新對象。下面的程序示范了這種做法:import java.util.*;

    import java.text.*;

    public class ThreeDates {

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

    GregorianCalendar gc1 = new GregorianCalendar(2000, Calendar.JANUARY, 1);

    GregorianCalendar gc2 = gc1;

    GregorianCalendar gc3 = new GregorianCalendar(2000, Calendar.JANUARY, 1);
    ??? //Three dates all equal to January 1, 2000

    gc1.add(Calendar.YEAR, 1);

    //gc1 and gc2 are changed????

    DateFormat df = DateFormat.getDateInstance();

    Date d1 = gc1.getTime();

    Date d2 = gc2.getTime();

    Date d3 = gc3.getTime();

    String s1 = df.format(d1);

    String s2 = df.format(d2);

    String s3 = df.format(d3);

    System.out.println("gc1 is " + s1);

    System.out.println("gc2 is " + s2);

    System.out.println("gc3 is " + s3);

    ? }

    }

    ????? 程序運行后,gc1和gc2被變成2001年(因為兩個對象指向同一個Date,而Date已經被改變了)。對象gc3指向一個單獨的Date,它沒有被改變。
    package com.minght.sys.util;

    /**
    * <p>Title: 開源,開放</p>
    * <p>Description: opeansource</p>
    * <p>Copyright: Copyright (c) 2004</p>
    * <p>Company: ?海棠</p>
    * @author HaiTang Ming
    * @version 1.0
    */

    import java.util.*;
    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.sql.Timestamp;
    import java.text.*;


    public class timeUtil {

    /**
    ? * 將Date類型日期轉化成String類型"任意"格式
    ? * java.sql.Date,java.sql.Timestamp類型是java.util.Date類型的子類
    ? * @param date Date
    ? * @param format String
    ? *?????????? "2003-01-01"格式
    ? *?????????? "yyyy年M月d日"
    ? *?????????? "yyyy-MM-dd HH:mm:ss"格式
    ? * @return String
    ? */
    public static String dateToString(java.util.Date date,String format) {

    ??? if (date==null || format==null) {
    ????? return null;
    ??? }

    ??? SimpleDateFormat sdf = new SimpleDateFormat(format);
    ??? String str = sdf.format(date);
    ??? return str;
    }

    /**
    ? * 將String類型日期轉化成java.utl.Date類型"2003-01-01"格式
    ? * @param str String 要格式化的字符串
    ? * @param format String
    ? * @return Date
    ? */
    public static java.util.Date stringToUtilDate(String str,String format) {

    ??? if (str==null||format==null) {
    ????? return null;
    ??? }

    ??? SimpleDateFormat sdf = new SimpleDateFormat(format);

    ??? java.util.Date date = null;
    ??? try
    ??? {
    ????? date = sdf.parse(str);
    ??? }
    ??? catch(Exception e)
    ??? {
    ??? }
    ??? return date;
    }


    /**
    ? * 將String類型日期轉化成java.sql.Date類型"2003-01-01"格式
    ? * @param str String
    ? * @param format String
    ? * @return Date
    ? */
    public static java.sql.Date stringToSqlDate(String str,String format) {

    ??? if (str==null||format==null) {
    ????? return null;
    ??? }

    ??? SimpleDateFormat sdf = new SimpleDateFormat(format);

    ??? java.util.Date date = null;
    ??? try
    ??? {
    ????? date = sdf.parse(str);
    ??? }
    ??? catch(Exception e)
    ??? {
    ????? return null;
    ??? }
    ??? return new java.sql.Date(date.getTime());
    }

    /**
    ? * 將String類型日期轉化成java.sql.Date類型"2003-01-01"格式
    ? * @param str String
    ? * @param format String
    ? * @return Timestamp
    ? */
    public static java.sql.Timestamp stringToTimestamp(String str,String format) {

    ??? if (str==null||format==null) {
    ????? return null;
    ??? }

    ??? SimpleDateFormat sdf = new SimpleDateFormat(format);

    ??? java.util.Date date = null;
    ??? try
    ??? {
    ????? date = sdf.parse(str);
    ??? }
    ??? catch(Exception e)
    ??? {
    ????? return null;
    ??? }
    ??? return new java.sql.Timestamp(date.getTime());
    }


    /**
    ? * 將java.util.Date日期轉化成java.sql.Date類型
    ? * @param Date
    ? * @return 格式化后的java.sql.Date
    ? */
    public static java.sql.Date toSqlDate(Date date) {

    ??? if (date==null) {
    ????? return null;
    ??? }

    ? return new java.sql.Date(date.getTime());
    }
    /**
    ? * 將字符串轉化為時間格式 string to string
    ? * @param str String
    ? * @param format String
    ? * @return String
    ? */
    public static String toDateString(String str,String oldformat,String newformat){

    ??? return dateToString(stringToUtilDate(str,oldformat),newformat);

    }

    /**
    ? * 將日歷轉化為日期
    ? * @param calendar Calendar
    ? * @return Date
    ? */
    public static java.util.Date converToDate(java.util.Calendar calendar){
    ? return Calendar.getInstance().getTime();
    }

    /**
    ? * 將日期轉化為日歷
    ? * @param date Date
    ? * @return Calendar
    ? */
    public static java.util.Calendar converToCalendar(java.util.Date date){
    ? Calendar calendar = Calendar.getInstance();
    ? calendar.setTime(date);
    ? return calendar;
    }

    /**
    ? * 求得從某天開始,過了幾年幾月幾日幾時幾分幾秒后,日期是多少
    ? * 幾年幾月幾日幾時幾分幾秒可以為負數
    ? * @param date Date
    ? * @param year int
    ? * @param month int
    ? * @param day int
    ? * @param hour int
    ? * @param min int
    ? * @param sec int
    ? * @return Date
    ? */
    public static java.util.Date modifyDate(java.util.Date date,int year ,int month,int day,int hour,int min,int sec){
    ? Calendar cal = Calendar.getInstance();
    ? cal.setTime(date);
    ? cal.add(Calendar.YEAR,year);
    ? cal.add(Calendar.MONTH,month);
    ? cal.add(Calendar.DATE,day);
    ? cal.add(Calendar.HOUR,hour);
    ? cal.add(Calendar.MINUTE,min);
    ? cal.add(Calendar.SECOND,sec);

    ? return cal.getTime();

    }


    /**
    ? * 取得當前日期時間
    ? * 1:year
    ? * 2:month
    ? * 3:day
    ? */
    public static int getCurTime(int i) {
    ? if (i == 1) {
    ??? return java.util.Calendar.getInstance().get(Calendar.YEAR);
    ? }
    ? else if (i == 2) {
    ??? return java.util.Calendar.getInstance().get(Calendar.MONTH) + 1;
    ? }
    ? else if (i == 3) {
    ??? return java.util.Calendar.getInstance().get(Calendar.DATE);
    ? }
    ? return 0;

    }

    public static void main(String[] args){
    ? System.out.println(dateToString(modifyDate(Calendar.getInstance().getTime(),-1,-1,-1,-1,-1,-1),"yyyy-MM-dd HH:mm:ss"));

    }

    }

    加一:為了保證跨年的周屬于同一周,java API規定跨年的周都是新的一年的第一周,例如:
    ?public static void main(String[] args){
    ??Calendar c = Calendar.getInstance();
    ??c.set(2005, 11, 31);
    ??System.out.println(DateUtil.formatDate(c.getTime()));//2006.12.31
    ??System.out.println(c.get(Calendar.WEEK_OF_MONTH));//06年12月的最后一周
    ??System.out.println(c.get(Calendar.WEEK_OF_YEAR));//07年的第一周
    ?}

    posted @ 2006-12-12 11:20 保爾任 閱讀(496) | 評論 (0)編輯 收藏

    第一篇、http://www.blueidea.com/bbs/newsdetail.asp?id=996916(里面有很多例子)

    第二篇、徹底明白Java的IO系統(文摘)---JAVA之精髓IO流
    一. Input和Output
    1. stream代表的是任何有能力產出數據的數據源,或是任何有能力接收數據的接收源。在Java的IO中,所有的stream(包括Input和Out stream)都包括兩種類型:
    1.1 以字節為導向的stream
    以字節為導向的stream,表示以字節為單位從stream中讀取或往stream中寫入信息。以字節為導向的stream包括下面幾種類型:
    1) input stream:
    1) ByteArrayInputStream:把內存中的一個緩沖區作為InputStream使用
    2) StringBufferInputStream:把一個String對象作為InputStream
    3) FileInputStream:把一個文件作為InputStream,實現對文件的讀取操作
    4) PipedInputStream:實現了pipe的概念,主要在線程中使用
    5) SequenceInputStream:把多個InputStream合并為一個InputStream
    2) Out stream
    1) ByteArrayOutputStream:把信息存入內存中的一個緩沖區中
    2) FileOutputStream:把信息存入文件中
    3) PipedOutputStream:實現了pipe的概念,主要在線程中使用
    4) SequenceOutputStream:把多個OutStream合并為一個OutStream
    1.2 以Unicode字符為導向的stream
    以Unicode字符為導向的stream,表示以Unicode字符為單位從stream中讀取或往stream中寫入信息。以Unicode字符為導向的stream包括下面幾種類型:
    1) Input Stream
    1) CharArrayReader:與ByteArrayInputStream對應
    2) StringReader:與StringBufferInputStream對應
    3) FileReader:與FileInputStream對應
    4) PipedReader:與PipedInputStream對應
    2) Out Stream
    1) CharArrayWriter:與ByteArrayOutputStream對應
    2) StringWriter:無與之對應的以字節為導向的stream
    3) FileWriter:與FileOutputStream對應
    4) PipedWriter:與PipedOutputStream對應
    以字符為導向的stream基本上對有與之相對應的以字節為導向的stream。兩個對應類實現的功能相同,字是在操作時的導向不同。如CharArrayReader:和ByteArrayInputStream的作用都是把內存中的一個緩沖區作為InputStream使用,所不同的是前者每次從內存中讀取一個字節的信息,而后者每次從內存中讀取一個字符。
    1.3 兩種不現導向的stream之間的轉換
    InputStreamReader和OutputStreamReader:把一個以字節為導向的stream轉換成一個以字符為導向的stream。
    2. stream添加屬性
    2.1 “為stream添加屬性”的作用
    運用上面介紹的Java中操作IO的API,我們就可完成我們想完成的任何操作了。但通過FilterInputStream和FilterOutStream的子類,我們可以為stream添加屬性。下面以一個例子來說明這種功能的作用。
    如果我們要往一個文件中寫入數據,我們可以這樣操作:
    FileOutStream fs = new FileOutStream(“test.txt”);
    然后就可以通過產生的fs對象調用write()函數來往test.txt文件中寫入數據了。但是,如果我們想實現“先把要寫入文件的數據先緩存到內存中,再把緩存中的數據寫入文件中”的功能時,上面的API就沒有一個能滿足我們的需求了。但是通過FilterInputStream和FilterOutStream的子類,為FileOutStream添加我們所需要的功能。
    2.2 FilterInputStream的各種類型
    2.2.1 用于封裝以字節為導向的InputStream
    1) DataInputStream:從stream中讀取基本類型(int、char等)數據。
    2) BufferedInputStream:使用緩沖區
    3) LineNumberInputStream:會記錄input stream內的行數,然后可以調用getLineNumber()和setLineNumber(int)
    4) PushbackInputStream:很少用到,一般用于編譯器開發
    2.2.2 用于封裝以字符為導向的InputStream
    1) 沒有與DataInputStream對應的類。除非在要使用readLine()時改用BufferedReader,否則使用DataInputStream
    2) BufferedReader:與BufferedInputStream對應
    3) LineNumberReader:與LineNumberInputStream對應
    4) PushBackReader:與PushbackInputStream對應
    2.3 FilterOutStream的各種類型
    2.2.3 用于封裝以字節為導向的OutputStream
    1) DataIOutStream:往stream中輸出基本類型(int、char等)數據。
    2) BufferedOutStream:使用緩沖區
    3) PrintStream:產生格式化輸出
    2.2.4 用于封裝以字符為導向的OutputStream
    1) BufferedWrite:與對應
    2) PrintWrite:與對應
    3. RandomAccessFile
    1) 可通過RandomAccessFile對象完成對文件的讀寫操作
    2) 在產生一個對象時,可指明要打開的文件的性質:r,只讀;w,只寫;rw可讀寫
    3) 可以直接跳到文件中指定的位置
    4. I/O應用的一個例子
    import java.io.*;
    public class TestIO{
    public static void main(String[] args)
    throws IOException{
    //1.以行為單位從一個文件讀取數據
    BufferedReader in =
    new BufferedReader(
    new FileReader("F:\\nepalon\\TestIO.java"));
    String s, s2 = new String();
    while((s = in.readLine()) != null)
    s2 += s + "\n";
    in.close();

    //1b. 接收鍵盤的輸入
    BufferedReader stdin =
    new BufferedReader(
    new InputStreamReader(System.in));
    System.out.println("Enter a line:");
    System.out.println(stdin.readLine());

    //2. 從一個String對象中讀取數據
    StringReader in2 = new StringReader(s2);
    int c;
    while((c = in2.read()) != -1)
    System.out.println((char)c);
    in2.close();

    //3. 從內存取出格式化輸入
    try{
    DataInputStream in3 =
    new DataInputStream(
    new ByteArrayInputStream(s2.getBytes()));
    while(true)
    System.out.println((char)in3.readByte());
    }
    catch(EOFException e){
    System.out.println("End of stream");
    }

    //4. 輸出到文件
    try{
    BufferedReader in4 =
    new BufferedReader(
    new StringReader(s2));
    PrintWriter out1 =
    new PrintWriter(
    new BufferedWriter(
    new FileWriter("F:\\nepalon\\ TestIO.out")));
    int lineCount = 1;
    while((s = in4.readLine()) != null)
    out1.println(lineCount++ + ":" + s);
    out1.close();
    in4.close();
    }
    catch(EOFException ex){
    System.out.println("End of stream");
    }

    //5. 數據的存儲和恢復
    try{
    DataOutputStream out2 =
    new DataOutputStream(
    new BufferedOutputStream(
    new FileOutputStream("F:\\nepalon\\ Data.txt")));
    out2.writeDouble(3.1415926);
    out2.writeChars("\nThas was pi:writeChars\n");
    out2.writeBytes("Thas was pi:writeByte\n");
    out2.close();
    DataInputStream in5 =
    new DataInputStream(
    new BufferedInputStream(
    new FileInputStream("F:\\nepalon\\ Data.txt")));
    BufferedReader in5br =
    new BufferedReader(
    new InputStreamReader(in5));
    System.out.println(in5.readDouble());
    System.out.println(in5br.readLine());
    System.out.println(in5br.readLine());
    }
    catch(EOFException e){
    System.out.println("End of stream");
    }

    //6. 通過RandomAccessFile操作文件
    RandomAccessFile rf =
    new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
    for(int i=0; i<10; i++)
    rf.writeDouble(i*1.414);
    rf.close();

    rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
    for(int i=0; i<10; i++)
    System.out.println("Value " + i + ":" + rf.readDouble());
    rf.close();

    rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
    rf.seek(5*8);
    rf.writeDouble(47.0001);
    rf.close();

    rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
    for(int i=0; i<10; i++)
    System.out.println("Value " + i + ":" + rf.readDouble());
    rf.close();
    }
    }
    關于代碼的解釋(以區為單位):
    1區中,當讀取文件時,先把文件內容讀到緩存中,當調用in.readLine()時,再從緩存中以字符的方式讀取數據(以下簡稱“緩存字節讀取方式”)。
    1b區中,由于想以緩存字節讀取方式從標準IO(鍵盤)中讀取數據,所以要先把標準IO(System.in)轉換成字符導向的stream,再進行BufferedReader封裝。
    2區中,要以字符的形式從一個String對象中讀取數據,所以要產生一個StringReader類型的stream。
    4區中,對String對象s2讀取數據時,先把對象中的數據存入緩存中,再從緩沖中進行讀取;對TestIO.out文件進行操作時,先把格式化后的信息輸出到緩存中,再把緩存中的信息輸出到文件中。
    5區中,對Data.txt文件進行輸出時,是先把基本類型的數據輸出屋緩存中,再把緩存中的數據輸出到文件中;對文件進行讀取操作時,先把文件中的數據讀取到緩存中,再從緩存中以基本類型的形式進行讀取。注意in5.readDouble()這一行。因為寫入第一個writeDouble(),所以為了正確顯示。也要以基本類型的形式進行讀取。
    6區是通過RandomAccessFile類對文件進行操作。

    第三篇、花1K內存實現高效I/O的RandomAccessFile類(http://www-128.ibm.com/developerworks/cn/java/l-javaio/index.html),解決RandomAccessFile類效率低下的問題,特別是“與JDK1.4新類MappedByteBuffer+RandomAccessFile的對比”部分講了怎樣用jdk自己的功能實現。

    posted @ 2006-12-12 11:19 保爾任 閱讀(242) | 評論 (0)編輯 收藏
    初始化(initialization)其實包含兩部分:
    1.類的初始化(initialization class & interface)
    2.對象的創建(creation of new class instances)。
    因為類的初始化其實是類加載(loading of classes)的最后一步,所以很多書中把它歸結為“對象的創建”的第一步。其實只是看問題的角度不同而已。為了更清楚的理解,這里還是分開來。
    順序:
    應為類的加載肯定是第一步的,所以類的初始化在前。大體的初始化順序是:
    類初始化 -> 子類構造函數 -> 父類構造函數 -> 實例化成員變量 -> 繼續執行子類構造函數的語句
    下面結合例子,具體解釋一下。
    1。類的初始化(Initialization classes and interfaces),其實很簡單,具體來說有:
    (a)初始化類(initialization of class),是指初始化static field 和執行static初始化塊。
    例如:
    class Super {
    ??????? static String s = “initialization static field”; //初始化static field,其中“= “initialization static field” ”又叫做static field initializer
    ??????? // static初始化塊,又叫做static initializer,或 static initialization block
    ??????? static {
    ??????? System.out.println(“This is static initializer”);
    }
    }
    btw,有些書上提到static initializer 和 static field initializer 的概念,與之對應的還有 instance initializer 和 instance variable initializer。例子中的注釋已經解釋了其含義。
    (b)初始化接口(initialization of interface),是指初始化定義在該interface中的field。
    *注意*
    --initialization classes 時,該class的superclass 將首先被初始化,但其實現的interface則不會被初始化。
    --initialization classes 時,該class的superclass,以及superlcass的superclass 會首先被遞歸地初始化,從java.lang.Object一直到該class為止。但initialiazation interface的時候,卻不需如此,只會初始化該interface本身。
    --對于由引用類變量(class field)所引發的初始化,只會初始化真正定義該field的class。
    --如果一個static field是編譯時常量(compile-time constant)(即定義為static final field),則對它的引用不會引起定義它的類的初始化。
    為了幫助理解最后兩點,請試試看下面的例子:
    public class Initialization {
    ???????
    ??????? public static void main(String[] args) {
    ???????????????
    ??????????????? System.out.println(Sub.x); // Won't cause initialization of Sub, because x is declared by Super, not Sub.
    ???????????????????????????????????????? // 不會引起Sub類的初始化,因為x是定義在Super類中的
    ??????????????? System.out.println("-------------------------");
    ??????????????? System.out.println(Sub.y); // Won't cause initialization of Sub, because y is constant.
    ???????????????????????????????????????? // 不會引起Sub類的初始化,因為y是常量
    ??????????????? System.out.println("-------------------------");
    ??????????????? System.out.println(Sub.z = 2004); // Will cause initialization of Sub class
    ??// 將會引起Sub的初始化
    ??}
    }
    class Super{
    ??????? static int x = 2006;
    }
    class Sub extends Super {
    ???????
    ??????? static final int y = 2005;
    ???????
    static int z;
    ???????
    static {
    ??????????????? System.out.println("Initialization Sub");
    ??????? }
    }
    2。對象的創建(creation of new class instances),稍微有點煩瑣,具體的步驟如下
    (a) 所有的成員變量—包括該類,及它的父類中的成員變量--被分配內存空間,并賦予默認值。(Btw,這里是第一次初始化成員變量)
    (b) 為所調用的構造函數初始化其參數變量。(如果有參數)
    (c) 如果在構造函數中用this 調用了同類中的其他構造函數,則按照步驟(b)~(f)去處理被調用到的構造函數。
    (d) 如果在構造函數中用super調用了其父類的構造函數,則按照步驟(b)~(f)去處理被調用到的父類構造函數。
    (e) 按照書寫順序,執行instance initializer 和 instance variable initializer來初始化成員變量。(Btw,這里是第二次初始化成員變量)
    (f) 按照書寫順序,執行constructor的其余部分。
    *注意*
    成員變量其實都被初始化2次,第一次是賦予默認值,第二次才是你想要設定的值。
    最后看一個例子:
    public class InitializationOrder {
    ??????? public static void main(String[] args) {
    ??????????????? Subclass sb = new Subclass();
    ??????? }
    }
    class Super{
    ???????
    ??????? static {
    ??????????????? System.out.println(1);
    ??????? }
    ???????
    ??????? Super(int i){
    ??????????????? System.out.println(i);
    ??????? }
    }
    class Subclass extends Super implements Interface{
    ???????
    ??????? static {
    ??????????????? System.out.println(2);
    ??????? }???????
    ???????
    ??????? Super su = new Super(4);
    ???????
    ??????? Subclass() {
    ??????????????? super(3);
    ??????????????? new Super(5);
    ??????? }
    }
    interface Interface{
    ??????? static Super su = new Super(0);
    }
    稍微解釋一下:
    首先,Java虛擬機要執行InitializationOrder類中的static 方法main(),這引起了類的初始化。開始初始化InitializationOrder類。具體的步驟略去不說。
    接著,InitializationOrder類初始化完畢后,開始執行main()方法。語句Subclass sb = new Subclass()將創建一個Subclass對象。加載類Subclass后對其進行類初始化,但因為Subclass有一個父類Super,所以先初始化Super類,初始化塊static {System.out.println(1);}被執行,打印輸出1;
    第三,Super初始化完畢后,開始初始化Subclass類。static {System.out.println(2);}被執行,打印輸出2;
    第四,至此,類的加載工作全部完成。開始進入創建Subclass的對象過程。先為Subclass類和其父類Super類分配內存空間,這時Super su 被附值為null;
    第五,執行構造函數Subclass()時,super(3)被執行。如前面(d)所說,Super類的構造函數Super(int i){….}被調用,并按照步驟(b)~(f)來處理。因此,遞歸調用Super類的父類Object類的構造函數,并按照步驟(b)~(f)來初始化Object類,不過沒有任何輸入結果。最后打印輸出3;
    第六,如前面(e)所說,初始化成員變量su,其結果是打印輸出4;
    第七,如前面(f)所說,執行new Super(5),并打印輸出5;
    最后,Subclass雖然實現了接口Interface,但是初始化它的時候并不會引起接口的初始化,所以接口Interface中的static Super su = new Super(0)自始至終都沒有被執行到。
    max做的小改動:
    public class Test {
    ??? public static void main(String[] args) {
    ??????????? Subclass sb = new Subclass();
    ??? }
    }
    class SS{
    ?public SS(int i){
    ??System.out.println(i);
    ?}
    }
    class Super{
    ???
    ??? static {
    ??????????? System.out.println(1);
    ??? }
    ??? SS ss = new SS(100);
    ???
    ??? Super(int i){
    ??????????? System.out.println(i);
    ??? }
    }
    class Subclass extends Super implements Interface{
    ???
    ??? static {
    ??????????? System.out.println(2);
    ??? }???????
    ???
    ??? Super su = new Super(4);
    ???
    ??? Subclass() {
    ??????????? super(3);
    ??????????? new Super(5);
    ??? }
    }
    interface Interface{
    ??? static Super su = new Super(0);
    }
    --------------------
    結果為:
    1
    2
    100
    3
    100
    4
    100
    5
    posted @ 2006-12-12 11:13 保爾任 閱讀(361) | 評論 (0)編輯 收藏
    僅列出標題
    共8頁: 上一頁 1 2 3 4 5 6 7 8 下一頁 

    <2025年5月>
    27282930123
    45678910
    11121314151617
    18192021222324
    25262728293031
    1234567

    常用鏈接

    留言簿(4)

    隨筆分類

    隨筆檔案

    文章分類

    文章檔案

    搜索

    •  

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 亚洲午夜视频在线观看| 一区二区三区免费在线观看| 国产亚洲色视频在线| 97人伦色伦成人免费视频| 国产成人久久AV免费| 高清免费久久午夜精品| 亚洲男同gay片| 亚洲xxxx18| 精品亚洲A∨无码一区二区三区 | 亚洲一级黄色大片| 亚洲成av人片在线观看无码不卡| 免费在线观看a级毛片| 成人免费视频试看120秒| 国产乱子精品免费视观看片| 成人黄网站片免费视频| 中文字幕高清免费不卡视频| 污视频网站免费在线观看| 久久精品亚洲日本波多野结衣 | 亚洲精品视频免费在线观看| 一级毛片成人免费看免费不卡| 国产日韩一区二区三免费高清| eeuss影院www天堂免费| 男女作爱免费网站| 免费人成再在线观看网站| 羞羞视频免费观看| 无遮挡呻吟娇喘视频免费播放| 亚洲AV成人片无码网站| 色欲aⅴ亚洲情无码AV| 亚洲heyzo专区无码综合| 亚洲av无码专区亚洲av不卡| 亚洲国产欧美日韩精品一区二区三区 | 精品亚洲国产成AV人片传媒| 久久亚洲AV成人无码国产| 亚洲国产精品久久久久婷婷软件| 久久精品国产亚洲AV麻豆王友容| 亚洲AV日韩AV永久无码绿巨人| 亚洲国产一区二区三区青草影视 | 91av视频免费在线观看| 亚洲免费电影网站| 成年大片免费视频| 免费一区二区三区四区五区|