这些年一直记不住的 Java I/O

本文目录html

  • 参考资料java

  • 前言程序员

  • 从对立到统一,字节流和字符流ubuntu

  • 从抽象到具体,数据的来源和目的设计模式

  • 从简单到丰富,使用 Decorator 模式扩展功能数组

  • Java 7 中引入的 NIO.2缓存

  • NIO.2 中的异步 I/O微信

  • 总结网络

参考资料

  该文中的内容来源于 Oracle 的官方文档。Oracle 在 Java 方面的文档是很是完善的。对 Java 8 感兴趣的朋友,能够从这个总入口Java SE 8 Documentation开始寻找感兴趣的内容。这一篇主要讲 Java 中的 I/O,官方文档在这里Java I/O, NIO, and NIO.2oracle

前言

  不知道你们看到这个标题会不会笑我,一个使用 Java 多年的老程序员竟然一直没有记住 Java 中的 I/O。不过说实话,Java 中的 I/O 确实含有太多的类、接口和抽象类,而每一个类又有好几种不一样的构造函数,并且在 Java 的 I/O 中又普遍使用了 Decorator 设计模式(装饰者模式)。总之,即便是在 OO 领域浸淫多年的老手,看到下面这样的调用同样会蛋疼:

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("somefile.txt")));

  固然,这仅仅只是我为了体现 Java I/O 的错综复杂的构造函数而虚构出来的一个例子,现实中建立一个 BufferedReader 不多会嵌套这么深,由于能够直接使用 FileReader 而避免多建立一个 FileInputStream。可是从一个 InputStream 转化成一个 BufferedReader 老是有那么几步路要走的,好比下面这个例子:

URL cnblogs = new URL("http://www.cnblogs.com/");
BufferedReader reader = new BufferedReader(new InputStreamReader(cnblogs.openStream()));

  Java I/O 涉及到的类也确实特别多,不只有分别用于操做字符流和字节流的 InputStream 和 Reader、OutputStream 和 Writer,还有什么 BufferedInputStream、BufferedReader、PrintWriter、PrintStream等,还有用于沟通字节流和字符流的桥梁 InputStreamReader 和 OutputStreamWriter,每个类都有其不一样的应用场景,如此细致的划分,光是名字就足够让人晕头转向了。

  我一直记不住 Java I/O 中各类细节的另外一个缘由多是我深受 ANSI C 的荼毒吧。在 C 语言的标准库中,将文件的打开方式分为两种,一种是将文件当成二进制格式打开,一种是当成文本格式打开。这和 Java 中的字节流和字符流的划分有类似之处,但却掩盖了全部的数据其实都是字节流这样的本质。ANSI C 用多了,总觉得二进制格式和文本格式是同一个层面的两种对立面,只能对立而不能统一,殊不知在 Java 中,字符流是对字节流的更高层次的封装,最底层的 I/O 都是创建在字节流的基础上的。若是抛开 ANSI C 语言的标准 I/O 库,直接考察操做系统层面的 POSIX I/O,会发现操做的一切都是原始的字节数据,根本没有什么字节字符的区别。

  除此以外,Java 走得更远,它考虑到了各类更加普遍的字节流,而不只仅限于文件。好比网络中传输的数据、内存中传输的对象等等,均可以用流来抽象。可是不一样的流具备不一样的特性,有的流能够随机访问,而有的却只能顺序访问,有的能够解释为字符,有的不能。在能解释为字符的流中,有的一次只能访问一个字符,有的却能够一次访问一行,并且把字节流解释成字符流,还要考虑到字符编码的问题。

  以上种种,均是形成 Java I/O 中类和接口多、对象构造方式复杂的缘由。

从对立到统一,字节流和字符流

  先来讲对立。在 Java 中若是要把流中的数据按字节来访问,就应该使用 InputStream 和 OutputStream,若是要把流中的数据按字符来访问,就应该使用 Reader 和 Writer。上面提到的这四个类都是抽象类,是全部其它具体类的基础。不能直接构造 InputStream、OutputStream、Reader 和 Writer 类的实例,可是根据 OO 原则,能够这样用:

InputStream in = new FileInputStream("somefile");
int c = in.read();

  或者这样:

Reader reader = new FileReader("somefile");
int c = reader.read();

  这里的 FileInputStream 和 FileReader 就是具体的类,这样的类还有不少,都位于 java.io 包中。文件读写是咱们最经常使用的操做,因此最经常使用的就是 FileInputStream、FileOutputStream、FileReader、FileWriter这四个。这几个类的构造函数有多个,可是最简单的,确定是接受一个表明文件路径的字符串作参数的那一个。根据 OO 原则,咱们通常使用更加抽象的 InputStream、OutputStream、Reader、Writer 来引用具体的对象。因此,在考察 API 的时候,只须要考察这四个抽象类就能够了,其它的具体类,基本上只须要考察它们的构造方式。

  而这几个类的 API 也确实很好记,用来输入的两个类 InputStream 和 Reader 主要定义了read()方法,而用来输出的两个类 OutputStream 和 Writer 主要定义了write()方法。所不一样者,前者操做的是字节,后者操做的是字符。read()write()最简单的用法是这样的:

package com.xkland.sample;

import java.io.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.FileNotFoundException;

public class JavaIODemo {
   public static void main(String[] args) {
       if(args.length < 1){
           System.out.println("Usage: JavaIODemo filename");
           return;
       }
       String somefile = args[0];
       InputStream in = null;
       try{
           in = new FileInputStream(somefile);
           int c;
           while((c = in.read()) != -1) {  //这里用到read()
               System.out.write(c);        //这里用到write()
           }
       }catch(FileNotFoundException e){
           System.out.println("File not found.");
       }catch(IOException e){
           System.out.println("I/O failed.");
       }finally{
           if(in != null){
               try {
                   in.close();
               }catch(IOException e){
                   //关闭流时产生的异常,直接抛弃
               }
           }
       }
   }
}

  上面的例子展现了read()write()的用法,在 InputStream 和 OutputStream 中,这两个方法操做的都是字节,可是,这里用来保存这个字节的变量倒是int类型的。这正是 API 设计的匠心所在,由于int的宽度明显比byte要大,因此将一个byte读入到一个int以后,有效的数据只占据int型变量的最低8位,若是read()方法返回的是有效数据,那么这个int型的变量永远都不多是负数。在这种状况下,read()方法能够用返回负数的方式来表示碰到特殊状况,好比返回-1表示到达了流的末尾,也就是用-1表明EOFwrite()方法接受的参数也是int型的,可是它只把这个int型变量的最低8位写入流,其他的数据被忽略。

  上面的例子还展现了 Java I/O 的一些特征:

  1. InputStream、OutputStream、Reader、Writer 等资源用完以后要关闭;

  2. 全部的 I/O 操做均可能产生异常,包括调用close()方法。

  这两个特征搅到一块儿就比较复杂了,原本由于异常的产生就容易让流的close()语句执行不到,因此只有把close()写到finally块中,可是在finally块中调用close()又要写一层try...catch...代码块。若是同时有多个流须要关闭,而前面的close()抛出异常,则后面的close()将不会执行,极易发生资源泄露。再加上若是前面的catch()块中的异常被从新抛出,而finally块中又没有处理好异常的话,前面的异常会被抑制,因此大部分人都 hold 不住这样的代码,包括 Oracle 的官方教程中的写法都是错误的。下面来看一下 Oracle 官方教程中的例子:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class CopyBytes {
   public static void main(String[] args) throws IOException {

       FileInputStream in = null;
       FileOutputStream out = null;

       try {
           in = new FileInputStream("xanadu.txt");
           out = new FileOutputStream("outagain.txt");
           int c;

           while ((c = in.read()) != -1) {
               out.write(c);
           }
       } finally {
           if (in != null) {
               in.close();
           }
           if (out != null) {
               out.close();
           }
       }
   }
}

  官方教程写得比我更偷懒,它直接让main()方法抛出IOException而避免了异常处理,也避免了在finally块中的close()语句外再写一层try...catch...。可是,这个示例的漏洞有两个,其一是若是in.close()抛出了异常,则out.close()就不会执行;其二是若是try块中抛出了异常,finally块中又抛出了异常,则前面抛出的异常会被丢弃。为了解决这个问题,Java 7中新加入了try-with-resource语法。后面都用这种方式写代码。

  很显然,一次处理一个字节效率是及其低下的,因此read()write()还有别的重载版本:

