Java基礎--I/O流知識總結
Java基礎–I/O流知識總結
引言
I/O(輸入/輸出)應該算是所有程式都必需的一部分,使用輸入機制,允許程式讀取外部的資料資源、接收使用者輸入;使用輸出機制,允許程式記錄允許狀態,並將資料輸出到外部裝置。Java的IO是通過java.io包下的類和介面來實現的,其下主要包括輸入、輸出兩種IO流,根據流中操作的資料單元的不同又分為位元組流(8位的位元組)和字元流(16位的字元)。
Java中IO的結構體系
位元組流(InputStream/OutputStream)
首先來看一看位元組輸入流InpuStream
,此抽象類是表示位元組輸入流的所有類的超類,它有如下三個基本方法。
int read()
int read(byte[] b)
:從輸入流中最多讀取b.length
個位元組的資料,並將其儲存在緩衝區陣列b中,返回實際讀取的位元組數。int read(byte[] b,int off,int len)
:將輸入流中最多len
個數據位元組讀入緩衝區陣列b中,off
為陣列 b 中將寫入資料的初始偏移量,返回實際讀取的位元組數。
位元組輸出流OutputStream
,此抽象類是表示位元組輸出流的所有類的超類,它有如下三個基本方法。
void write(int c)
:將指定的位元組寫入此輸出流。void write(byte[] buf)
b.length
個位元組從指定的byte
陣列寫入此輸出流。void write(byte[] buf,int off,int len)
:將指定byte
陣列中從偏移量off
開始的len
個位元組寫入此輸出流。
接下來逐個介紹位元組流的各個子類。
FileInputStream/FileOutputStream
這兩個類直接繼承自InputStream
和OutputStream
,可實現對檔案的讀取和資料寫入。如下程式碼即可實現對檔案的複製操作。
package cn.lincain.io; import java.io.FileInputStream; import java.io.FileOutputStream; public class FileStreamTest { public static void main(String[] args) { try ( // 建立一個位元組輸入流 FileInputStream fis = new FileInputStream("FileStreamTest .java"); // 建立一個位元組輸出流 FileOutputStream fos = new FileOutputStream("copy.txt")) { int hasRead = 0; // 從ByteStreamTest.java檔案讀取資料到流中 while ((hasRead = fis.read()) != -1) { // 將資料寫入到指定的檔案中 fos.write(hasRead); } } catch (Exception e) { e.printStackTrace(); } } }
我們將輸入流比作是一個水管,裡面裝的水就是檔案的資料,read()
和write(int c)
方法相當於每次從中取一滴水,資料量不大的時候,效率還可以,但是當資料量非常大時,讀取的效率就很低。
下面我們採用int read(byte[] b)
和void write(byte[] b,int off,int len)
方法來示範檔案複製的效果。
package cn.lincain.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class FileStreamTest {
public static void main(String[] args) {
try (
// 建立一個位元組輸入流
FileInputStream fis = new FileInputStream("FileStreamTest .java");
// 建立一個位元組輸出流
FileOutputStream fos = new FileOutputStream("copy.txt"))
{
long start = System.currentTimeMillis();
byte[] buff = new byte[12];
int hasRead = 0;
// 從ByteStreamTest.java檔案讀取資料到流中
while ((hasRead = fis.read(buff)) > 0) {
// 將資料寫入到指定的檔案中
fos.write(buff, 0, hasRead);
}
long end = System.currentTimeMillis();
System.out.println("檔案複製總共花了:" + (end - start) + "ms");
} catch (Exception e) {
e.printStackTrace();
}
}
}
同樣的案例,當我們採用陣列作為read()
、write()
的引數時,該陣列就相當如一個“水桶”,每次將水桶的水接滿了或者水管的水接完了才輸出,這樣就極大的提高了讀取效率,其中第一個程式複製檔案消耗了21ms,而第二個程式則只消耗了1ms,由此可見二者的差距。
後面對其他位元組流流我們都採取就用緩衝效果的方法進行示範。
ByteArrayInputStream/ByteArrayOutputStream
以上兩個位元組流從字面意思可知,就是位元組陣列與位元組輸入輸出流之間的各種轉換。
ByteArrayInputStream
的構造方法就包含一個位元組陣列作為它本身的內部緩衝區,該緩衝區包含從流中讀取的位元組。
ByteArrayOutputStream
實現了一個輸出流,其中的資料被寫入一個 byte 陣列,緩衝區會隨著資料的不斷寫入而自動增長,可使用 toByteArray() 和 toString() 獲取資料。
舉個例子如下:
package cn.lincain.io;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
public class ByteStreamDemo {
public static void main(String[] args) {
// 建立記憶體中的位元組陣列
byte[] btyeArray = { 1, 2, 3 };
try (
// 建立位元組輸入流
ByteArrayInputStream bis = new ByteArrayInputStream(btyeArray);
// 建立位元組輸出流
ByteArrayOutputStream bos = new ByteArrayOutputStream())
{
byte[] bff = new byte[3];
// 從位元組流中讀取位元組到指定陣列中
bis.read(bff);
System.out.println(bff[0]+","+bff[1]+","+bff[2]);
// 向位元組輸出流中寫入位元組
bos.write(bff);
// 從輸出流中獲取位元組陣列
byte[] bArrayFromBos = bos.toByteArray();
System.out.println(bArrayFromBos[0] + "," + bArrayFromBos[1] + "," + bArrayFromBos[2]);
} catch (Exception e) {
e.printStackTrace();
}
}
}
需要注意的是:在ByteArrayInputStream裡面直接使用外部位元組陣列的引用,也就是說,即使得到位元組流物件後,當改變外部陣列時,通過流讀取的位元組也會改變。
PipedOutputStream/PipedInputStream
PipedOutputStream
和PipedInputStream
分別是管道輸出流和管道輸入流。
它們的作用是讓多執行緒可以通過管道進行執行緒間的通訊。在使用管道通訊時,必須將PipedOutputStream
和PipedInputStream
配套使用。
使用管道通訊時,大致的流程是:我們線上程A中向PipedOutputStream
中寫入資料,這些資料會自動的傳送到與PipedOutputStream
對應的PipedInputStream中,進而儲存在PipedInputStream
的緩衝中;此時,執行緒B通過讀取PipedInputStream
中的資料。就可以實現,執行緒A和執行緒B的通訊。
package cn.lincain.io1;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PipedStreamTest {
public static void main(String[] args) {
try (final PipedOutputStream pos = new PipedOutputStream();
final PipedInputStream pis = new PipedInputStream(pos))
{
ExecutorService es = Executors.newFixedThreadPool(2);
es.execute(new Runnable() {
@Override
public void run() {
try {
byte[] bArr = new byte[] { 1, 2, 3 };
pos.write(bArr);
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
es.execute(new Runnable() {
@Override
public void run() {
byte[] bArr = new byte[3];
try {
// 會導致執行緒阻塞
pis.read(bArr, 0, 3);
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(bArr[0] + "," + bArr[1] + "," + bArr[2]);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
BufferedInputStream/BufferedOutputStream
BufferedInputStream
和BufferedOutputStream
分別是緩衝輸入流和快取輸出流,他們分別繼承自FilterInputStream
和FilterOutputStream
,主要功能是一次讀取/寫入一大塊位元組到緩衝區,避免每次頻繁訪問外部媒介,提高效能。
其中BufferedInputStream
的作用是緩衝輸入以及支援 mark()
和reset ()
方法的能力。
BufferedOutputStream
的作用是為另一個輸出流提供“緩衝功能”。
下面的案例演示了採用緩衝流進行檔案的複製。
package cn.lincain.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class BufferedStreamTest {
public static void main(String[] args) {
try (BufferedInputStream bis =
new BufferedInputStream(new FileInputStream("ByteStreamTest.java"));
BufferedOutputStream bos =
new BufferedOutputStream(new FileOutputStream("copy.txt")))
{
int hasRead = 0;
while ((hasRead = bis.read()) != -1) {
bos.write(hasRead);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
該程式和上面的FileStreamTest .java
的程式碼大致相同,但是該案例的效率顯然更高,只用了不到1ms的時間。
DataOutputStream/DataInputStream
DataInputStream
允許應用程式讀取在與機器無關方式從底層輸入流基本Java資料型別,應用程式可以使用資料輸出流寫入稍後由資料輸入流讀取的資料。
DataOutputStream
允許應用程式以適當方式將基本 Java 資料型別寫入輸出流中。然後,應用程式可以使用資料輸入流將資料讀入。
package cn.lincain.io;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class DataStreamTest {
public static void main(String[] args) {
try (
DataOutputStream dos =
new DataOutputStream(new FileOutputStream("target.txt"));
DataInputStream dis =
new DataInputStream(new FileInputStream("target.txt")))
{
dos.writeBoolean(false);
dos.writeChar('夜');
dos.write(18);
dos.writeUTF("權利的遊戲");
System.out.println(dis.readBoolean());
System.out.println(dis.readChar());
System.out.println(dis.read());
System.out.println(dis.readUTF());
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果要想使用資料輸出流,則肯定要由使用者自己制定資料的儲存格式,必須按指定好的格式儲存資料,才可以使用資料輸入流將資料讀取進來。
ObjectInputStream/ObjectOutputStream
ObjectOutputStream
用於將指定的物件寫入到檔案的過程,就是將物件序列化的過程。ObjectInputStream
則是將儲存在檔案的物件讀取出來,就是將物件反序列化的過程。
一個物件如果要實現序列化和反序列化,則需要該物件實現Serializable
或者Externalizable
介面。
class Person implements Serializable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
package cn.lincain.io;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectStreamTest {
public static void main(String[] args) {
Person person = new Person();
person.setAge(28);
person.setName("Lincain");
writeObject(person);
readObject();
}
// 將物件寫入指定的檔案
public static void writeObject(Object object) {
try (
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("object.txt")))
{
oos.writeObject(object);
} catch (Exception e) {
e.printStackTrace();
}
}
// 將檔案中將物件讀取出來
public static void readObject() {
try (
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("object.txt")))
{
Person person = (Person)ois.readObject();
System.out.println(person);
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行上面程式,在磁碟上產生了object.txt檔案,說明物件已將序列化到本地檔案,如下圖:
需要注意的是,反序列化過程讀取的僅僅是Java物件的資料,而不是Java類,因此反序列化恢復Java物件時,必須提供該物件的class檔案,否則會引起ClassNotFoundException
異常。另外反序列化也無須通過構造器來初始化物件。
關於序列化的知識可參考:https://www.cnblogs.com/fnz0/p/5410856.html
PushbackInputStream
退回:其實就是將從流讀取的資料再推回到流中。實現原理也很簡單,通過一個緩衝陣列來存放退回的資料,每次操作時先從緩衝陣列開始,然後再操作流物件。
package cn.lincain.io;
import java.io.ByteArrayInputStream;
import java.io.PushbackInputStream;
public class PushBackInputStreamTest {
public static void main(String[] args) {
String str = "abcdefghijk";
try (
// //建立位元組推回流,緩衝區大小為 7
ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes());
PushbackInputStream pis = new PushbackInputStream(bis,7))
{
byte[] hasRead = new byte[7];
// 從流中將7個位元組讀入陣列
pis.read(hasRead);
System.out.println(new String(hasRead)); //abcdefg
// 從陣列的第一個位置開始,推回 4 個位元組到流中
pis.unread(hasRead,0,4);
// 重新從流中將位元組讀取陣列
pis.read(hasRead);
System.out.println(new String(hasRead)); // abcdhij
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 建立位元組推回流,同時建立了大小為7的緩衝區陣列;要從位元組輸入流讀取位元組到
hasRead
陣列; - 從流中讀取 7 個位元組到
hasRead
陣列; - 現在要從
hasRead
的 0 下標(第一個位置)開始,推回 4 個位元組到流中去(實質是推回到緩衝陣列中去); - 再次進行讀取操作,首先從緩衝區開始讀取,再對流進行操作。
PrintStream
PrintStream
為其他輸出流添加了功能,使它們能夠方便地列印各種資料值表示形式,使其它輸出流能方便的通過print()
, println()
或printf()
等輸出各種格式的資料。
package cn.lincain.io;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class PrintStreamTest {
public static void main(String[] args) {
try (
// 建立列印流,並允許在檔案末端追加
PrintStream ps = new PrintStream(new FileOutputStream("a.txt", true)))
{
// 將字串“Hello,位元組流!”+回車符,寫入到輸出流中
ps.println("Hello,位元組流!");
// 97對應ASCII碼的是'A',也就是說此處寫入的是字元'A'
ps.write(97);
// 將字串"97"寫入輸出流中,等價於ps.write(String.valueOf(97))
ps.print(97);
// 將'B'追加到輸出流中,和ps.print('B')效果相同
ps.append('B');
// Java的格式化輸出
String str = "CDE";
int num = 5;
ps.printf("%s is %d\n", str, num);
} catch (Exception e) {
e.printStackTrace();
}
}
}
SequenceInputStream
SequenceInputStream
表示其他輸入流的邏輯串聯。它從輸入流的有序集合開始,並從第一個輸入流開始讀取,直到到達檔案末尾,接著從第二個輸入流讀取,依次類推,直到到達包含的最後一個輸入流的檔案末尾為止。
package cn.lincain.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
public class SequenceInputStreamTest {
public static void main(String[] args) throws IOException {
enumMerge();
}
// 通過public SequenceInputStream(Enumeration<? extends InputStream> e)建立SequenceInputStream
public static void enumMerge() throws IOException {
List<FileInputStream> arrayList = new ArrayList<>();
FileInputStream is1 = new FileInputStream("1.txt");
FileInputStream is2 = new FileInputStream("2.txt");
FileInputStream is3 = new FileInputStream("3.txt");
arrayList.add(is1);
arrayList.add(is2);
arrayList.add(is3);
Enumeration<FileInputStream> enumeration = Collections.enumeration(arrayList);
BufferedInputStream bis =
new BufferedInputStream(new SequenceInputStream(enumeration));
BufferedOutputStream bos =
new BufferedOutputStream(new FileOutputStream("4.txt"));
int hasRead = 0;
while ((hasRead = bis.read()) != -1) {
bos.write(hasRead);
}
bis.close();
bos.close();
}
}
字元流(Reader/Writer)
位元組流和字元流的操作方式幾乎一樣,只是他們操作的資料單元不同,體現在方法上時,就是方法的引數有所差別。
首先字元輸入流Reader
,它的三個方法分別是:
int read()
:從輸入流中讀取單個字元,返回所讀取的世界資料。int read(char[] c)
:從輸入流中最多讀取b.length
個字元的資料,並將其儲存在緩衝區陣列c中,返回實際讀取的字元數。int read(char[] c,int off,int len)
:將輸入流中最多len
個數據字元讀入緩衝區陣列c中,off
為陣列 c 中將寫入資料的初始偏移量,返回實際讀取的字元數。
字元輸出流Writer
,因為它可以直接操作字元,可以用字串代替字元陣列,所以它有如下五個基本方法。
void write(int c)
:將指定的字元寫入此輸出流。void write(char[] c)
:將b.length
個字元從指定的c陣列寫入此輸出流。void write(char[] c,int off,int len)
:將字元陣列中從偏移量off
開始的len
個位元組寫入此輸出流。void write(String str)
:將str
中包含的字元輸出到指定的輸出流中。void write(String str, int off,int len)
:將str
從off
位置開始,長度為len
的字元輸出到指定輸出流中。
FileReader/FileWriter
通過FileReader
/FileWriter
我們可以實現對文字檔案的讀取、寫入,從而實現文字檔案的複製。
package cn.lincain.io;
import java.io.FileReader;
import java.io.FileWriter;
public class FileCopyTest {
public static void main(String[] args) {
try (
FileReader fr = new FileReader("FileCopyTest.java");
FileWriter fw = new FileWriter("copy.txt"))
{
char[] cbuf = new char[32];
int hasRead = 0;
while ((hasRead = fr.read(cbuf)) != -1) {
fw.write(cbuf, 0, hasRead);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
CharArrayReader/CharArrayWriter
CharArrayReader
/CharArrayWriter
就是字元陣列與字元輸入輸出流之間的各種轉換。和ByteArrayInputStream
/ByteArrayOutputStream
類似,它們都有一個內部緩衝區,區別是前者操作字元,後置操作位元組。
package cn.lincain.io;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
public class CharArrayTest {
public static void main(String[] args) {
char[] charArray = "無邊落木蕭蕭下,不盡長江滾滾來!!!".toCharArray();
try (
CharArrayReader cr = new CharArrayReader(charArray);
CharArrayWriter cw = new CharArrayWriter())
{
int hasRead = 0;
char[] buff = new char[1024];
while ((hasRead = cr.read(buff)) != -1) {
cw.write(buff, 0, hasRead);
}
// 將輸入資料轉換為字串。
System.out.println(cw.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
同樣需要注意的是,在CharArrayReader
裡面直接使用外部字元陣列的引用,也就是說,即使得到字元流物件後,當改變外部陣列時,通過流讀取的字元也會改變。
BufferedReader/BufferedWriter
和緩衝位元組流相似的,緩衝字元流可以用來裝飾其他字元流,為其提供字元緩衝區,避免頻繁的和外界互動,從而提高效率。被裝飾的字元流可以有更多的行為,比如readLine()
和newLine()
方法等。
下來我們還是通過檔案複製的案例,對其效果進行演示。
package cn.lincain.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class BufferedCopyTest {
public static void main(String[] args) {
try (
// 建立字元緩衝輸入流
BufferedReader br =
new BufferedReader(new FileReader("BufferedCopyTest.java"));
// 建立字元緩衝輸出流
BufferedWriter bw =
new BufferedWriter(new FileWriter("copy.txt")))
{
String line = null;
// 呼叫readLine()方法,每次讀取一行
while ((line = br.readLine()) != null) {
// 將字串寫入到輸出流中
bw.write(line);
// 向輸出流寫入一個換行符
bw.newLine();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
PipedReader/PipedWriter
通過閱讀原始碼可知,PipedReader
/PipedWriter
分別將對方作為自己的成員變數,可知二者需要配套使用。通過二者建立聯絡,構建一個字元流通道,PipedWriter
向管道寫入資料,PipedReader
從管道中讀取資料,從而實現執行緒的通訊。
如下示例:
package cn.lincain.io;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PipedTest {
public static void main(String[] args) throws IOException {
final PipedWriter pw = new PipedWriter();
final PipedReader pr = new PipedReader(pw);
ExecutorService eService = Executors.newFixedThreadPool(2);
eService.execute(new Runnable() {
@Override
public void run() {
try {
pw.write("執行緒互動!!!");
} catch (IOException e) {
e.printStackTrace();
}
}
});
eService.execute(new Runnable() {
@Override
public void run() {
char[] cbuf = new char[32];
try {
// 會導致執行緒阻塞
pr.read(cbuf);
pr.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(cbuf);
}
});
}
}
需要注意的是:一定要同一個JVM中的兩個執行緒,且讀寫都會阻塞執行緒。
StringReader/StringWriter
這兩個類和前面介紹的CharArrayReader
/CharArrayWriter
很相似,只是後者操作的資料型別為字元陣列,而這裡操作的是字串,二者的方法也大致相同,下面通過案例來說明。
package cn.lincain.io;
import java.io.StringReader;
import java.io.StringWriter;
public class StringBufferTest {
public static void main(String[] args) {
String str = "字串緩衝流";
try (
StringReader sr = new StringReader(str);
StringWriter sw = new StringWriter())
{
// 從輸入流中讀取資料
int hasRead = 0;
char[] cbuf = new char[32];
while ((hasRead = sr.read(cbuf)) != -1) {
System.out.println(new String(cbuf, 0, hasRead));
}
// 向輸出流中寫入資料
sw.write(str);
System.out.println(sw.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
PushbackReader
PushbackReader
和PushbackInputStream
的操作方式基本一樣,這裡就不在再詳細的介紹,通過案例對其進行說明。
package cn.lincain.io;
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;
public class PushbackReaderTest {
public static void main(String[] args) throws IOException {
PushbackReader pbr =
new PushbackReader(new FileReader("PushbackReaderTest.java"),30);
char[] cbuf = new char[24];
pbr.read(cbuf);
System.out.println(new String(cbuf)); // package cn.lincain.io3;
// 如果pbr中的緩衝區的大小小於回退陣列的大小,則丟擲異常
pbr.unread(cbuf);
pbr.read(cbuf);
System.out.println(new String(cbuf)); // package cn.lincain.io3;
pbr.close();
}
}
PrintWriter
PrintWriter
和PrintStream
的操作基本一致,只是前者多了兩個方法:void write(String s)
和void write(String s,int off,int len)
。
但通過原始碼可知,PrintStream
也有這兩個方法,只是用private
修飾,方法內容不一樣,但是最終都指向了Writer
類的void write(String str, int off, int len)
方法。
package cn.lincain.io;
import java.io.PrintWriter;
public class PrintWriterTest {
public static void main(String[] args) {
try (
PrintWriter pw = new PrintWriter(System.out))
{
// 97對應ASCII碼的是'A',也就是說此處寫入的是字元'A'
pw.write(97);
// 將字串"97"寫入輸出流中,等價於ps.write(String.valueOf(97))
pw.print(97);
// 將'B'追加到輸出流中,和ps.print('B')效果相同
pw.append('B');
// Java的格式化輸出
String str = "CDE";
int num = 5;
pw.printf("%s is %d\n", str, num);
} catch (Exception e) {
e.printStackTrace();
}
}
}