Java IO操作詳解
在Java程式設計中,IO(輸入輸出)是重要的組成部分,Java應用常常需要從外界輸入資料或者把資料輸出到外界。
Java IO的核心用一句話概括:抽象類或介面之中的抽象方法會根據例項化子類的不同,會完成不同的功能。所有的IO操作都在java.io包之中進行定義,而且整個java.io包實際上就是五個類和一個介面:
(1)五個類:File、InputStream、OutputStream、Reader、Wirter;
(2)一個介面:Serializable。
一、檔案操作類File
在整個java.io包之中,File類是唯一的一個與檔案本身操作有關的類,所謂的檔案本身指的是:檔案的建立、刪除、重新命名、取得檔案大小、修改日期。
1、檔案類物件例項化
如果要想使用File類操作檔案的話,那麼肯定要通過構造方法例項化File類物件,而例項化File類物件的過程之中主要使用以下兩種構造方法:
(1)public File(String pathname);//主要在Java EE的開發之中
(2)public File(File parent, String child);//主要在Android開發之中
2、檔案的基本操作
檔案的基本操作,主要有兩種功能:建立和刪除
(1)建立檔案:
public boolean createNewFile() throws IOException;
(2)刪除檔案:
public boolean delete();
範例:
import java.io.File;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:\\demo.txt"); // 檔案的路徑
if (file.exists()) { // 檔案存在
file.delete(); // 刪除檔案
} else { // 檔案不存在
file.createNewFile(); // 建立新檔案
}
}
}
本程式操作就表示檔案如果存在則刪除,如果不存在,則建立一個新的檔案,此時基本功能是實現了,不過這個程式此時卻存在問題:
(1)關於路徑分隔符
在windows作業系統之中,使用“\”作為路徑分隔符,而在linux系統下使用“/”作為路徑的分隔符,而從實際的開發而言,大部分情況下都會在windows中做開發,而後將專案部署到linux下,那麼此時,路徑的分隔符都需要進行修改,這樣實在是過於麻煩,為此在File類之中提供了一個常量:
public static final String separator
(按照Java的命名規範來講,對於全域性常量應該使用大寫字母的方式定義,而此處使用的是小寫,是由Java的發展歷史所帶來的問題)。
所以以上程式例項化File類物件修改為:
File file = new File("D:" + File.separator + "demo.txt"); // 檔案的路徑
(2)之前進行檔案建立的時候都是在根路徑下建立完成的,如果說現在要建立的檔案有目錄呢?
例如,現在要建立一個d:\hellodemo\my\test\demo.txt檔案,而此時在執行程式的時候hellodemo目錄不存在,這個時候執行的話就會出現錯誤提示:
Exception in thread "main" java.io.IOException: 系統找不到指定的路徑。
因為現在目錄不存在,所以不能建立,那麼這個時候必須要首先判斷要建立檔案的父路徑是否存在,如果不存在應該建立一個目錄,之後再進行檔案的建立,而要想完成這樣的操作,需要以下幾個方法的支援:
①找到一個指定檔案的父路徑:
public File getParentFile();
②建立目錄:
public boolean mkdirs();
範例:
import java.io.File;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "hellodemo"
+ File.separator + "my" + File.separator + "test"
+ File.separator + "demo.txt"); // 檔案的路徑
if (!file.getParentFile().exists()) { // 父路徑不存在
file.getParentFile().mkdirs(); // 建立目錄
}
if (file.exists()) { // 檔案存在
file.delete(); // 刪除檔案
} else { // 檔案不存在
file.createNewFile(); // 建立新檔案
}
}
}
以後在任何的java.io.File類開發的過程之中,都一定要考慮檔案目錄的問題。
3、獲取檔案的基本資訊
除了以上的常用的方法之外,在File類之中還可以通過以下的方法取得一些檔案的基本資訊:
(1)取得檔案的名稱:
public String getName();
(2)給定的路徑是否是資料夾:
public boolean isDirectory();
(3)給定的路徑是否是檔案:
public boolean isFile();
(4)是否是隱藏檔案:
public boolean isHidden();
(5)檔案的最後一次修改日期:
public long lastModified();
(6)取得檔案大小:
public long length();
是以位元組為單位返回的。
二、位元組流與字元流
使用File類執行的所有操作都是針對於檔案本身,但是卻沒有針對於檔案的內容,而要進行檔案內容操作就需要通過Java之中提供的兩組類完成:
(1)位元組操作流:OutputStream、InputStream;
(2)字元操作流:Writer、Reader。
但是不管是位元組流還是字元流的操作,本身都表示資源操作,而執行所有的資源操作都會按照如下的幾個步驟進行,下面以檔案操作為例(對檔案進行讀、寫操作):
(1)如果要操作的是檔案,那麼首先要通過File類物件找到一個要操作的檔案路徑(路徑有可能存在,有可能不存在,如果不存在,則要建立路徑);
(2)通過位元組流或字元流的子類為位元組流或字元流的物件例項化(向上轉型);
(3)執行讀 / 寫操作;
(4) 最後一定要關閉操作的資源(資料流屬於資源操作,資源操作必須關閉)
1、位元組流
OutputStream和InputStream是位元組流的兩個頂層父類。讓他們提供了輸出流類和輸入流類通用API,位元組流一般用於讀寫二進位制資料,如影象和聲音資料。
[1]輸出流:OutputStream
java.io.OutputStream主要的功能是進行位元組資料的輸出的,這個類的定義如下:
public abstract class OutputStream
extends Object
implements Closeable, Flushable
發現OutputStream類定義的時候實現了兩個介面:Closeable、Flushable,這兩個介面的定義如下:
(1)Closeable (JDK 1.5推出):
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
(2)Flushable(JDK 1.5推出):
public interface Flushable {
public void flush() throws IOException;
}
對於OutputStream類而言發現其本身定義的是一個抽象類(abstract class),按照抽象類的使用原則來講,需要定義抽象類的子類,而現在如果要執行的是檔案操作,則可以使用FileOutputStream子類完成,如果按照面向物件的開發原則,子類要為抽象類進行物件的例項化,而後呼叫的方法以父類中定義的方法為主,而具體的實現找例項化這個父類的子類完成,也就是說在整個的操作之中,使用者最關心的只有子類的構造方法:
(1)例項化FileOutputStream(新建資料):
public FileOutputStream(File file) throws FileNotFoundException;
(2)例項化FileOutputStream(追加資料):
public FileOutputStream(File file, boolean append)
throws FileNotFoundException
當取得了OutputStream類的例項化物件之後,下面肯定要進行輸出操作,在OutputStream類之中定義了三個方法:
(1)輸出單個位元組資料:
public abstract void write(int b) throws IOException;
(2)輸出一組位元組資料:
public void write(byte[] b) throws IOException;
(3)輸出部分位元組資料:
public void write(byte[] b, int off, int len) throws IOException;
範例:使用OutputStream向檔案之中輸出資料,輸出路徑:d:\demo\test.txt
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class TestDemo {
public static void main(String[] args) throws Exception {
//第一步:定義檔案路徑
File file = new File("D:"+File.separator + "demo"+ File.separator + "test.txt");
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
//第二步:例項化輸出流
OutputStream output = new FileOutputStream(file);
String data = "hello world !\r\nhello world !\r\nhello world !\r\nhello world !";
// 第三步:輸出資料,要將資料變為位元組陣列輸出
output.write(data.getBytes());
//第四步:關閉資源
output.close();
}
}
執行結果:
在整個的檔案輸出過程之中可以發現,如果現在要輸出的檔案不存在,那麼會出現自動建立檔案的情況,並且如果重複執行以上的程式碼,會出現新的內容覆蓋掉舊內容的操作,所以下面可以使用FileOutputStream類的另外一個構造方法進行資料的追加:
OutputStream output = new FileOutputStream(file, true);
如果說現在不想全部內容輸出,也可以使用另外一個write()方法部分內容輸出:
output.write(data.getBytes(), 0, 5);
執行結果:
在OutputStream類之中所有的資料都是以位元組資料為主的。
[2]輸入流:InputStream
如果現在要從指定的資料來源之中讀取資料,使用InputStream,這個類的定義如下:
public abstract class InputStream
extends Object
implements Closeable
既然InputStream為抽象類,那麼這個抽象類要使用就必須有子類,現在是通過檔案讀取內容,肯定使用FileInputStream子類進行操作,與OutputStream類的使用一樣,對於FileInputStream也只關心構造方法:
public FileInputStream(File file) throws FileNotFoundException;
在InputStream類之中,定義了三個讀取資料的操作方法:
(1)讀取單個位元組:
public abstract int read() throws IOException;
注意:每次執行read()方法都會讀取一個數據源的指定資料,如果現在發現已經讀取到了結尾返回-1;
(2)將讀取的資料儲存到位元組陣列中:
public int read(byte[] b) throws IOException;
注意:如果現在要讀取的資料小於byte的資料,這個時候read()方法的返回值int返回的是資料個數,如果資料已經讀完了,則這個時候的int返回的是-1;
(3)將讀取的資料儲存在部分位元組陣列中:
public int read(byte[] b, int off, int len) throws IOException;
範例:一次性全部讀取資料進來
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class TestDemo {
public static void main(String[] args) throws Exception {
//第一步:定義檔案路徑
File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
if (file.exists()) { // 檔案存在則可以讀取
//第二步:例項化輸入流
InputStream input = new FileInputStream(file);
//第三步:讀取資料到位元組陣列
byte data[] = new byte[1024]; // 假設要讀的長度是1024
int len = input.read(data); // 讀取資料,返回讀取個數
//第四步:關閉資源
input.close();
System.out.println("讀取的資料是:【" + new String(data, 0, len) + "】");
}
}
}
範例:單個位元組讀取
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
if (file.exists()) { // 檔案存在則可以讀取
InputStream input = new FileInputStream(file);
byte data[] = new byte[1024]; // 假設要讀的長度是1024
int foot = 0; // 操作data陣列的腳標
int temp = 0;
// 第一步:temp = input.read(),讀取一個單個位元組,並且將內容給temp變數
// 第二步:temp != -1,將接收到的temp的數值判斷是否為-1,如果為-1則表示退出迴圈,如果不是,則儲存資料
while ((temp = input.read()) != -1) {
data[foot++] = (byte) temp; // 儲存讀取進來的單個位元組
}
input.close(); // 關閉
System.out.println("讀取的資料是:【" + new String(data, 0, foot) + "】");
}
}
}
在今後開發中,都會使用以上的while迴圈方式進行資料的讀取。
2、字元流
[1]字元輸出流:Writer
Writer類也是一個專門用於資料輸出的操作類,這個類定義:
public abstract class Writer
extends Object
implements Appendable, Closeable, Flushable
在Wirter類之中比OutputStream類最為方便的一點就是其可以直接使用String型資料輸出,並且不再需要將其變為位元組陣列了。而Writer類本身也是一個抽象類,那麼如果要使用依然要依靠它的子類,尤其是現在操作的是檔案,使用FileWriter子類。
在Wirter類之中定義的write()方法都是以字元資料為主,但是在這些方法之中,只關心一個:輸出一個字串:
public void write(String str) throws IOException;
範例:使用Wirter類進行內容的輸出
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();// 建立父目錄
}
Writer out = new FileWriter(file);
String data = "Hello World .";
out.write(data); // 直接輸出字串
out.close(); // 關閉輸出
}
}
執行結果:
從輸出來講,Wirter類的輸出要比OutputStream類更加的方便。
[2]字元輸入流:Reader
Reader是進行字元資料讀取的操作類,其定義:
public abstract class Reader
extends Object
implements Readable, Closeable
在Writer類之中存在了直接輸出一個字串資料的方法,可是在Reader類之中並沒有定義這樣的方法,只是定義了三個按照字元讀取的方法,為什麼會這樣?
因為在使用OutputStream輸出資料的時候,其程式可以輸出的大小一定是程式可以承受的資料大小,那麼如果說使用InputStream讀取的時候,可能被讀取的資料非常的大,那麼如果一次性全讀進來了,就會出現問題,所以只能一個一個的進行讀取。
在Reader類裡也提供了一系列的read方法,其中一個:
讀取內容到字元陣列:
public int read (char[] cbuf) throws IOException;
注意:返回值表示讀取的資料長度,如果已經讀取到結尾。返回-1。
Reader依然是抽象類,那麼如果從檔案讀取,依然使用FileReader類。
import java.io.File;
import java.io.FileReader;
import java.io.Reader;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "demo" + File.separator + "test.txt"); // 定義檔案路徑
if (file.exists()) {
Reader in = new FileReader(file); // 字元輸入流
char data[] = new char[1024]; // 開闢陣列
int len = in.read(data); // 讀取資料
in.close();
System.out.println("讀取資料內容:【" + new String(data, 0, len) + "】");
}
}
}
三、位元組流與字元流的區別
通過以上的程式碼演示,現在可以發現,對於位元組流和字元流可以完成類似的功能,那麼在開發之中使用那一種呢?兩者的區別:
位元組流在進行IO操作的時候,直接針對的是操作的資料終端(例如:檔案),而字元流操作的時候不是直接針對於終端,而是針對於快取區(理解為記憶體)操作,而後由快取取操作終端(例如:檔案),屬於間接操作,按照這樣的方式,如果說在使用位元組流的時候不關閉最後的輸出流操作,也可以將所有的內容進行輸出,而字元輸出流的時候如果不關閉,則意味著緩衝區之中的內容不會被輸出,當然,這個時候可以由使用者自己去呼叫flush()方法進行強制性的手工清空:
import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
public class TestDemo {
public static void main(String[] args) throws Exception {
File file = new File("D:" + File.separator + "demo"
+ File.separator + "test.txt"); // 定義檔案路徑
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();// 建立父目錄
}
Writer out = new FileWriter(file);
String data = "Hello World .";
out.write(data) ; // 直接輸出字串
out.flush() ; // 清空緩衝區
}
}
對於電腦磁碟或者是網路資料傳輸上,使用最多的資料型別都是位元組資料,包括圖片、音樂、各種可執行程式也都是位元組資料,很明顯,位元組資料要比字元資料更加的廣泛,但是在進行中文處理的過程之中,字元流又要比位元組流方便許多,所以如果要使用的話,首先考慮的是位元組流(還有一個原因是因為位元組流和字元流的程式碼形式類似),如果有中文處理的問題,才會考慮使用字元流。
四、綜合例項:檔案拷貝
1、實現思路:
如果要想實現檔案的複製那麼肯定要使用的是位元組流,因為檔案有可能是圖片,而對於這樣的操作有兩種實現方式:
(1)方式一:將要複製的檔案內容全部讀取到記憶體之中,而後將所有內容一次輸出到目標檔案之中;
(2)方式二:採用邊讀邊寫的方式一塊一塊的進行檔案的拷貝。
很明顯,使用方式二會更加的方便,因為如果要複製的檔案很大,那麼方式一是不可能在記憶體之中裝如此只大的資料量,所以全部讀取是不太可能的。
需要拷貝的檔案如下,約47M:
2、拷貝程式:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class TestDemo {
public static void main(String[] args) throws Exception {
File inFile = new File("D:" + File.separator + "demo"
+ File.separator + "test.zip"); // 定義檔案路徑
File outFile = new File("D:" + File.separator + "demo"
+ File.separator + "test2.zip"); // 定義檔案路徑
long start = System.currentTimeMillis();
if (!inFile.exists()) { // 原始檔不存在
System.out.println("原始檔不存在!");
System.exit(1); // 程式退出
}
if(!outFile.getParentFile().exists()){
outFile.getParentFile().mkdirs();
}
InputStream input = new FileInputStream(inFile);
OutputStream output = new FileOutputStream(outFile);
int temp = 0;//儲存每次讀取的個數
byte data[] = new byte[4096]; // 每次讀取4096位元組
while ((temp = input.read(data)) != -1) { // 將每次讀取進來的資料儲存在位元組數組裡,並返回讀取的個數
output.write(data, 0, temp); // 輸出陣列
}
long end = System.currentTimeMillis();
System.out.println("拷貝完成,所花費的時間:" + (end - start) + "毫秒");
input.close();
output.close();
}
}
執行結果:
拷貝完成,所花費的時間:125毫秒
檢視檔案: