輸入/輸出(二)
推回輸入流
在輸入/輸出流體系中,有兩個特殊的流與眾不同,就是PushbackReader和PushbackInputStream:
這兩個推回輸入流都帶有一個推回緩衝區,當程式呼叫這兩個推回輸入流的unread()方法時,系統將會把指定陣列的內容推回到該緩衝區裡,而推回輸入流每次呼叫read()方法時總是先從推回緩衝區讀取,只有完全讀取了推回緩衝區的內容後,但還沒有裝滿read()所需的陣列時才會從原輸入流中讀取。
當程式建立一個PushbackReader和PushbackInputStream時需要指定推回緩衝區的大小,預設的推回緩衝區的長度為1。如果程式中推回緩衝區的內容超出了推回緩衝區的大小,將會引發Pushback buffer overflow的IOException異常。
PushbackTest.java
public class PushbackTest
{
public static void main(String[] args)
{
try(PushbackReader pr = new PushbackReader(new FileReader("PushbackTest.java") , 64);)
{
//建立一個PushbackReader物件,指定推回緩衝區的長度為64
char[] buf = new char[32];
//用以儲存上次讀取的字串內容
String lastContent = "";
int hasRead = 0;
//迴圈讀取檔案內容
while ((hasRead = pr.read(buf)) > 0)
{
//將讀取的內容轉換成字串
String content = new String(buf , 0 , hasRead);
int targetIndex = 0;
//將上次讀取的字串和本次讀取的字串拼起來,檢視是否包含目標字串
//如果包含目標字串
if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0)
{
//將本次內容和上次內容一起推回緩衝區
pr.unread((lastContent + content).toCharArray());
//再次讀取指定長度的內容(就是目標字串之前的內容)
pr.read(buf , 0 , targetIndex);
//列印讀取的內容
System.out.print(new String(buf , 0 ,targetIndex));
System.exit(0);
}
else
{
//列印上次讀取的內容
System.out.print(lastContent);
//將本次內容設為上次讀取的內容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
重定向標準輸入/輸出
Java的標準輸入/輸出分別通過System.in和System.out來代表,在預設的情況下它們分別代表鍵盤和顯示器,當程式通過System.in來獲取輸入時,實際上是從鍵盤讀取輸入;當程式試圖通過System.out執行輸出時,程式總是輸出到螢幕;
在System類裡提供瞭如下三個重定向標準輸入/輸出的方法:
- static void setErr(PrintStream err):重定向“標準”錯誤輸出流;
- static void setIn(InputStream in):重定向“標準”輸入流;
- static void setOut(PrintStream out):重定向“標準”輸出流;
RedirectOut.java
public class RedirectOut
{
public static void main(String[] args)
{
try(PrintStream ps = new PrintStream(new FileOutputStream("out.txt"));) //建立PrintStream輸出流
{
//將標準輸出重定向到ps輸出流
System.setOut(ps);
//向標準輸出輸出一個字串
System.out.println("普通字串");
//向標準輸出輸出一個物件
System.out.println(new RedirectOut());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
RedirectIn.java
public class RedirectIn
{
public static void main(String[] args)
{
try(FileInputStream fis = new FileInputStream("RedirectIn.java"))
{
//將標準輸入重定向到fis輸入流
System.setIn(fis);
//使用System.in建立Scanner物件,用於獲取標準輸入
Scanner sc = new Scanner(System.in);
//增加下面一行將只把回車作為分隔符
sc.useDelimiter("\n");
//判斷是否還有下一個輸入項
while(sc.hasNext())
{
//輸出輸入項
System.out.println("鍵盤輸入的內容是:" + sc.next());
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
執行,程式不會等待使用者輸入,而是直接輸出了RedirectIn.java檔案的內容,這表明程式不再使用鍵盤作為標準輸入,而是使用RedirectIn.java作為標準輸入源。
RandomAccessFile
RandomAccessFile是Java輸入/輸出流體系中功能最豐富的檔案內容訪問類,它提供了眾多的方法來訪問檔案內容,它既可以讀取檔案內容,也可以向檔案輸出資料。與普通的輸入/輸出流不同的是,RandomAccessFile支援“隨機訪問”的形式,程式可以直接跳轉到檔案的任意地方來讀寫資料。
由於 RandomAccessFile可以自由訪問檔案的任意位置,所以如果只需要訪問檔案部分內容,而不是把檔案從頭都到尾,使用RandomAccessFile將是更好的選擇。
RandomAccessFile允許自由定位檔案記錄指標,RandomAccessFile可以不從開始的地方開始輸出,因此RandomAccessFile可以向已存在的檔案後追加內容。如果程式需要向已存在的檔案後追加內容,則應該使用RandomAccessFile。
注:RandomAccessFile的方法雖多,但它有一個很大的侷限,就是隻能讀寫檔案,不能讀寫其他IO節點。
RandomAccessFileTest.java
public class RandomAccessFileTest
{
public static void main(String[] args)
{
//以只讀方式開啟一個RandomAccessFile物件
try(RandomAccessFile raf = new RandomAccessFile("RandomAccessFileTest.java" , "r");)
{
//獲取RandomAccessFile物件檔案指標的位置,初始位置是0
System.out.println("RandomAccessFile的檔案指標的初始位置:"
+ raf.getFilePointer());
//移動raf的檔案記錄指標的位置
raf.seek(300);
byte[] bbuf = new byte[1024];
//用於儲存實際讀取的位元組數
int hasRead = 0;
//使用迴圈來重複“取水”過程
while ((hasRead = raf.read(bbuf)) > 0 )
{
//取出“竹筒”中水滴(位元組),將位元組陣列轉換成字串輸出!
System.out.print(new String(bbuf , 0 , hasRead ));
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
程式將從300位元組處開始讀。執行,看到程式只讀取後面部分的效果;
AppendContent.java
public class AppendContent
{
public static void main(String[] args)
{
//以讀、寫方式開啟一個RandomAccessFile物件
try(RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
{
//將記錄指標移動的out.txt檔案的最後
raf.seek(raf.length());
raf.write("追加的內容!\r\n".getBytes());
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
InsertContent.java
public class InsertContent
{
public static void insert(String fileName , long pos ,
String insertContent)throws IOException
{
//建立一個臨時檔案來儲存插入點後的資料
File tmp = File.createTempFile("tmp" , null);
tmp.deleteOnExit();
try(RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp);)
{
raf.seek(pos);
//--------下面程式碼將插入點後的內容讀入臨時檔案中儲存---------
byte[] bbuf = new byte[64];
//用於儲存實際讀取的位元組數
int hasRead = 0;
//使用迴圈方式讀取插入點後的資料
while ((hasRead = raf.read(bbuf)) > 0 )
{
//將讀取的資料寫入臨時檔案
tmpOut.write(bbuf , 0 , hasRead);
}
//----------下面程式碼插入內容----------
//把檔案記錄指標重新定位到pos位置
raf.seek(pos);
//追加需要插入的內容
raf.write(insertContent.getBytes());
//追加臨時檔案中的內容
while ((hasRead = tmpIn.read(bbuf)) > 0 )
{
raf.write(bbuf , 0 , hasRead);
}
}
}
public static void main(String[] args) throws IOException
{
insert("InsertContent.java" , 45 , "插入的內容\r\n");
}
}
每次執行,都會看到InsertContent.java中插入了一行字串:
序列化
序列化機制允許將實現序列化的Java物件轉換成位元組序列,這些位元組序列可以儲存在磁碟上,或通過網路傳輸,以備以後重新恢復成原來的物件。序列化機制使得物件可以脫離程式的執行而獨立存在。
物件的序列化(Serialize)指將一個Java物件寫入IO流中,與此對應的是,物件的反序列化(Deserialize)則指從IO流中恢復該Java物件。
如果需要讓某個物件支援序列化機制,則必須讓它的類是可序列化的。為了讓某個類是可序列化的,該類必須實現如下兩個介面之一:
- Serializable
- Externalizable
大部分基本都採用Serializable介面方式來實現序列化;
使用物件流實現序列化
一旦某個類實現了Serializable介面,該類的物件就是可序列化的,程式可以通過如下兩個步驟來序列化該物件:
1:建立一個ObjectOutputStream,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎之上;
//建立一個ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
2:呼叫ObjectOutputStream物件的writeObject()方法輸出可序列化物件;
//將per物件寫入輸出流
oos.writeObject(per);
Person.java
public class Person
implements java.io.Serializable
{
private String name;
private int age;
public Person(String name , int age)
{
System.out.println("有引數的構造器");
this.name = name;
this.age = age;
}
}
WriteObject.java
public class WriteObject
{
public static void main(String[] args)
{
//建立一個ObjectOutputStream輸出流
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt")))
{
Person per = new Person("孫悟空", 500);
//將per物件寫入輸出流
oos.writeObject(per);
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
}
執行,將會生成一個object.txt檔案,該檔案的內容就是Person物件:
如果希望從二進位制流中恢復Java物件,則需要使用反序列化。反序列化的步驟如下:
1:建立一個ObjectInputStream,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎之上;
//建立一個ObjectInputStream輸出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
2:呼叫ObjectInputStream物件的readObject()方法讀取流中的物件,該方法返回一個Object型別的Java物件,如果程式知道該Java物件的型別,則可以將該物件強制型別轉換成其真實的型別;
//從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
Person p = (Person)ois.readObject();
ReadObject.java
public class ReadObject
{
public static void main(String[] args)
{
//建立一個ObjectInputStream輸出流
try(ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("object.txt"));)
{
//從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
Person p = (Person)ois.readObject();
System.out.println("名字為:" + p.getName()
+ "\n年齡為:" + p.getAge());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
物件引用的序列化
如果某個類的成員變數的型別不是基本型別或String型別,而是另一個引用型別,那麼這個引用型別必須是可序列化的,否則擁有該型別成員變數的類也是不可序列化的;
版本
反序列化Java物件時必須提供該物件的class檔案,現在的問題是,隨著專案的升級,系統的class檔案也會升級,Java如何保證兩個class檔案的相容性?
Java序列化機制允許為序列化類提供一個private static final的serialVersionUID值,該類變數的值用於標識該Java類的序列化版本,也就是說,如果一個類升級後,只要它的serialVersionUID類變數的值保持不變,序列化機制也會把它們當成同一個序列化版本。
private static final long serialVersionUID = 512L;
NIO
使用Buffer
在Buffer中有三個重要的概念:容量(capacity)、界限(limit)和位置(position)。
- 容量(capacity):緩衝區的容量(capacity)表示該Buffer的最大資料容量,即最多可以儲存多少資料。緩衝區的容量不可能為負值,建立後不能改變;
- 界限(limit):第一個不應該被讀出或者寫入的緩衝區位置索引。也就是說,位於limit後的資料既不可被讀,也不可被寫;
- 位置(position):用於指明下一個可以被讀出的或者寫入的緩衝區位置索引(類似於IO流中的記錄指標)。當使用Buffer從Channel中讀取資料時,position的值恰好等於已經讀到 了多少資料。當剛剛新建一個Buffer物件時,其position為0;如果從Channel中讀取了2個數據到該Buffer中,則position為2,指向Buffer中第三個(第一個位置的索引為0)位置。
BufferTest.java
public class BufferTest
{
public static void main(String[] args)
{
//建立Buffer
CharBuffer buff = CharBuffer.allocate(8); //1
System.out.println("capacity: "
+ buff.capacity());
System.out.println("limit: "
+ buff.limit());
System.out.println("position: "
+ buff.position());
//放入元素
buff.put('a'); //2
buff.put('b'); //3
buff.put('c'); //4
System.out.println("加入三個元素後,position = "
+ buff.position());
//呼叫flip()方法
buff.flip(); //5
System.out.println("執行flip()後,limit = "
+ buff.limit());
System.out.println("position = "
+ buff.position());
//取出第一個元素
System.out.println("第一個元素(position=0):"
+ buff.get()); //6
System.out.println("取出一個元素後,position = "
+ buff.position());
//呼叫clear方法
buff.clear(); //7
System.out.println("執行clear()後,limit = "
+ buff.limit());
System.out.println("執行clear()後,position = "
+ buff.position());
System.out.println("執行clear()後,緩衝區內容並沒有被清除:"
+ buff.get(2)); //8
System.out.println("執行絕對讀取後,position = "
+ buff.position());
}
}
1號程式碼處: CharBuffer的一個靜態方法allocate()建立了一個capacity為8的CharBuffer。此時:
4號程式碼處取出一個元素後position向後移動一位;
使用Channel
FileChannelTest.java
public class FileChannelTest
{
public static void main(String[] args)
{
FileChannel inChannel = null;
FileChannel outChannel = null;
try
{
File f = new File("FileChannelTest.java");
//建立FileInputStream,以該檔案輸入流建立FileChannel
inChannel = new FileInputStream(f)
.getChannel();
//將FileChannel裡的全部資料對映成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY,
0 , f.length());
//使