一、引言
IO(輸入/輸出),輸入是指允許程序讀取外部數據(包括來自磁盤、光盤等存儲設備的數據)、用戶輸入數據。輸出是指允許程序記錄運行狀態,將程序數據輸出到磁盤、光盤等存儲設備中。
IO的主要內容包括輸入、輸出兩種IO流,這兩種流中又分為字節流和字符流,字節流是以字節為單位來處理輸入、輸出流,而字符流是以字符為單位來處理輸入、輸出流。
二、
File 類是用來操作文件和目錄的,File能創建、刪除、重命名文件和目錄,File不能訪問文件內容本身,File 類可以通過文件路徑字符串來創建對象,創建完對象之后有很多方法來操作文件和目錄:
2.1 構造方法
-
File(String pathname):根據一個路徑得到File對象
-
-
File(File parent, String child):根據一個父File對象和一個子文件/目錄得到File對
2.2 創建方法
//在當前路徑來創建一個File對象 File file = new File("1.txt"); //創建文件 System.out.println(file.createNewFile()); File file2 = new File("temp"); //創建對象對應的目錄 System.out.println(file2.mkdir());
//把文件重命名為指定的文件路徑 file2.renameTo(new File("temp2")); //刪除文件或者文件夾 file2.delete();
//判斷文件或目錄是否存在 System.out.println(file.exists()); //判斷是否是文件 System.out.println(file.isFile()); //判斷是否是目錄 System.out.println(file.isDirectory()); //是否為絕對路徑 System.out.println(file.isAbsolute()); //文件或目錄是否可讀 System.out.println(file.canRead()); //文件或目錄是否可寫 System.out.println(file.canWrite());
//返回文件內容長度 System.out.println(file.length()); //獲取文件或目錄名 System.out.println(file.getName()); //獲取文件或目錄相對路徑 System.out.println(file.getPath()); //獲取文件或目錄絕對路徑 System.out.println(file.getAbsolutePath()); //獲取上一級路徑 System.out.println(file.getAbsoluteFile().getParent()); //返回當前目錄的子目錄或文件的名稱 String[] list = file1.list(); for (String fileName : list) { System.out.println(fileName); } //返回當前目錄的子目錄或文件,返回的是File數組 File[] files = file1.listFiles(); //返回系統的所有根路徑 File[] listRoots = File.listRoots(); for (File root : listRoots) { System.out.println(root); }
三、
按照不同的分類方式,流也可以分為不同類型
-
輸入流和輸出流:根據流向來分,可以分為輸入流與輸出流
-
輸入流:從中讀取數據,而不能向其寫入數據
-
輸出流:向其寫入數據,而不能讀取數據
-
-
字節流和字符流:這兩種流用法幾乎完全一樣,區別在於所操作的數據單元不一樣,字節流操作的數據單元是8位的字節,而字符流是16位的字符。
3.2 InputStream與Reader
InputStream和Reader是所有輸入流的抽象基類,這是輸入流的模板,InputStream中有三個方法
-
int read() :從輸入流讀取單個字節,返回所讀取的字節數據。
-
int read(byte b[]):從輸入流中最多讀取b.length個字節的數據,並將其存儲在數組b中。
-
int read(byte b[], int off, int len):從輸入流中最多讀取len個字節的數據,並將其存儲在數組b中,放入的位置是從off中開始。
Reader中也有三個方法
-
int read() :從輸入流讀取單個字符,返回所讀取的字符數據。
-
int read(char cbuf[]):從輸入流中最多讀取cbuf.length個字符的數據,並將其存儲在數組cbuf中。
-
int read(char cbuf[], int off, int len):從輸入流中最多讀取len個字符的數據,並將其存儲在數組cbuf中,放入的位置是從off中開始。
兩個類的方法基本相同,用法相同,只是操作單位不一樣
InputStream inputStream = new FileInputStream("StreamTest.java");
byte[] bytes = new byte[1024];
int hasRead = 0;
while ((hasRead = inputStream.read(bytes)) > 0) {
System.out.println(new String(bytes, 0, hasRead));
}
inputStream.close();
OutputStream與Writer是所有輸出流的抽象基類,是輸出流模板,OutputStream有三個方法:
-
void write(int b):指定字節輸出到流中
-
void write(byte b[]):將指定字節數組輸出到流中
-
void write(byte b[], int off, int len):將指定字節數組從off位置到len長度輸出到流中
Writer中也有三個方法:
-
void write(int b):指定字符輸出到流中
-
void write(char buf[]):將指定字符數組輸出到流中
-
void write(char cubf[], int off, int len):將指定字符數組從off位置到len長度輸出到流中
由於Writer是以字符為單位進行操作,那可以使用String 來代替,於是有另外的方法
-
void write(String str):將str字符串輸出到流中
-
void write(String str, int off, int len):將str從off位置開始長度為len輸出到流中
FileWriter fileWriter = new FileWriter("test.txt"); fileWriter.write("日照香爐生紫煙\r\n"); fileWriter.write("遙看瀑布掛前川\r\n"); fileWriter.write("飛流直下三千尺\r\n"); fileWriter.write("遙看瀑布掛前川\r\n"); fileWriter.close();
注:操作流時一定要記得關閉流,因為打開的IO資源不屬於內存資源,垃圾回收無法回收。
四、
| 分類 | 字節輸入流 | 字節輸出流 | 字符輸入流 | 字符輸出流 |
|---|---|---|---|---|
| 抽象基類 | InputStream | OutputStream | Reader | Writer |
| 訪問文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
| 訪問數組 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
| 訪問管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
| 訪問字符串 | StringReader | StringWriter | ||
| 緩沖流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
| 轉換流 | InputStreamReader | OutputStreamWriter | ||
| 對象流 | ObjectInputStream | ObjectOutputStream | ||
| 過濾流 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
| 打印流 | PrintStream | PrintWriter | ||
| 退回輸入流 | PushbackInputStream | PushbackReader | ||
| 特殊流 | DataInputStream | DataOutputStream |
一般如果輸入/輸出的內容是文本內容,應該考慮使用字符流,如果輸入/輸出內容是二進制內容,則應該考慮使用字節流。
體系中提供了兩個轉換流,實現將字節流轉換成字符流,InputStreamReader將字節輸入流轉換成字符輸入流,OutputStreamWriter將字節輸出流轉換成字符輸出流,System.in代表標准輸入,這個標准輸入是字節輸入流,但是鍵盤輸入的都是文本內容,這個時候我們可以InputStreamReader轉換成字符輸入流,普通的Reader讀取內容不方便,我們可以使用BufferedReader一次讀取一行數據,如:
//先將System.in轉換成Reader 對象 InputStreamReader inputStreamReader = new InputStreamReader(System.in); //再將Reader包裝成BufferedReader BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String line = null; while ((line = bufferedReader.readLine()) != null) { if (line.equals("exit")) { System.exit(1); } System.out.println("輸入的內容是:" + line); }
BufferedReader具有緩沖功能,在沒有讀到換行符則阻塞,讀到換行符再繼續。
推回輸入流PushbackInputStream和PushbackReader中都提供了如下方法:
-
void unread(int b) :將一個字節/字符推回到推回緩沖區,從而允許重復讀取剛剛讀取的內容。
-
void unread(byte[] b/char[] b, int off, int len) :將一個字節/字符數組里從off開始,長度為len字節/字符的內容推回到推回緩沖區,從而允許重復讀取剛剛讀取的內容。
-
void unread(byte[] b/char[]):將一個字節/字符數組內容推回到推回緩沖區,從而允許重復讀取剛剛讀取的內容。
這兩個推回流都帶有一個推回緩沖區,當調用unread()方法時,系統將會把指定的內容推回到該緩沖區,而當每次調用read方法時會優先從推回緩沖區讀取,只有完全讀取了推回緩沖區的內容后,但還沒有read()所需的數組時才會從原輸入流中讀取。
//創建PushbackReader對象,指定推回緩沖區的長度為64 PushbackReader pushbackReader = new PushbackReader(new FileReader("StreamTest.java"), 64); char[] buf = new char[32]; //用以保存上次讀取的字符串內容 String lastContent = ""; int hasRead = 0; //循環讀取文件內容 while ((hasRead = pushbackReader.read(buf)) > 0) { //將讀取的內容轉換成字符串 String content = new String(buf, 0, hasRead); int targetIndex = 0; if ((targetIndex = (lastContent + content).indexOf("new PushbackReader")) > 0) { //將本次內容和上次的內容一起推回緩沖區 pushbackReader.unread((lastContent + content).toCharArray()); //重新定義一個長度為targetIndex的char數組 if (targetIndex > 32) { buf = new char[targetIndex]; } //再次讀取指定長度的內容 pushbackReader.read(buf, 0, targetIndex); //打印讀取的內容 System.out.print(new String(buf, 0, targetIndex)); System.exit(0); } else { //打印上次讀取的內容 System.out.print(lastContent); //將本次內容設為上次讀取的內容 lastContent = content; } }
RandomAccessFile是Java輸入/輸出流體系中最豐富的文件內容訪問類,提供了眾多的方法來訪問文件內容,既可讀取文件內容,也可以向文件輸出數據,RandomAccessFile可以自由訪問文件的任意位置。
RandomAccessFile包含一個記錄指針,用以標識當前讀和寫的位置,當創建新對象時,指針位置在0處,而當讀/寫了N個字節后,指針就會向后移動N個字節,並且RandomAccessFile可以自動的移動該指針位置,當然我們也可以直接的獲取指針的位置。
-
getFilePointer():獲取文件記錄指針的當前位置。
-
seek(long pos):將文件記錄指針定位到pos位置。
RandomAccessFile有兩個構造函數:
-
RandomAccessFile(File file, String mode):使用File文件,指定文件本身 RandomAccessFile(String name, String mode):使用文件名稱,指定文件
其中還有一個參數mode(訪問模式),訪問模式有4個值:
-
r:以只讀方式打開文件
-
rw:以讀、寫方式打開文件,如果文件不存在,則創建
-
rws:以讀、寫方式打開文件,並要求對文件的內容或者元數據的每個更新都同步寫入到底層存儲設備
-
rwd:以讀、寫方式打開文件,並要求對文件的內容的每個更新都同步寫入到底層存儲設備
RandomAccessFile raf = new RandomAccessFile("StreamTest.java", "r"); System.out.println("文件指針的初始位置:" + raf.getFilePointer()); //移動指針位置 raf.seek(300); byte[] buf = new byte[1024]; int hasRead = 0; while ((hasRead = raf.read(buf)) > 0) { //讀取數據 System.out.println(new String(buf, 0, hasRead)); } //追加內容 RandomAccessFile randomAccessFile=new RandomAccessFile("out.txt","rw"); randomAccessFile.setLength(randomAccessFile.length()); randomAccessFile.write("追加的內容!\r\n".getBytes());
六、對象序列化
對象序列化機制是允許把內存中的java對象轉換成平台無關的二進制流,這樣我們可以將這二進制流保存在磁盤上或者通過網絡將起傳輸到另一個網絡節點,其他程序獲取到此二進制流后,可以將其恢復成原來的java對象。
要使一個對象是可序列化的,只需要繼承Serializable或者Externalizable接口,無需實現任何方法。所有可能在網絡上傳輸的對象的類都應該是可序列化的,如我們JavaWeb中的輸入參數及返回結果。
6.1 使用對象流實現序列化
我們使用一個對象流來實現序列化對象
先建一個對象類:
@Data public class Person implements Serializable { private int age; private String name; public Person(String name, int age) { System.out.println("有參數的構造器"); this.age = age; this.name = name; } }
序列化對象與反序列化對象
//創建輸出流 ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("object.txt")); Person person = new Person("張三", 10); //將person寫入文件中 objectOutputStream.writeObject(person); //創建輸入流 ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("object.txt")); try { //讀出數據 Person p = (Person) objectInputStream.readObject(); System.out.println(p); } catch (ClassNotFoundException e) { e.printStackTrace(); }
反序列化讀取的僅僅是Java對象的數據,而不java類,因此反序列化時必須提供對象所屬類的class文件,在反序列化對象時沒有調用有參數的構造器,說明反序列化時不需要通過構造器來初始化Java對象。
如果一個類中包含了引用類型,那么引用類型也必須是可序列化的,否則該類也是不可序列化的。
如果我們不希望某個變量被序列化,比如敏感信息,那需要使用transient來修飾此變量即可。
七、NIO
上面學習的IO都是阻塞式的,而且是底層都是通過字節的移動來處理的,這樣明顯效率不高,於是后面新增了NIO來進行改進,這些類都放在java.nio包中。
新IO 是將文件或文件的一段區域映射到內存中,這樣就可以像訪問內存一樣來訪問文件中的內容,相當於虛擬內存概念,這種方式比傳統的IO快很多。
新IO的兩大核心對象是Channel(通道)與Buffer(緩沖),Channel與傳統的InputStream、OutputStream最大的區別在於提供了一個map()方法,這個方法是將一塊數據映射到內存中,這樣新IO就是面向塊進行處理;Buffer本質是一個數組,可以看做一個容器,發送到Channel中的所有對象都必須首先放在Buffer中,讀取數據也是從Buffer中讀取。
Buffer是一個抽象類,最常用的子類是ByteChannel和CharBuffer,Buffer類都沒有提供構造器,都是通過XXXBuffer allocate(int capacity) 來得到對象,如
CharBuffer allocate = CharBuffer.allocate(8);
Buffer有三個重要概念:
-
容量(capacity):緩沖區的容量,表示該buffer的最大數據容量,即最多可存儲多少數據,創建后不可改變。
-
界限(limit):位於limit后的數據既不可以讀,也不可以寫。
-
位置(position):用於指明下一個可以被讀出或寫入的緩沖區位置索引,類似IO中的指針。

Buffer的主要作用是裝入數據,然后輸出,當創建buffer時,position在0位置,limit在capacity,當添加數據時,position向后移動。
當Buffer裝好數據時,調用flip()方法,這個方法將limit設置為position,position設置為0,也就是說不能繼續輸入,這就給輸出數據做好准備了,而當輸出數據結束后,調用clear()方法,這是將position設置為0,limit設置為capacity,這樣就為裝入數據做好了准備。
除了上面的幾個概念,Buffer還有兩個重要方法,即put()與get()方法,就是存儲與讀取數據方法,在存儲和讀取數據時,分為相對和絕對兩種:
-
相對:從Buffer的position位置開始讀取或者寫入數據,這時候會改變position的數值。
-
絕對:根據索引讀取或寫入數據,這個時候不會影響position的數值。
//創建buffer CharBuffer buffer = CharBuffer.allocate(10); System.out.println("capacity: " + buffer.capacity()); System.out.println("limit:" + buffer.limit()); System.out.println("position:" + buffer.position()); //加入數據 buffer.put('a'); buffer.put('b'); buffer.put('c'); System.out.println("加入元素后,position:" + buffer.position()); buffer.flip(); System.out.println("執行flip后,limit:" + buffer.limit()); System.out.println("position:" + buffer.position()); System.out.println("取出一個數據," + buffer.get()); System.out.println("取出數據后,position:" + buffer.position()); buffer.clear(); System.out.println("執行clear后,limit:" + buffer.limit()); System.out.println(",position:" + buffer.position()); System.out.println("執行clear后緩沖區未被清空:" + buffer.get(2)); System.out.println("絕對讀取后,position不會改變:" + buffer.position());
7.2 Channel
Channel類似傳統流對象,主要區別在於Channel可以將指定文件的部分或者全部直接映射成Buffer,程序不能直接對Channel中的數據進行讀寫,只能通過Channel來進行數據讀寫。我們用FileChannel來看看如何使用:
File file = new File("StreamTest.java"); //輸入流創建FileChannel FileChannel inChannel = new FileInputStream(file).getChannel(); //以文件輸出流創建FileChannel,控制輸出 FileChannel outChannel = new FileOutputStream("a.txt").getChannel(); //將FileChannel映射成ByteBuffer, MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); Charset charset = Charset.forName("GBK"); //輸出數據 outChannel.write(buffer); buffer.clear(); CharsetDecoder charsetDecoder = charset.newDecoder(); //轉換成CharBuffer進行輸出 CharBuffer charBuffer = charsetDecoder.decode(buffer); System.out.println(charBuffer);
7.3 字符集與Charset
我們知道,在計算機底層文件都是二進制文件,都是字節碼,那為什么我們還能看到字符,這里面涉及編碼和解碼兩個概念,簡單講,將字符轉換成二進制為編碼,而將二進制轉成字符為解碼。
Java默認使用Unicode字符集(字符集是指二進制序列與字符之間的對應關系),但很多操作系統不使用Unicode字符集,這樣就會出錯,我們要根據實際情況來使用對應的字符集。
Charset包含了創建解碼器和編碼器的方法,還提供了獲取Charset所支持字符集的方法,我們可以通過Charset的forName()獲取對象,通過對象獲取到CharsetEncoder和CharsetDecoder對象,再通過此對象進行字符序列與字節序列的轉換。
SortedMap<String, Charset> stringCharsetSortedMap = Charset.availableCharsets(); for(String name:stringCharsetSortedMap.keySet()){ System.out.println(name); } //創建簡體中文對應的Charset Charset cn = Charset.forName("GBK"); //創建對應的編碼器及解碼器 CharsetEncoder cnEncoder = cn.newEncoder(); CharsetDecoder cnDecoder = cn.newDecoder(); CharBuffer buff = CharBuffer.allocate(8); buff.put('李'); buff.put('白'); buff.flip(); //將buff的字符轉成字節序列 ByteBuffer bbuff = cnEncoder.encode(buff); for (int i = 0; i <bbuff.capacity() ; i++) { System.out.print(bbuff.get(i)+ " "); } //將bbuff的數據解碼成字符 System.out.println("\n"+cnDecoder.decode(bbuff));
Path path = Paths.get(".");
System.out.println("path包含的文件數量:" + path.getNameCount());
System.out.println("path的根路徑:" + path.getRoot());
Path path1 = path.toAbsolutePath();
System.out.println("path的絕對路徑:" + path1);
//多個String構建路徑
Path path2 = Paths.get("G:", "test", "codes");
System.out.println("path2的路徑:" + path2);
System.out.println("StreamTest.java是否為隱藏文件:" + Files.isHidden(Paths.get("StreamTest.java")));
//一次性讀取所有行
List<String> allLines = Files.readAllLines(Paths.get("StreamTest.java"), Charset.forName("gbk"));
System.out.println(allLines);
//讀取大小
System.out.println("StreamTest.java文件大小:" + Files.size(Paths.get("StreamTest.java")));
List<String> poem = new ArrayList<>();
poem.add("問君能有幾多愁");
poem.add("恰似一江春水向東流");
//一次性寫入數據
Files.write(Paths.get("poem.txt"), poem, Charset.forName("gbk"));
可以看到Paths與Files非常的強大,提供了很多方法供我們使用,在之前這些方法我們自己寫的話比較麻煩,更多的方法可以自己去看API。
7.5 文件屬性
java.nio.file.attribute包下提供了大量的屬性工具類,提供了很方便的方法去獲取文件的屬性:
BasicFileAttributeView baseView = Files.getFileAttributeView(Paths.get("poem.txt"), BasicFileAttributeView.class);
BasicFileAttributes basicFileAttributes = baseView.readAttributes();
System.out.println("創建時間:" + basicFileAttributes.creationTime().toMillis());
System.out.println("最后更新時間:" + basicFileAttributes.lastModifiedTime().toMillis());