int read(byte[] b)
int read(byte[] b, int off, int len)
void write(byte[] b)
void write(byte[] b, int off, int len)

  它们均可以一次操做一块数据,用字节数组作为存储数据的容器。read()返回的是实际读取的字节数。而对于 Reader 和 Writer,它们的read()write()方法的定义是这样的:

int read()
int read(char[] cbuf)
int read(char[] cbuf, int off, int len)
void write(int c)
void write(char[] cbuf)
void write(char[] cbuf, int off, int len)
void write(String str)
void write(String str, int off, int len)

  能够看出,使用 Reader 和 Writer 一次操做一个字符的时候,依然使用的是int型的变量。若是一次操做一块数据,则使用字符数组。输出的时候,还能够直接使用字符串。

  到这里,已经能够很轻易记住八个类了:InputStream、OutputStream、Reader、Writer、FileInputStream、FileOutputStream、FileReader、FileWriter。前四个是抽象类,后四个是操做文件的具体类。并且这八个类分红两组,一组操做字节流,一组操做字符流。很简单的对立分组。

  然而,前面我提到过,其实字节流和字符流并非彻底对立的存在,其实字符流是在字节流上更高层次的封装。在底层,一切数据都是字节,可是通过适当的封装,能够把这些字节解释成字符。并且,并非全部的 Reader 都是能够像 FileReader 那样直接建立的,有时,只能拿到一个能够读取字节数据的 InputStream,却须要在它之上封装出一个 Reader,以方便按字符的方式读取数据。最典型的例子就是能够这样访问一个网页:

URL cnblogs = new URL("http://www.cnblogs.com/");
InputStream in = cnblogs.openStream();

  这时,拿到的是字节流 InputStream,若是想得到按字符读取数据的 Reader,能够这样建立:

Reader reader = new InputStreamReader(in);

  因此, InputStreamReader 是沟通字节流和字符流的桥梁。一样的桥梁还用用于输出的 OutputStreamWriter。至此,不只又轻松地记住了两个类,也再次证实了字节流和字符流既对立又统一的辩证关系。

从抽象到具体,数据的来源和目的

  InputStream、OutputStream、Reader 和 Writer 是抽象的,根据不一样的数据来源和目的又有不一样的具体类。前面的例子中提到了基于 File 的流,也初步展现了一个基于网络的流。结合平时使用计算机的经验,咱们也能够想到其它一些不一样的数据来源和目的,好比从内存中读取字节或把字节写入内存,从字符串中读取字符或者把字符写入字符串等等,还有从管道中读取数据和向管道中写入数据等等。根据不一样的数据来源和目的,能够有这样一些具体类:FileInputStream、ByteArrayInputStream、PipedInputStream、FileOutputStream、ByteArrayOutputStream、PipedOutputStream、FileReader、StringReader、CharArrayReader、PipedReader、FileWriter、StringWriter、CharArrayWriter、PipedWriter等。从这些类的命名能够看出,凡是以Stream结尾的,都是操做字节的流,凡是以 Reader 和 Writer 结尾的,都是操做字符的流。只有 InputStreamReader 和 OutputStreamWriter 是例外,它是沟通字节和字符的桥梁。对于这些具体类,使用起来是没有什么困难的,只须要考察它们的构造函数就能够了。下面两幅 UML 类图能够展现这些类的关系。

  InputStreams 和 Readers:

  OutputStreams 和 Writers:

从简单到丰富,使用 Decorator 模式扩展功能

  从前文能够看出,全部的流都支持read()write(),可是这样的功能毕竟仍是太简单,有时还须要更高层次的功能需求,因此须要使用 Decorator 模式来对流进行扩展。好比,一次操做一个字节或一个字符效率过低,想把数据先缓存在内存中再进行操做,就能够扩展出 BufferedInputStream、BufferedReader、BufferedOutputStream、BufferedWriter 类。能够猜想到,BufferedOutputStream 和 BufferedWriter 类中必定有一个flush()方法,用来把缓存的数据写入到流中。并且,BufferedReader 还有 readLine() 方法,能够一次读取一行字符,甚至能够再扩展出一个 LineNumberReader,还能够提供行号的支持。再好比,有时从流中读出一个字节或一个字符后,又不想要了,想把它还回去,就能够再扩展出 PushbackInputStream 和 PushbackReader,提供unread()方法将刚读取的字节或字符还回去。能够想象,这种还回去的功能应该是须要缓存功能支持的,因此它们应该是在 BufferedInputStream 和 BufferedReader 外面又加了一层的装饰。这就是 Decorator 模式。

  Java I/O 中自带的这种扩展类还有不少,不容易记。后面的介绍中,会针对重要的类举几个例子。在此以前,仍是经过 UML 类图来了解一下扩展类。

  从 InputStream 扩展的类:

  从 Reader 扩展的类:

  从 OutputStream 扩展的类:

  从 Writer 扩展的类:

  从上图中能够看到,每个分组中扩展的类的数量是不同的,不再是一种对称的关系。仔细一想也很好理解,例如 Pushback 这样的功能就只能用在输入流 InputStream 和 Reader 上,而向输出流中写入数据就像泼出去的水,没办法再 Pushback 了。再例如,向流中写入对象和读取对象,操做的确定是字节流而不是字符流,因此只有 ObjectInputStream 和 ObjectOutputStream,而没有相应的 Reader 和 Writer 版本。再例如打印,操做的确定是输出流,因此只有 PrintStream 和 PrintWriter,没有相应的输入流版本,这没有什么好奇怪的。

  在这些类中,能够经过 PrintStream 和 PrintWriter 向流中写入格式化的文本,也能够经过 DataInputStream 和 DataOutputStream 从流中读取或向流中写入原始的数据,还能够经过 ObjectInputStream 和 ObjectOutputStream 从流中读取或写入一个完整的对象。若是要从流中读取格式化的文本,就必须使用 java.util.Scanner 类了。

  下面先看一个简单的示例,使用 DataOutputStream 的writeInt()writeDouble()以及writeUTF()方法将intdoubleString类型的数据写入流中,而后再使用 DataInputStream 的readInt()readDouble()readUTF()方法从流中读取intdoubleString类型的数据。为了简单起见,就使用基于文件的流做为存储数据的方式。代码以下:

package com.xkland.sample;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.EOFException;

public class DataStreamsDemo {
   public static void writeToFile(String filename){

       double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
       int[] units = { 12, 8, 13, 29, 50 };
       String[] descs = {
           "Java T-shirt",
           "Java Mug",
           "Duke Juggling Dolls",
           "Java Pin",
           "Java Key Chain"
       };
       try(DataOutputStream out = new DataOutputStream(
               new BufferedOutputStream(
                       new FileOutputStream(filename)))){
           for (int i = 0; i < prices.length; i ++) {
               out.writeDouble(prices[i]);
               out.writeInt(units[i]);
               out.writeUTF(descs[i]);
           }
           
       }catch(IOException e){
           System.out.println(e.getMessage());
       }
   }
   
