1. 概览

  1. 基于单个字节(single-byte)的IO类:InputStream和OutputStream家族
  2. 基于两个字节(two-byte)的IO类:Reader和Writer家族

Java IO 结构思维导图

注意:下图并不是完整的,查看完整的IO类请看API。

2. InputStream和OutputStream

字节流顾名思义,按照byte来读取内容。InputStream和OutputStream是字节流中最大的父类(抽象类),其他子类通过继承他们形成庞大的字节流IO家族。

2.1 InputStream中的方法

  1. abstract int read():其他read方法都是基于该抽象方法来实现
  2. int read(byte[] b),最大读取b.length个字节数据;
  3. int read(byte[] b, int off, int len),最大读取len个字节数据到b字节数组中,从off位置开始存放;
  4. long skip(long n),在输入流中跳过n个字节,返回实际跳过的字节数。当遇到末尾的时候实际跳过的数据可能小于n;
  5. int available(),返回在不阻塞的情况下流中的可以读取的字节数;
  6. void close(),关闭流;
  7. void mark(int readlimit),在输入流的当前位置打一个标记(注:不是所有的流都支持这一特性);
  8. void reset(),返回到最后一个标记处。随后调用read方法会从最后一个标记处重新读取字节数据。如果当前没有标记,则不会有任何变化;
  9. boolean markSupported(),判断当前流是否支持标记操作;

总结:InputStream主要是提供了read抽象方法以及一些read的重载方法,当读到末尾的时候返回值是-1

2.2 OutputStream中的方法

  1. abstract void write(int b): 抽象方法
  2. void write(byte[] b):将b中存放的所有数据都写入到流中;
  3. void write(byte[], int off, int len):将b字节数组中从off位置开始的len个字节数据写入到流中;
  4. void close():关闭和flush输出流;
  5. void flush():对输出流做flush操作,也就是说,将所有输出流中缓存的数据都写入到实际的目的地;

2.3 关于阻塞

上面抽象的read()和write()方法都会阻塞,直到byte读写成功为止。这就意味着,如果在读写过程中,如果当前流不可用,那么当前线程就会被阻塞。为解决阻塞的问题InputStream类提供了一个avaliable()方法,可以检测当前可读的字节数。所以,下面这段代码永远不会被阻塞:

int bytesAvailable = in.available();
if(bytesAvailable > 0){
     byte[] data = new byte[bytesAvailable];
     in.read(data);         
}

2.4 关闭流

当我们读写完毕以后,应该要调用close()函数来关闭流。这样做,一方面可以释放掉流所持有的系统资源。另外一方面,关闭一个输出流也会将暂存在流中的数据flush到目标文件中去:输出流会持有一个buffer,在其buffer没有满的时候是不会实际将数据传递出去的。特别的,如果你没有关闭一个输出流,那么很有可能会导致最后那些存放在buffer中的数据没有被实际的传递出去。当然,我们也可以通过调用flush()方法手动的将buffer中的数据flush出去。

3. FilterInputStream和FilterOutputStream

3.1 IO字节流中的装饰者模式

这2个类是InputStream和OutputStream的重要子类。设计这个类实际上是应用了装饰者模式。BufferedInputStream和DateInputStream这些类实际上可以理解为“功能”,这些功能能够在其他InputStream和OutputStream上灵活添加或者去除。如果不设计过滤类,那么为了实现同样的功能,我就不得不设计有Buffered功能的piped类、Object类等等,这样很容易就类爆炸咯,而且以后万一多了一个新的“功能”又要添加很多类了。

例1:在使用FileInputStream按照字节读取的时候我希望能够使用DateInputStream的功能读取特定类型数据,那么就可以发挥装饰者模式的效果:

FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream din = new DataInputStream(fin);
Double s = din.readDouble();

例2:希望在例1的基础上,再增加缓冲功能,提高效率

FileInputStream fin = new FileInputStream("employee.dat");
BufferedInputStream bin = new BufferedInputStream(fin); //这个必须放在中间这层,因为BufferedInputStream是按照字节读取的,要比DateInputStream先封装
DataInputStream din = new DataInputStream(bin); //按照数值类型读取
Double s = din.readDouble();

3.2 再议关闭流

关闭流的时候只需要关闭最外层的装饰流即可,比如例2中BufferedInputStream装饰了FileInputStream,而DataInputStream又装饰了BufferedInputStream。只要关闭最后调用的装饰流DataInputStream即可。原因可以通过查看JAVA IO的源代码理解:

//java.io.BufferedInputStream的api:
public void close()throws IOException //关闭此输入流并释放与该流关联的所有系统资源。

4. Reader和Writer

这2个抽象类是产生了字符流的IO大家族。每次处理2个字节,比较方便处理字符。当处理字符的时候优先考虑Reader和Writer,他们一些函数能够识别编码,按照编码来读取。其中也有两个重要的方法。

abstract int read();  //返回一个0~65535之间的整数,遇到流末尾则返回-1。
abstract void write(int c);

4.1 从文件读取

我们有很多地方需要将一个file绑定到reader或者是writer上面;所以,JDK给我们提供了一对方便的读写类FileReader和FileWriter。比如说下面两种定义是等价的:

//方便的定义方式
FileWriter out = new FileWriter("output.txt");

//等价的定义方式
FileWriter out = new FileWriter(new FileOutputStream("output.txt"));

4.2 标准输出PrintWriter

 对于文本的输出,有一个方便的类PrintWriter。因为,这个类提供了文本格式的写字符串和写数字的方法,其print方法有很多种重载方式。同时,我们还可以很方便的将PrintWriter和FileWriter联系起来,下面的两种方式是等价的:

//定义PrintWriter的便捷方式
PrintWriter out = new PrintWriter("out.txt");

//等价的定义方式
PrintWriter out = new PrintWriter(new FileWriter("out.txt"));

//联想到FileWriter我们还可以得出一种等价方式
PrintWriter out = new PrintWriter(new FileWriter(new FileOutputStream("out.txt")));

PrintWriter自带了一个缓冲器,默认情况下只有在缓冲区填满的时候才会将数据flush到目的地。PrintWriter的构造器有两种:

//默认情况下 autoFlush是关闭的,缓冲区慢才会将数据传递出去
PrintWriter out = new PrintWriter(Writer out);

//可以指定autoFlush为true。这样,无论何时调用print函数,都会立刻flush缓冲区
PrintWriter out = new PrintWriter(Writer out, boolean autoFlush);

PrintWriter有如下的构造函数

PrintWriter(Writer out)
PrintWriter(Writer out, boolean autoFlush)

PrintWriter(String fileName)
PrintWriter(File file)

//这个很强大,可以直接对输出流做打印
PrintWriter(OutputStream out)
PrintWriter(OutputStream out, boolean autoFlush)

//还有一个很有意思的printf函数。这个对调整格式很方便
void printf(String format, Object... args)

4.3 怎样读文本

 如我们所知道的,对二进制数据的读写很方便的可以使用DataInputStream和DataOutputStream对。上面也说了,写Text有一个很好用的PrintWriter。那么,读Text呢?还会有想二进制这么方便吗?比如说,我想读取一个Double类型的数据: r.readDouble()。答案:不好意思,没有!!

  就目前来讲,有两种方式:①、Scanner类可用,也提供了不少方法;②、BufferedReader in = new BufferedReader(new FileReader("employee.txt"));可用,用它来读取一行,然后自行分解去吧。但是,BufferedReader么有读取numeric这么方便的方法。

  其实,也可想而知,文本嘛,就没有所谓的Double啊,Integer啊什么的区别了,所有的都是“文本”了,只是它长得像数字罢了。

5. IO类的选择

5.1 按数据来源(去向)分类:

1、是文件: FileInputStream, FileOutputStream, FileReader, FileWriter。其中根据文件特性选择使用字符流还是字节流,文件是图片、音频等,自然考虑字节流,如果是文本文件,那就使用字符流。
2、是byte[]:ByteArrayInputStream, ByteArrayOutputStream
3、是Char[]: CharArrayReader, CharArrayWriter
4、是String: StringReader, StringWriter
5、网络数据流:InputStream, OutputStream, Reader, Writer

5.2 按是否格式化输出分:

1、要格式化输出:PrintStream, PrintWriter

5.3 按是否要缓冲分:

1、要缓冲:BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter

5.4 按数据格式分:

1、二进制格式(只要不能确定是纯文本的): InputStream, OutputStream及其所有带Stream结束的子类
2、纯文本格式(含纯英文与汉字或其他编码方式);Reader, Writer及其所有带Reader, Writer的子类

5.6 按输入输出分:

1、输入:Reader, InputStream类型的子类
2、输出:Writer, OutputStream类型的子类

5.7 特殊需要:

1、从Stream到Reader,Writer的转换类:InputStreamReader, OutputStreamWriter
2、对象输入输出:ObjectInputStream, ObjectOutputStream
3、进程间通信:PipeInputStream, PipeOutputStream, PipeReader, PipeWriter
4、合并输入:SequenceInputStream
5、更特殊的需要:PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader

决定使用哪个类以及它的构造进程的一般准则如下(不考虑特殊需要):
首先,考虑最原始的数据格式是什么: 字符流还是字节流
第二,是输入还是输出
第三,是否需要转换流
第四,数据来源(去向)是什么
第五,是否要缓冲:(特别注明:一定要注意的是readLine()是否有定义,有什么比read, write更特殊的输入或输出方法)
第六,是否要格式化输出

6 Java中Inputstream与Reader的区别

  • Reader及其子类支持16位的Unicode字符输出,InputStreamr及其子类支持8位的字符输出。
  • Reader和InputStream分别是I/O库提供的两套平行独立的等级机构,
  • InputStream、OutputStream是用来处理8位元的流,
  • 值得说明的是,在这两种等级机构下,还有一道桥梁InputStreamReader、OutputStreamWriter负责进行InputStream到Reader的适配和由OutputStream到Writer的适配。

  • Socket 用于套接字;

  • URLConnection 用于 URL 连接。

  • Socket和URLConnection这两个类使用 getInputStream() 来读取数据。

7. 使用例子

7.1 字节流读取网络图片文件,并且写入本地

这里注意下读取这种字节流,最好指定下开始读取的位置和读取的长度,以免出错!

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * Created by Kaiming Wan on 2015/10/15 0015.
 */
public class TestIO {
    public static void main(String[] args) throws IOException{
        //创建一个URL对象,我才不会告诉你这是一张性感的图片,哈哈
        URL url = new URL("http://e.hiphotos.baidu.com/image/pic/item/6159252dd42a2834880890dc5fb5c9ea14cebfbf.jpg");

        //打开连接
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();

        //设置 GET请求
        conn.setRequestMethod("GET");

        //设置响应超时5s
        conn.setConnectTimeout(5*1000);

        //由于是图片文件,采用字节流获取
        InputStream is = conn.getInputStream();

        //使用过滤类的子类中的缓冲类包装下提高下效率=。=
        BufferedInputStream bis = new BufferedInputStream(is);

        //4k读取
        byte[] buffer = new byte[4096];


        //创建文件
        File image = new File("D:/test_img.jpg");



        //FileOutputStream来写入外部文件,不使用缓冲区
        FileOutputStream os = new FileOutputStream(image);

        //指定读取长度,否则会出错
        int len = 0;

        //读到尾部就是返回-1了
        while((len=bis.read(buffer))!=-1){
           os.write(buffer,0,len);              //从0位置开始读,每次读取len的长度
        }


        //关闭流
        bis.close();
        os.close();
    }
}

7.2 使用字符流存取本地文件

使用FileReader和FileWriter有时候会出现乱码问题,因为它使用的是系统默认编码,如果需要自己指定编码,可以使用InputStreamReader和InputStreamWriter

import java.io.*;

/**
 * Created by Kaiming Wan on 2015/10/15 0015.
 */
public class TestLocalIO {
    public static void main(String[] args) throws IOException{


        //父类接受要读取的文件,接受一个string
        FileReader fr = new FileReader("D://test.txt");


        //装饰类装饰下
        BufferedReader br = new BufferedReader(fr);

        //记录内容
        String s;

        //创建文件,用于写入结果
        File f = new File("D://new.txt");

        //创建一个带缓冲的FileWriter用于写出文件
        FileWriter fw = new FileWriter(f);

        //缓冲类装饰下
        BufferedWriter bw = new BufferedWriter(fw);

        while((s=br.readLine())!=null){         //一行为空的时候作为结束
            bw.write(s);        //写入文件,默认使用系统编码,如果需要自定义编码请使用InputStreamReader和OutputStreamWriter
        }

        br.close();
        bw.close();

    }
}

参考资料:
http://www.cnblogs.com/lj95801/p/4872955.html?utm_source=tuicool
http://zhidao.baidu.com/link?url=gYzbqjPGmQNGqETt7HpgIKCXuucQd99F93O_XLEacyWegxtebkUxrjVJZTLKPlydAMiMdFLU1MQMfFy-qYJRvTzR9-lZGEtaB4MzJW8ghii
http://www.ihypo.net/1908.html