1. 程式人生 > >Tinking in Java ---Java的NIO和對象序列化

Tinking in Java ---Java的NIO和對象序列化

asc class文件 tle server double 大數據 效率 是否 ssh

前面一篇博客的IO被稱為經典IO,因為他們大多數都是從Java1.0開始就有了的;然後今天這篇博客是關於NIO的,所以的NIO其實就是JDK從1.4開始,Java提供的一系列改進的輸入/輸出處理的新功能,這些新功能被統稱為新IO(New IO ,簡稱NIO)。另一個概念對象序列化指的是將那些實現了Serializable接口的對象轉換成一個字節序列,並能夠在以後將這個字節序列再轉換成原來的對象。這樣的話我們就可以將對象寫入磁盤中,或者是將對象在網絡上進行傳遞。下面就對這兩個內容進行總結。

一.Java的NIO
java的NIO之所以擁有更高的效率,是因為其所使用的結構更接近與操作系統的IO方式:使用通道和緩沖器。

通道裏面放有數據,但是我們不能直接與它打交道,無論是從通道中取數據還是放數據,我們都必須通過緩沖器進行,更嚴格的是緩沖器中存放的是最原始的字節數據而不是其它類型。其中Channel類對應我們上面講的通道,而Buffer類則對應緩沖器,所以我們有必要了解一下這兩個類。
(1).Buffer類
從底層的數據結構來看,Buffer像是一個數組,我們可以在其中保存多個相同類型的數據。Buffer類是一個抽象數組,它最常用的子類是ByteBuffer,這個類存取的最小單位是字節,正好用於和Channel打交道。當然除了ByteBuffer外,其它基本類型(除boolean外)都有自己對應的Buffer:CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。使用這些類型我們可以很方便的將基本類型的數據放入ByteBuffer中去.Buffer類還有一個子類MappedByteBuffer,這個子類用於表示Channel將磁盤文件的部分或全部內容得到的結果。
Buffer中有三個概念比較重要:容量(capacity),界限(limit)和位置(positiion)。
容量指的緩沖區的大小,即該Buffer能裝入的最大數據量。
界限指的是當前裝入的最後一個數據位置加1的那個值,表示的是第一個不應該被讀或寫的位置。
位置用於表明下一個可以被讀出或寫入的緩沖區位置索引。
Buffer的主要功能就是裝數據,然後輸出數據。所以我們有必要了解一下這個具體的過程:首先Buffer的positiion為0,limit等於capacity,程序可以通過put()方法向Buffer中放入一些數據(或者是從Channel中取出一些數據),在這個過程中position會往後移動。當Buffer裝入數據結束以後,調用Buffer的flip()方法為輸出數據做好準備,這個方法會把limit設為position,將position設為limit。當輸出數據結束後,Buffer調用clear()方法,clear()方法不會情況所有的數據,只會把position設為0,將limit設為capacity,這樣又為向Buffer中輸入數據做好了準備。
另外指的註意的是Buffer的子類是沒有構造函數的,所以不能顯式的聲明一個Buffer。下面的這份代碼展示了CharBuffer的基本用法:

package lkl;
import java.nio.*;

public class BufferTest {