   public static void readFromFile(String filename){
       double price;
       int unit;
       String desc;
       double total = 0.0;
       try(DataInputStream in = new DataInputStream(
               new BufferedInputStream(
                       new FileInputStream(filename)))){
           while (true) {
               price = in.readDouble();
               unit = in.readInt();
               desc = in.readUTF();
               System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price);
               total += unit * price;
           }
           
       }catch(EOFException e){
           //达到文件末尾
           System.out.format("全部数据已读入,总价格为:$%.2f%n", total);
       }catch(IOException e){
           System.out.println(e.getMessage());
       }
   }
}

  而后在main()方法中这样调用:

package com.xkland.sample;

public class JavaIODemo {

   public static void main(String[] args) {
       if(args.length < 1){
           System.out.println("Usage: JavaIODemo filename");
           return;
       }
       //向文件中写入数据
       DataStreamsDemo.writeToFile(args[0]);
       //从文件中读取数据并显示
       DataStreamsDemo.readFromFile(args[0]);
   }

}

  而后这样运行该程序:

java com.xkland.sample.JavaIODemo /home/youxia/testfile

  最后输出是这样:

You ordered 12 units of Java T-shirt at $19.99
You ordered 8 units of Java Mug at $9.99
You ordered 13 units of Duke Juggling Dolls at $15.99
You ordered 29 units of Java Pin at $3.99
You ordered 50 units of Java Key Chain at $4.99
全部数据已读入,总价格为:$892.88

  若是使用cat命令查看/home/youxia/testfile文件的内容,只会看到一堆乱码,说明该文件是以二进制格式存储的。以下:

  上面的代码展现了 DataInputStream 和 DataOutputStream 的用法,经过前面的探讨,对它们这样层层包装的构造方式已经见怪不怪了。而且在示例代码中使用了 Java 7 中新引入的try-with-resource语法,这样大大减小了代码的复杂度,全部打开的流均可以自动关闭,并且异常处理也更简洁。从代码中还能够看到,须要捕获 DataInputStream 的 EOFException 异常才能判断读取到了文件结尾。另外,使用这种方式写入和读取数据要很是当心,写入数据的顺序和读取数据的顺序必定要保持一致,若是先写一个int,再写一个double,则必定要先读一个int,再读一个double,不然只会读取错误的数据。不信能够经过修改上述示例代码中读取数据的顺序进行测试。

  使用 DataInputStream 和 DataOutputStream 只能写入和读取原始的数据类型的数据,如bytecharshortfloat等,若是要读取和写入复杂的对象就不行了,好比java.math.BigDecimal。这个时候就须要使用 ObjectInputStream 和 ObjectOutputStream 了。全部须要写入流和从流读取的 Object 必须实现Serializable接口,而后调用 ObjectInputStream 和 ObjectOutputStream 的writeObject()方法和readObject()方法就能够了。并且很奇妙的是,若是一个 Object 中包含了其它的 Object 对象,则这些对象都会被写入到流中,并且能保持它们之间的引用关系。从流中读取对象的时候,这些对象也会同时被读入内存,并保持它们之间的引用关系。若是把同一批对象写入不一样的流,再从这些流中读出,就会得到这些对象多个副本。这里就不举例了。

  与以二进制格式写入和读取数据相对的,就是以文本的方式写入和读取数据。PrintStream 和 PrintWriter 中的 Print 就是表明着输出能供人读取的数据。好比浮点数3.14能够输出为字符串"3.14"。利用 PrintStream 和 PrintWriter 中提供的大量print()方法和println()方法就能够作到这点,利用format()方法还能够进行更加复杂的格式化。把上面的例子作少许修改,以下:

package com.xkland.sample;

import java.io.*;
import java.util.Scanner;

