1. 程式人生 > >輸入/輸出(二)

輸入/輸出(二)

推回輸入流

在輸入/輸出流體系中,有兩個特殊的流與眾不同,就是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類裡提供瞭如下三個重定向標準輸入/輸出的方法:

  1. static void setErr(PrintStream err):重定向“標準”錯誤輸出流;
  2. static void setIn(InputStream in):重定向“標準”輸入流;
  3. 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)。

  1. 容量(capacity):緩衝區的容量(capacity)表示該Buffer的最大資料容量,即最多可以儲存多少資料。緩衝區的容量不可能為負值,建立後不能改變;
  2. 界限(limit):第一個不應該被讀出或者寫入的緩衝區位置索引。也就是說,位於limit後的資料既不可被讀,也不可被寫;
  3. 位置(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());
			//使