    public static void main(String[] args){

        //通過靜態方法創建一個CharBuffer
        System.out.println("創建buffer之後: ");
        CharBuffer buffer = CharBuffer.allocate(10);
        System.out.println("position: "+buffer.position());
        System.out.println("limit: "+buffer.limit());
        System.out.println("capacity: "+buffer.capacity());

        ///向buffer裏放三個字符
        buffer.put("a");
        buffer.put("b");
        buffer.put("c");

        ///為使用buffer做準備
        System.out.println();
        System.out.println("在向buffer中裝入數據並調用flip()方法後: ");
        buffer.flip();
        System.out.println("position: "+buffer.position());
        System.out.println("limit: "+buffer.limit());
        System.out.println("capacity: "+buffer.capacity());

        ///讀取buffer中的元素,以絕對方式和相對方式兩種
        //絕對方式不會改變position指針的
        //而相對方式每次都會讓position指針後移一位
        System.out.println(buffer.get());
        System.out.println(buffer.get(2));

        System.out.println();
        System.out.println("調用clear()後: ");
        //調用clear()方法,為再次向buffer中輸入數據做準備
        //但是這個方法只是移動各個指針的位置,而不會清空緩沖區中的數據
        buffer.clear();
        System.out.println("position: "+buffer.position());
        System.out.println("limit: "+buffer.limit());
        System.out.println("capacity: "+buffer.capacity());

        ///clear()方法沒有清空緩沖區
        //所以還可以通過絕對方式來訪問緩沖區裏面的內容
        System.out.println(buffer.get(2));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

一般都是用的ByteBuffer,所以需要先將數據轉成字節數組後在放入,但是對應基本數據類型可以使用ByteBuffer類的asXXXBuffer簡化這個過程。如下面的代碼所示:

package lkl;

import java.nio.ByteBuffer;

//向Channel中寫入基本類型的數據
//向ByteBuffer中插入基本類型數據的最簡單的方法就是:利用ascharBuffer()
//asShortBuffer()等獲得該緩沖器上的視圖,然後調用該視圖的put()方法
//short類型需要轉一下型,其它基本類型不需要
public class GetData {

    public static void main(String[] args){
        ByteBuffer buff = ByteBuffer.allocate(1024);

        ///讀取char型數據
        buff.asCharBuffer().put("java");
        //buff.flip(); //這時候不需要flip()
        char c;
        while((c=buff.getChar())!=0){
            System.out.print(c+" ");
        }
        System.out.println();
        buff.rewind();

        //讀取short型數據
        buff.asShortBuffer().put((short)423174);
        System.out.println(buff.getShort());
        buff.rewind();

        //讀取long型數據
        buff.asLongBuffer().put(689342343);
        System.out.println(buff.getLong());
        buff.rewind();

        //讀取float型數據
        buff.asFloatBuffer().put(2793);
        System.out.println(buff.getFloat());
        buff.rewind();

        //讀取double型數據
        buff.asDoubleBuffer().put(4.223254);
        System.out.println(buff.getDouble());
        buff.rewind();
    }/*Output
    j a v a 
    29958
    689342343
    2793.0
    4.223254
          */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

當然Buffer類還有其它的很多方法,可以通過它的API文檔來進行了解。反正現在我們知道了要想跟Channel打交道,必須要使用Buffer。

(2).Channel類
Channel類對應我們開頭說的通道了,註意到Channel類是面向字節流的,所以並不是我們前面學習的所有IO類都可以轉換成Channel的。實際上Java為Channel提供了FileChannel,DataGramChannel,selectableChannel,ServerSocketChannel,SocketChannel等實現類。在這裏我們只了解FileChannel,它可以通過FileInputStream,FileOutputStream,RandomAccessFile這幾個類的getChannel()方法得到;當然這幾個類得到的對應的FileChannel對象在功能上也是不同的,FileOutputStream對應的FileChannel只能向文件中寫入數據,FileInputStream對應的FileChannel只能向文件中讀數據,而RandomAccessFile對應的FileChannel對文件即能讀又能寫。這也說明這個類也是沒有構造器可以調用的。下面的代碼演示了如何使用Channel向文件中寫數據和讀取文件中的數據:

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;


///Channel是java提供的新的一種流對象
//Channel可以將文件部分或則全部映射為Channel
///但是我們不能直接與Channel打交道,無論是讀寫都需要通過Buffer才行
///Channel不通過構造器來獲得,而是通過傳統結點的InputStream,OutputStream的getChannel()方法獲得
public class FileChannelTest {

    public static void main(String[] args){

         File f= new File("/Test/a.java");

         try( 
                  ///創建FIleInputStream,以該文件輸入流創建FileChannel
                   FileChannel  inChannel = new FileInputStream(f).getChannel();

                 ///以文件輸出流創建FileChannel,用以控制輸出
                 FileChannel outChannel = new FileOutputStream("/Test/test.txt").getChannel())
                 {
                     ///將FileChannel裏的全部數據映射成ByteBuffer
                       MappedByteBuffer buffer   = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());

                       ///使用GBK字符集來創建解碼器
                       Charset charset = Charset.forName("GBK");

                       ///將buffer中的內容全部輸出
                       outChannel.write(buffer);

                       buffer.clear();

                       ///創建解碼器對象
                       CharsetDecoder decoder = charset.newDecoder();

                       ///使用解碼器將ByteBuffer轉換成CharBuffer
                       CharBuffer charbuffer = decoder.decode(buffer);

                       ///CharBuffer中的toString方法可以獲得對應的字符串
                       System.out.println(charbuffer.toString());
                 }
                catch(IOException e){
                    e.printStackTrace();
                }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

註意到上面的代碼中使用了解碼,這是因為ByteBuffer中裝的是字節,所以如果我們直接輸出則會產生亂碼,如果想從ByteBuffer中讀取到正確得內容,那麽就需要進行編碼。有兩種形式,第一種是在將數據寫入ByteBuffer中時就進行編碼;第二種是從ByteBuffer中讀出後進行解碼。至於編碼解碼的話可以使用Charset類進行。如下面的代碼所示:

package lkl;

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.io.*;

///FileChannel轉換數據類型
//FileChannel的寫入類型只能是ByteBuffer,所以就引生出來編碼解碼的問題
public class BufferToText {

    private static final int SIZE =1024;
    public static void main(String[] args) throws IOException{
        FileChannel fc = new FileInputStream("/Test/b.txt").getChannel();
        ByteBuffer buff = ByteBuffer.allocate(SIZE);
        fc.read(buff);
        buff.flip();
        ///將ByteBuffer轉成CharBuffer,但是實際上沒有實現類型的轉換,輸出亂碼
        System.out.println(buff.asCharBuffer());

        buff.rewind(); //指針返回開始位置,為解碼做準備    
        //輸出時解碼,使得字節正確的轉換成字符
        String encoding = System.getProperty("file.encoding");
        System.out.println("Decoded using "+encoding+": \n"+Charset.forName(encoding).decode(buff));

        buff.clear();
        //輸入時進行編碼,使得字節正確的轉換成字符
        fc= new FileOutputStream("/Test/a1.txt").getChannel();
        buff.put("some txt".getBytes("UTF-8"));  ///將字符轉成字節時進行編碼
        buff.flip();
        fc.write(buff);
        fc.close();
        fc= new FileInputStream("/Test/a1.txt").getChannel();
        buff.clear();
        fc.read(buff);
        buff.flip();
        System.out.println(buff.asCharBuffer()); //進行編碼以後再轉換就不會有問題了

        ///如果直接使用CharBuffer進行寫入的話,也不會有編碼的問題
        fc = new FileOutputStream("/Test/a1.txt").getChannel();
        buff.clear();
        buff.asCharBuffer().put("this is test txt");
        fc.write(buff);

        fc = new FileInputStream("/Test/a1.txt").getChannel();
        buff.clear();
        fc.read(buff);
        buff.flip();
        System.out.println(buff.asCharBuffer());
        fc.close();
    }
    /*
        瑨楳?猠瑥獴?楬攊
        Decoded using UTF-8: 
        this is test file

        獯浥?硴
        this is test txt
     */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

(3).關於大端序和小端序的問題
大端序(高位優先)和小端序(低位優先)的問題
大端序是指將重要的字節放在地址最低的存儲單元
小端序是指將重要的字節放在地址最高的存儲單元
ByteBuffer是以大端序的形式存儲數據的。
舉個例子:00000000 01100001
上面這組二進制數據表示short型整數(一個數8位)
如果采用大端序表示97,如果是小端序則表示(0110000100000000)24832
下面的代碼演示了大端序和小端序的比較:

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

//我們可以采用帶有參數的ByteOrder.BIG_ENDIAN 或ByteOrder.LITTLE_ENDIAN的
//oder()方法改變ByteBuffer的字節排序方式
public class Endians {

    public static void main(String[] args){

        ByteBuffer bb  =ByteBuffer.allocate(12);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));

        bb.rewind();
        bb.order(ByteOrder.BIG_ENDIAN);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));

        bb.rewind();
        bb.order(ByteOrder.LITTLE_ENDIAN);
        bb.asCharBuffer().put("abcdef");
        System.out.println(Arrays.toString(bb.array()));
    }
}/*Output
    [0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0]
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

數據在網上傳輸時用的也是大端序(高位優先)。

二.對象序列化問題
對象序列化指的是將那些實現了Serializable接口的對象轉換成一個字節序列,並能夠在以後將這個字節序列完全恢復為原先的對象。利用對象的序列化可以實現輕量級的持久性。“持久性”意味著一個對象的生存周期並不取決與程序是否正在執行;他可以生存在程序的調用之間。通過將一個序列化的對象寫入磁盤然後在重新調用程序時恢復該對象就可以實現持久性的過程。之所以稱為”輕量級”,是因為沒有一個關鍵字可以方便的實現這個過程,整個過程還需要我們手動維護。
總的來說一般的序列化是沒有什麽很困難的,我們只要然相應的類繼承一下Serializable接口就行了,而這個接口是一個標記接口,並不需要實現什麽具體的內容,然後調用ObjectOutputStream將對象寫入文件(序列化),如果想要恢復就用ObjectInputStream從文件中讀取出來(反序列化);註意這兩個類都是包裝流,需要傳入其它的結點流。如下面的代碼所示,從輸出來看,反序列化後對象的確實和原來的對象是一樣的:

package lkl;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.*;

class Base implements Serializable{
    private int i;
    private int j;
    public Base(int i,int j){
        this.i=i;
        this.j=j;
    }

    public String toString(){
        return"[ "+ i+" "+j+" ]";

    }
}

public class Test {
    public  static void main(String[] args) throws IOException,ClassNotFoundException{
        Base base =new Base(1,3);
        System.out.println(base);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/Test/Base.out"));
        out.writeObject(base); //將對像寫入磁盤

       ObjectInputStream in = new ObjectInputStream(new FileInputStream("/Test/Base.out"));
       Base base1 =(Base)in.readObject(); ///將對象從磁盤讀出
       System.out.println(base1);
    }/*
       [ 1 3 ]
       [ 1 3 ]
    */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

除了實現Serializable接口外我們也可以通過實現Externalizable接口來實現序列話,這個接口運行我們自己對序列化的過程進行控制,我們手動的選擇對那些變量進行序列化和反序列化。這些是依據這個接口中的兩個函數:writeExternal()和readExternal()函數實現的。下面的代碼演示了Externalizable接口的簡單實現,要註意Blip1和Blip2類是有輕微不同的:

Constructin objects: 
Blip1 Constructor
Blip2.Constructor
Saving objects: 
Blip1.writeExternal
Blip2.writeExternal
Recovering p1: 
Blip1 Constructor
Blip1.readExternal
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我們可以看到在反序列化的過程中,是會調用默認構造器的,如果沒有默認構造器可以調用(權限不為public)則在反序列的過程中,會出錯。

另外如果我們實現的是Serializable接口但是我們希望某些變量不進行序列化,那麽我們就可以用transient關鍵字對它們進行修飾。然後還要註意的是對於實現了Serializable接口的量,static變量是不會自動序列化的,我們必須手動進行序列化和反序列化才行。下面的代碼演示了這兩點:

package lkl;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.*;

class Base implements Serializable{
    private int i;
    private transient int j;
    private static int k=9;
    public Base(int i,int j){
        this.i=i;
        this.j=j;
    }