public class PrintStreamDemo {
   public static void writeToFile(String filename){

       double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 };
       int[] units = { 12, 8, 13, 29, 50 };
       String[] descs = {
               "Java T-shirt",
               "Java Mug",
               "Duke Juggling Dolls",
               "Java Pin",
               "Java Key Chain"
       };
       try(PrintStream out = new PrintStream(
               new BufferedOutputStream(
                       new FileOutputStream(filename)))){
           for (int i = 0; i < prices.length; i ++) {
               out.println(prices[i]);
               out.println(units[i]);
               out.println(descs[i]);
           }

       }catch(IOException e){
           System.out.println(e.getMessage());
       }
   }

   public static void readFromFile(String filename){
       double price;
       int unit;
       String desc;
       double total = 0.0;
       try(Scanner s = new Scanner(new BufferedReader(new FileReader(filename)))){
           s.useDelimiter("\n");
           while (s.hasNext()) {
               price = s.nextDouble();
               unit = s.nextInt();
               desc = s.next();
               System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price);
               total += unit * price;
           }
           System.out.format("全部数据已读入,总价格为:$%.2f%n", total);
       }catch(IOException e){
           System.out.println(e.getMessage());
       }
   }
}

  这时输出的数据和输入的数据都是通过良好格式化的,很是便于阅读和打印,可是在处理数据的时候须要进行适当的转换和解析,因此会必定程度上影响效率。在使用java.util.Scanner时,可使用useDelimiter()方法设置合适的分隔符,在 Linux 系统中,空格、冒号、逗号都是经常使用的分隔符,具体状况具体分析。在上面的例子中,我直接将每一个数据做为一行保存,这样更加简单。若是使用cat命令查看/home/youxia/testfile文件的内容,能够看到格式良好的数据,以下:

youxia@ubuntu:~$ cat testfile
19.99
12
Java T-shirt
9.99
8
Java Mug
15.99
13
Duke Juggling Dolls
3.99
29
Java Pin
4.99
50
Java Key Chain

  若是不想使用流,只想像 C 语言那样简单地操做文件,可使用 RandomAccessFile 类。

  对于 PrintStream 和 PrintWriter,咱们用得最多的就是基于命令行的标准输入输出,也就是从键盘读入数据和向屏幕写入数据。Java 中有几个内建的对象,它们分别是 System.in、System.out、System.err,由于平时用得多,我就不一一细讲了。须要说明的是,这几个对象都是字节流而不是字符流,这也能够理解,虽然咱们的键盘不能输入纯二进制数据,可是经过管道和文件重定向却能够,在控制台中输出乱码也是常见的现象,因此这几个流必须是字节流而不是字符流。若是要想按字符的方式读取标准输入,可使用 InputStreamReader 这样转换一下:

InputStreamReader cin = new InputStreamReader(System.in);

  除此以外,还可使用 System.console 对象,它是 Console 类的一个实例。它提供了几个实用的方法来操做命令行,如readLine()readPassword()等,它的操做是基于字符流的。不过在使用 System.console 以前,先要判断它是否存在,若是操做系统不支持或程序运行在一个没有命令行的环境中,则其值为null

Java 7 中引入的 NIO.2

  早在 2002 年发布的 Java 1.4 中就引入了所谓的 New I/O,也就是 NIO。可是依然被打脸, NIO 仍是不那么好用,还白白浪费了 New 这个词,搞得 Java 7 中对 I/O 的改进不得不称为 NIO.2。在 Java 7 以前的 I/O 怎么很差用呢?主要表如今如下几点:

  1. 在不一样的操做系统中,对文件名的处理不一致;

  2. 不方便对目录树进行遍历;

  3. 不能处理符号连接;

  4. 没有一致的文件属性模型,不能方便地访问文件的属性。

  因此,虽然存在java.io.File类,我前文中却没有介绍它。在 Java 7 中,引入了 Path、Paths、Files等类来对文件进行操做。Path 表明文件的路径,不一样操做系统有不一样的文件路径格式,并且还有绝对路径和相对路径之分。能够这样建立路径:

Path absolute = Paths.get("/", "home", "youxia");
Path relative = Paths.get("myprog", "conf", "user.properties");

  静态方法Paths.get()能够接受一个或多个字符串,而后它将这些字符串用文件系统默认的路径分隔符链接起来。而后它对结果进行解析,若是结果在指定的文件系统上不是一个有效的路径,那么它会抛出一个 InvalidPathException 异常。固然,也能够给该方法传递一个含有分隔符的字符串:

Path home = Paths.get("/home/youxia");

  Path 类提供不少有用的方法对路径进行操做。例如:

Path home = Paths.get("/home/youxia");
Path conf = Paths.get("myprog", "conf", "user.properties");
home.resolve(conf);   // 返回"/home/youxia/myprog/conf/user.properties"
Path another_home = Paths.get("/home/another");
home.relativize(another_home);   //返回相对路径"../another"
Paths.get("/home/youxia/../another/./myprog").normalize();    //去掉路径中冗余,返回"/home/another/myprog"
conf.toAbsolutePath();    //根据程序的运行目录返回绝对路径,如过在用户的根目录中启动程序,则返回"/home/youxia/myprog/conf/user.properties"
conf.getParent();    //得到路径的不含文件名的部分,返回"myprog/conf/"
conf.getFileName();    //得到文件名,返回"user.properties"
conf.getRoot();    //得到根目录

  使用 Files 类能够快速实现一些经常使用的文件操做。例如,能够很容易地读取一个文件的所有内容:

byte[] bytes = Files.readAllBytes(path);

  若是想将文件内容解释为字符串,能够在 readAllBytes 后调用:

String content = new String(bytes, StandardCharsets.UTF_8);

  也能够按行来读取文件:

List<String> lines = Files.readAllLines(path);

  反过来,将一个字符串写入文件:

Files.write(path, content.getBytes(StandardCharsets.UTF_8));

  按行写入:

Files.write(path, lines);

  将内容追加到指定文件中:

Files.write(path, lines, StandardOpenOption.APPEND);

  固然,仍然可使用前文介绍的 InputStream、OutputStream、Reader、Writer 类。这样建立它们:

InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
Reader reader = Files.newBufferedReader(path);
Writer in = Files.newBufferedWriter(path);

  同时,使用Files.copy()方法,能够简化某些工做:

Files.copy(in, path);    //将一个 InputStream 中的内容保存到一个文件中
Files.copy(path, out);   //将一个文件的内容复制到一个 OutputStream 中

  一些建立、删除、复制、移动文件和目录的操做:

Files.createDirectory(path);    //建立一个新目录
Files.createFile(path);         //建立一个空文件
Files.exists(path);             //检测一个文件或目录是否存在
Files.createTempFile(prefix, suffix);  //建立一个临时文件
Files.copy(fromPath, toPath);   //复制一个文件
Files.move(fromPath, toPath);   //移动一个文件
Files.delete(path);             //删除一个文件

  若是目标文件或目录存在的话,copy()move()方法会失败。若是但愿覆盖一个已存在的文件,可使用StandardCopyOption.REPLACE_EXISTING选项。也能够指定使用原子方式来执行移动操做,这样要么移动操做成功完成,要么源文件依然存在,可使用StandardCopyOption.ATOMIC_MOVE选项。

  能够经过Files.isSymbolicLink()方法判断一个文件是不是符号连接,还能够经过File.readSymbolicLink()方法读取该符号连接目标的真实路径。关于文件属性,Java 7 中提供了 BasicFileAttributes 对真正通用的文件属性进行了抽象,对于更具体的文件属性,还提供了 PosixFileAttributes 等类。可使用Files.readAttributes()方法读取文件的属性。关于符号连接和属性,来看一个示例:

package com.xkland.sample;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributes;

