Java中的I/O流

作者: 宝塔山上的猫 | 来源:发表于2016-05-21 11:20 被阅读566次

    谷歌I/O大会,现在赶一下时髦,在这里说一下Java的I/O流。
    I/O流可以简单的理解input/output流,既是输入输出流。谷歌I/O大会也可以解释为谷歌输入/输出大会,哈哈!电脑不就是用来输入输出的工具嘛,这样理解也没问题。
    话归正题,I/O流在java中是属于基础类,是很重要的一个工具,有了它,才能够实现信息之间的交互,即从本地向网络发出请求,然后在网络中传回信息。
    input和output是相对而言的,即我们拥有一个客户端处理器,如果外部有信息要传进来处理,那边是输入流input,而当我们有信息要输出时,那边是输出流output了。

    I/O流解析

    举个栗子来说吧,
    例子1:我们在平常使用手机上网的时候,从手机上点击链接,这时向互联网发出指令,这是就是输出output。而当互联网返回页面的时候,那么就是输入流了。
    例子2:当我们下载文件的时候,互联网源源不断的将文件内容传到手机上,这是输入流input。但同时传到手机上时只是一个缓存,还需要在存储卡中把内容保存起来,因此手机把文件信息保存到内置存储卡中时便是输出流output了。
    解释完什么是I/O,现在我们开始输入/输出流的使用吧。

    I/O流的使用

    我们在这里主要讲解Stream流、Reader和Writer这三种。

    字节流

    Java中的Stream流主要指InputStream流和OutputStream流。
    其中InputStream流和OutputStream流是抽象类,但是它们各自有同名的子类InputStream流和OutputStream,没错,它们的名字是一样的。
    作为抽象类的InputStream有读取字节流(byte)的read()方法,它可以读取字节或者字节数组,其次是close()方法,每次使用InputStream流之后记得使用close()方法,这样才不会占用系统资源。
    相应的OutputStream流着拥有wrire()和close()方法,效果同InputSream,只是它是将直接写出。
    作为抽象类的InputStream流和OutputStream流,他们各自有许多个实现的之类
    如InputStream的子类有:

    AudioInputStream
    , ByteArrayInputStream
    , FileInputStream
    , FilterInputStream
    , InputStream
    , ObjectInputStream
    , PipedInputStream
    , SequenceInputStream
    , StringBufferInputStream

    如OutputStream的子类有:

    ByteArrayOutputStream
    , FileOutputStream
    , FilterOutputStream
    , ObjectOutputStream
    , OutputStream
    , PipedOutputStream

    其中有个子类FileInputStream流,这个子类是从电脑磁盘或者内存卡中的文件直接读取字节,而和FileOutputStream流则直接输出信息到文件的流。

    fileInputStream.png fileOutputSream.png

    用法:这是复制文本文件的一个方法,src和dst分别指传入和传出的文本文件路径

    public static void copyFile(File src, File dst) throws IOException {
        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dst);
        int data = -1;
        // fis.read()如果已到达文件末尾,则返回 -1。
        while ((data = fis.read()) != -1) {
            fos.write(data);
        }
        fis.close();
        fos.close();
    }
    

    上面的方法虽然能达到目的,但是在实际使用的时候因为缺乏缓存,复制文件的时间过长导致卡顿,因此可以使用缓存区来加速,可以将代码修改为:

    public static void copyFile(File src, File dst) throws IOException {
        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dst);
        // 创建一个1M大小的缓冲区,用来存放输入流中的字节
        byte[] buf = new byte[1024 * 1024];
        // 用来保存实际读到的字节数
        int len = 0;
        // fis.read(buf) 读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。
        while ((len = fis.read(buf)) != -1) {
            // fos.write(buf, 0, len) 将指定 byte 数组中从偏移量 0 开始的 len 个字节写入此文件输出流。
            fos.write(buf, 0, len);
        }
        fis.close();
        fos.close();
    }
    

    接下来就是ByteArrayInputStream流,它将字节数组当作源的输入流;ByteArrayOutputStream则是将字节数组当作目标的输出流。值得注意的是这个输入输出流虽然都有一个close()方法,但是由于ByteArrayInputSteam/ByteArrayOutputStream并没有调用系统资源,因此这个关闭方法没有任何意义。
    ByteArrayInputSteam使用示例:

    public static void main(String[] args) throws IOException {
        String str = "hello,World";
        ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes());
        int data = -1;
        while ((data = bis.read()) != -1) {
            System.out.print((char) data);
        }
        bis.close();
    }
    

    ByteArrayOutputStream使用示例:

    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 写入字节,字符a在Unicode编码表中是97,A是65
        bos.write(97);
        bos.write(65);
        bos.write("hello,world".getBytes());
        byte[] buff = bos.toByteArray();
        for (byte data : buff) {
            System.out.print((char) data);
        }
    }
    

    FilterInputStream/FilterOutputStream过滤流

    FilterInputStream/FilterOutputStream过滤流,为底层透明的提供扩展方法的输入/输出流的包装。过滤流的构造方法需要传入一个参数,FilterInputStream流及其子类均需要传入InputStream流,而FilterOutputStream流及其子类则需要传入OutputStream流。

    ** FilterInputStream的子类有:**

    BufferedInputStream,
    CheckedInputStream,
    CipherInputStream,
    DataInputStream,
    DeflaterInputStream,
    DigestInputStream,
    InflaterInputStream,
    LineNumberInputStream,
    ProgressMonitorInputStream,
    PushbackInputStream

    FilterOutputStream的子类有:

    BufferedOutputStream,
    CheckedOutputStream,
    CipherOutputStream,
    DataOutputStream,
    DeflaterOutputStream,
    DigestOutputStream,
    InflaterOutputStream,
    PrintStream

    例如BufferedInputStream/BufferedOutputStream流,它们自带一个缓冲区,不必像上面copyFile()方法一般需要自定义一个byte数组来充当缓冲区,我们再次修改copyFile()方法,实现用过滤流复制文件。

    public static void copyFile(File src, File dst) throws IOException {
        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dst);
        // FileInputStream与FileOutputStream是InputStram和OutputStream的子类,
        // 因此能直接被BufferedInputStream与BufferedOutputStream读取
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        // data用来保存实际读到的字节数
        int data = 0;
        while ((data = bis.read()) != -1) {
            bos.write(data);
        }
        bis.close();
        bos.close();
    }
    

    但是对于上面的输入输出流仍然存在着一个问题,那就是对于Java的基本数据类型的读取,如int/double/boolean/char类型的读取,因此引入了DataInputStream/DataOutputStream来处理。需要注意的是使用它们时,读取与写出的顺序必须要一致。
    DataInputStream使用示例:

    public static void main(String[] args) throws IOException {
         String name = "zhangsan";
         int age = 10;
         boolean flag = true;
         char sex = '男';
         double money = 100.56;
         DataOutputStream dos = new DataOutputStream(new FileOutputStream("e:\\b.txt"));
         dos.writeUTF(name);
         dos.writeInt(age);
         dos.writeBoolean(flag);
         dos.writeChar(sex);
         dos.writeDouble(money);
         dos.close();
    }
    

    DataOutputStream使用示例(读取上面使用DataInputStream写入的信息):

    public static void main(String[] args) throws IOException {
        DataInputStream dis = new DataInputStream(new FileInputStream("e:\\b.txt"));
        // 读的顺序必须和写的顺序一致
        System.out.println(dis.readUTF());
        System.out.println(dis.readInt());
        System.out.println(dis.readBoolean());
        System.out.println(dis.readChar());
        System.out.println(dis.readDouble());
        dis.close();
    }
    

    字符流

    原本使用字节流已经满足了I/O的所有需求的,但是可惜的是计算机的编码格式太多,如中国的GBK,还有ASCII/Unicode等编码,不同的编码之间并不兼容,如GBK是一个汉字有两个字节,而ASCII/Unicode则是一个字母一个字节,如果用ANSI来解析GBK则会造成乱码。这时字符流Reader类和Writer类就派上用场了,要注意的是,字符流虽然读取的是char数组,但它们的底层还是依靠byte字节来读取的。
    由于Reader和Writer是抽象类,因此要靠子类来实现它们的抽象方法

    Reader类的子类:

    BufferedReader,
    CharArrayReader,
    FilterReader,
    InputStreamReader,
    PipedReader,
    StringReader

    Writer类的子类:

    BufferedWriter,
    CharArrayWriter,
    FilterWriter,
    OutputStreamWriter,
    PipedWriter,
    PrintWriter,
    StringWriter

    FilterReader/FilterWriter

    FilterReader/FilterWriter对应的是字节流中的FilterInputStream和FilterOutputStream对应,能够直接对文件进行操作,但是它是假定文件中的默认编码格式是电脑中的默认编码格式来读取的,是InputStreamReader和OutputStreamWriter的子类
    示例:

    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("e:\\c.txt");
        FileWriter fw = new FileWriter("e:\\d.txt");
        char[] buff = new char[100];
        int len = 0;// 实际读到的字符个数
        while ((len = fr.read(buff)) != -1) {
            fw.write(buff, 0, len);
            // 当FileWriter忘记关闭,而FileWriter缓存没写满时,文件将写入失败,
            // 这是应该调用flush()方法,强制清空缓存,将信息写入文件中
            // fw.flush();
        }
        fr.close();
        // close()在使用的时候会强制调用flush(),强制清空缓存
        fw.close();
    }
    

    BuferedReader和BufferedWriter

    通过缓冲输入/输出提高性能。这个方法所读取得是字符输入/输出流,即InputStreamReader/OutputStreamWriter流,因为Reader类和Writer类(如FileReader和FileWriter)会频繁的调用底层资源,因此建议使用BufferedReader对其进行包装,如:BufferedReader in = new BufferedReader(new FileReader("foo.in"));
    另外由于每个平台如Linux和windows的换行符符号不同的,因此BufferedWriter中有newLine()方法来实现各的平台统一换行。

    BuferedReader和BufferedWriter示例:

    public static void main(String[] args) throws IOException {
        FileReader fr = new FileReader("e:\\EclipseDemo1.java");
        BufferedReader br = new BufferedReader(fr);
        FileWriter fw = new FileWriter("e:\\aa.java");
        BufferedWriter bw = new BufferedWriter(fw);
        String line = null;
        // br.readLine()到达流的末尾时会返回null
        while ((line = br.readLine()) != null) {
            System.out.println(line);
            bw.write(line);
            bw.newLine();// 换行
            // flush()方法同FileWriter
            // bw.flush();
        }
        br.close();
        bw.close();
    }
    

    转换流InputStreamReader/OutputStreamWriter

    转换流能够将字节流和字符流相互转换,如将InputStream流转换为InputStreamReader字符流,用OutputStreamWriter转换为OutputStream,需要传入InputStream/OutputStream类的参数。
    另外转换流还有一个优势,它能够将读取指定编码格式的文件信息,而FileReader只能够读取默认编码格式的文本文件。

    OutputStringWriter示例:

    public static void main(String[] args) throws IOException {
        FileOutputStream fos = new FileOutputStream("e:\\1.txt");
        OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
        BufferedWriter bw = new BufferedWriter(osw);
        bw.write("你好");
        bw.close();
    }
    

    InputStreamReader示例:

    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("e:\\1.txt");
        InputStreamReader isr = new InputStreamReader(fis, "utf-8");
        BufferedReader br = new BufferedReader(isr);
        String line = null;
        // br.readLine()到达流的末尾时会返回null
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        br.close();
    }
    

    总结

    对于JAVA中的I/O流就到这里结束了,I/O流中的类和方法都大同小异,相信只要掌握了本文所说的这些类和方法,那么就能掌握绝大部分的I/O流操作了。
    I/O流之中有一些共同的特性,它们可以直接在各自的抽象类中找到.
    如:

    • 继承自InputStream抽象类和Reader抽象类的子类中,它们都有read()方法,而且使用这个方法读取到文件末尾时都是返回-1。
      (字符流中的BufferedReader有readLine()方法,它返回的是null值。相应的BufferedWriter中有newLine()方法用来换行,但没有返回值)

    最后以获得网络图片作为JAVA中的I/O流的结束吧!

    /**
     * 
     * @param urlString 下载网络资源的地址,如图片的下载地址
     * @param fileName  要把下载的资源的重命名
     * @param savePath  要下载资源保存到哪里
     * @throws Exception
     */
        public static void download (String urlString, String fileName,
                String savePath) throws IOException {
            URL url = new URL(urlString);
            URLConnection conn = url.openConnection();
            // 网络连接超时的时间限制
            conn.setReadTimeout(5*1000);
            conn.setConnectTimeout(5*1000);
            // 获取文件流
            InputStream is = conn.getInputStream();
            byte[] buff = new byte[1024];
            int len = 0;
            // 判断下载保存的路径是否存在,不存在则创建
            File file = new File(savePath);
            if(!file.exists()){
                file.mkdirs();
            }
            // 将获取的流写入到电脑磁盘中
            OutputStream os = new FileOutputStream(file.getAbsolutePath() + "\\" + fileName);
            while((len = is.read(buff)) != -1 ) {
                os.write(buff, 0, len);
            }
            is.close();
            os.close();
        }
    

    补充知识点

    最近重新看了一下IO流的知识,发现又有些迷糊,然后又仔细研究了一下。

    关于字节流:

    字节流也就是byte,按照字节读取,也就是说只要集成了InputStream的流对象都是字节流,都使用byte。
    字节流的读取方法一般用以下两种:

    read() :从此输入流中读取下一个数据字节,每次会返回一个字节
    这种时候,读取和写入的方法如同上面的FileInputStream的操作一般,每读取一个字节就写入一个字节。

    第二种办法就是使用数组缓冲区的方法read(byte[] b) ,它将会将读取的数据保存在缓冲区中,也是像上面使用缓冲的FileInputStream的操作一般,然后使用OutputStream写入。

    但是在实际操作中发现写入的过程中使用write(byte b[])和write(byte b[], int off, int len)(上文中的FileInputStream缓冲区写入)方法均可写入,代码如下:

            File file1 = new File("D:\\a\\1.txt");
            File file2 = new File("D:\\a\\2.txt");
            FileInputStream fis = new FileInputStream(file1);
            FileOutputStream fos = new FileOutputStream(file2);
            // 创建一个1M大小的缓冲区,用来存放输入流中的字节
            byte[] buf = new byte[1024 * 1024];
            // fis.read(buf) 读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。
            while (fis.read(buf) != -1) {
                fos.write(buf);            
            }
            fis.close();
            fos.close();
    

    这让我不禁脑洞打开,或许只使用write(byte b[])方法便能够完成操作,而不必调用传入多个参数的方法呢?

    但实际上这种方法并不可取,为什么呢?

    在完成操作之后,仔细查看1.txt和2.txt的却别,发现1.txt的文件大小之后1k,但是2.txt却有1024k,打开2.txt之后发现多出来的大小全部都是空格!
    通过查看源码我们得知:

    public void write(byte b[]) throws IOException {
        writeBytes(b, 0, b.length, append);
        // 写入了整个缓冲区的大小,包括没有内容的部分
    }
    
    public void write(byte b[], int off, int len) throws IOException {
        writeBytes(b, off, len, append);
        // 按照实际的数组内容长度写入
    }
    

    这两种方法同样调用了writeBytes()方法,但是write(byte b[])它会将byte[]的大小设置为需要读取的大小,也就是说如果我们的缓冲区设置为1024*1024也就是1024k的大小,它就读取1024k的大小,如果设置了1024m的大小也将同样读取1024M的大小,即使内容的实际长度并没有这么多。
    这边是两者之间的区别。

    关于字符流

    字符流也就是char,按照字符读取,也就是说只要集成了Reader的流对象都是字符流,都使用char。

    如果字节流的读写操作一样,他们都有read()和writer()方法,区别只是将其中的byte换成char而已。

    同样写入方法有这两种:

    write(char[] cbuf) 
    write(char[] cbuf, int off, int len) 
    

    它们出现的问题与字节流是一样的。

    因此我们写入文件操作的时候切记要设置好写入的长度,否则写入空内容不仅浪费效率,还会导致保存的文件信息错误而无法打开。

    由此明白了这些事情:

    字节流设置了缓冲数组:

    byte[] buf = new byte[1024 * 1024];
    

    在InputStream读取的过程中,会将内容保存到buf中。

    字符流设置缓冲数组:

    char[] buff = new char[100];
    

    在Reader的读取过程中会将内容保存到buff当中。

    但是在读取过程中我们要设置当前读了多少,也就是设置:

    int len = 0;// 实际读到的字符个数
    (len = fr.read(buff)) != -1
    

    然后在通过write(char[]/byte[] cbuf, int off, int len) 保存相应长度的内容。

    流转String

    对于String字符串对象而已,也是一样,

    我们发现String的构造方法中有char和byte的方法,如下:

    String(byte[] bytes) // 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
    String(byte[] bytes, int offset, int length) 
    // 通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。
    
    String(char[] value)     // 分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。
    String(char[] value, int offset, int count) 
    // 分配一个新的 String,它包含取自字符数组参数一个子数组的字符。
    

    但是与字节流和字符流一样,都是参数少的会调用参数多的方法来创建String,因此在将流存为字符串的时候,也要遵守上面的规则,也就是说要设置读取了多长的内容。

    字节流转为String方法如下:

        File file1 = new File("D:\\a\\1.txt");
        FileReader fr = new FileReader(file1);
        // 创建一个1M大小的缓冲区,用来存放输入流中的字节
        char[] buf = new char[1024 * 1024];
        // 用来保存实际读到的字节数
        int len = 0;
        // fis.read(buf) 读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。
        while ((len = fr.read(buf)) != -1) {
            System.out.println(new String(buf, 0, len));
        }
        fr.close();
    

    字符流转换方法如下:

        File file1 = new File("D:\\a\\1.txt");
        FileInputStream fis = new FileInputStream(file1);
        // 创建一个1M大小的缓冲区,用来存放输入流中的字节
        byte[] buf = new byte[1024 * 1024];
        // 用来保存实际读到的字节数
        int len = 0;
        // fis.read(buf) 读入缓冲区的字节总数,如果因为已经到达文件末尾而没有更多的数据,则返回 -1。
        while ((len = fis.read(buf)) != -1) {
            System.out.println(new String(buf, 0, len));
        }
        fis.close();
    

    关于I/O流就到此结束了

    相关文章

      网友评论

      • a841b6c2d7ca:例一错了吧
        宝塔山上的猫:@十年布局只为娶你 恩恩,这里确实写错了
        a841b6c2d7ca: @宝塔山上的猫 您举的例子说手机向互联网发送指令信息时"输入output",是不是应该是输出呢?
        宝塔山上的猫:@十年布局只为娶你 哪里出问题了呢?

      本文标题:Java中的I/O流

      本文链接:https://www.haomeiwen.com/subject/frunrttx.html