    public String toString(){
        return"[ "+ i+" "+j+" "+k+" ]";
    }
}

public class Test {
    public  static void main(String[] args) throws IOException,ClassNotFoundException{
        Base base =new Base(1,3);
        System.out.println(base);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("/Test/Base.out"));
        out.writeObject(base); //將對像寫入磁盤

       ObjectInputStream in = new ObjectInputStream(new FileInputStream("/Test/Base.out"));
       Base base1 =(Base)in.readObject(); ///將對象從磁盤讀出
       System.out.println(base1);
    }/*
       [ 1 3 9 ]
       [ 1 0 9 ]
    */
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

下面的代碼演示了在Serializable接口中我們也可以通過自己編寫方法來控制序列化和反序列的過程(感覺很亂):

package lkl;
import java.io.*;

//通過在Serializable接口的實現中添加以下兩個方法:
//private void writeObject(ObjectOutputStream stream) throws IOException
//private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException
//就可以在這兩個方法中自己自定需要序列化和反序列化的元素
///在writeObject()中調用defaultWriteObject()就可以選擇執行默認的writeObject()
//在readObject()中調用defaultReadObject()就可以執行默認的readObject()
public class SerialCtl implements Serializable{

    private String a;
    private transient String b;
    public SerialCtl(String aa,String bb){
        a="Not Transient: "+aa; 
        b="transient: "+bb;
    }

    public String toString(){
        return a+"\n"+b;
    }

    private void writeObject(ObjectOutputStream stream) throws IOException{
        stream.defaultWriteObject();
        stream.writeObject(b);
    }

    private void readObject(ObjectInputStream stream) throws IOException,ClassNotFoundException{
        stream.defaultReadObject();
        b=(String)stream.readObject();
    }

    public static void main(String[] args)throws IOException,ClassNotFoundException{
        SerialCtl sc = new SerialCtl("Test1","Test2");
        System.out.println("Before: ");
        System.out.println(sc);

        //這次序列化信息不存到文件,而是存到緩沖區去
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(buf);
        o.writeObject(sc);

        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
        SerialCtl sc2 =(SerialCtl)in.readObject();
        System.out.println("After: ");
        System.out.println(sc2);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

最後需要強調的是:如果我們有很多個可以序列化的對象存在相互引用關系,序列化時只需要將他們統一打包進行序列化就可以,系統會自動維護一個序列化關系的網絡。然後我們進行反序列化時,其實系統還是通過.class文件獲得這個對象相應的信息的。

Tinking in Java ---Java的NIO和對象序列化