public class JavaIODemo {
   public static void main(String[] args) {
       if(args.length < 1){
           System.out.println("Usage: JavaIODemo filename");
           return;
       }
       Path path = Paths.get(args[0]);
       Path real = null;
       try{
           if(Files.isSymbolicLink(path)){
               real = Files.readSymbolicLink(path);
           }
           PosixFileAttributes attr = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
           System.out.format("%s, size: %d, isSymbolicLink: %b .", path, attr.size(), attr.isSymbolicLink());
           System.out.println();
           PosixFileAttributes attrOfReal = Files.readAttributes(real, PosixFileAttributes.class);
           System.out.format("%s, size: %d, isSymbolicLink: %b .", real, attrOfReal.size(), attrOfReal.isSymbolicLink());
           System.out.println();
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

  若是这样运行程序,能够查看/etc/alternatives/js文件是不是符号连接,并查看具体连接到哪一个文件:

youxia@ubuntu:~$java com.xkland.sample.JavaIODemo /etc/alternatives/java
/etc/alternatives/java, size: 35, isSymbolicLink: true .
/usr/java/jdk1.8.0_102/jre/bin/java, size: 7734, isSymbolicLink: false .

  NIO.2 API 会默认跟随符号连接,若是不要上述示例代码中的LinkOption.NOFOLLOW_LINKS选项,则Files.readAttributes()返回的结果就是实际文件的属性,而不是符号连接文件的属性。

NIO.2 中的异步 I/O

  因为 I/O 操做常常会阻塞,因此编写异步 I/O 操做的代码历来都是提升程序运行效率的有效手段。特别是 Node.js 的出现,使异步 I/O 的影响达到空前的巨大,基于 Callback 的异步 I/O 早已深刻人心。 Java 7 中有三个新的异步通道:

  1. AsynchronousFileChannel —— 用于文件 I/O;

  2. AsynchronousSocketChannel —— 用于套接字 I/O,支持超时;

  3. AsynchronousServerSocketChannel —— 用于套接字接受异步连接。

  这里只考察一下基于文件的异步 I/O。使用异步 I/O 有两种形式,一种是基于 Future,一种是基于 Callback。使用 Future 的示例代码以下:

try{
   Path file = Paths.get("/home/youxia/testfile");
   AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);    //异步打开文件
   ByteBuffer buffer = ByteBuffer.allocate(100_000);
   Future<Integer> result = channel.read(buffer, 0);    //读取 100 000 字节
   while(!result.isDone()){
       //干点儿别的事情
   }
   Integer bytesRead = result.get();    //获取结果
   System.out.println("已读取的字节数:" + bytesRead);
}catch(IOException | ExecutionException | InterruptedException e){
   System.out.println(e.getMessage());
}

  若是使用基于 Callback 的异步 I/O,其示例代码是这样的:

try{
   Path file = Paths.get("/home/youxia/testfile");
   AsynchronousFileChannel channel = AsynchronousFileChannel.open(file);
   ByteBuffer buffer = ByteBuffer.allocate(100_000);  //异步方式打开文件,分配缓冲区准备读取,和前面是同样的

   channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>(){

       public void completed(Integer result, ByteBuffer attachment){
           System.out.println("已读取的字节数:" + bytesRead);
       }

       public void failed(Throwable exception, ByteBuffer attachment){
           System.out.println(exception.getMessage());
       }
   });  //调用 channel.read() 的另外一个版本,接受一个 CompletionHandler 类的对象作参数

}catch(IOException e){
   System.out.println(e.getMessage());
}

  在这里,建立了一个回调对象,该对象有completed()方法和failed()方法,根据 I/O 操做是否成功相应的方法会被回调,这和 Node.js 中的异步 I/O 是何其的类似啊。

总结

  写完这一篇,估计我是不再会忘记 Java I/O 的用法了。认真读完我这一篇的朋友应该也同样,若是读一遍又忘记了的话,就多读几遍。固然,我这一篇文章仍不可能包含 Java I/O 的方方面面。关于具体的 API,你们直接查看 Oracle 的官方文档就能够了。读到这里的朋友,请不要忘记给个赞,谢谢。

我有一个微信公众号,常常会分享一些Java技术相关的干货;若是你喜欢个人分享,能够用微信搜索“Java团长”或者“javatuanzhang”关注。

相关文章
相关标签/搜索