Java 杂谈程序员技术栈Android基础知识

【Java】1.0 Java核心之IO流(一)——生猛理解字节流

2019-03-09  本文已影响0人  bobokaka
1.0 为什么要写这个。
1. 到底什么情况下怎么写
2. 什么方案和代码,写会没有什么大的问题。
3.本来一个内容就打算写一篇的,简书说我写得太长了,不许我发布,所以拆成两篇,查阅本篇的朋友请结合另一篇一同参考,谢谢。

链接如下:
【Java】2.0 Java核心之IO流(二)——生猛理解字符流

2.0 概念
3.0 IO流常用父类
4.0 说到这里 ,你肯定觉得烦,因为上面讲地的确都是废话。
5.0 IO程序代码只有3步

是的,无论你要考虑什么情况,判断什么条件,发送什么东西,通通只要2元……
好吧,总共分为4步走:

6.0 字节流(7.0是缓冲字节流,8.0是字符流、9.0是缓冲字符流,END,IO流结束,本篇结束)
6.1 下面贴上一段完整的版本的处理方案:
package com.edpeng.stream;

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

public class Demo_TryFinally {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        //demo1();
        try(
            //这个xxx.txt,自己在工程目录下新建一个就好,里面自己录入一些英文字母就可以
            //别录入中文,早晚会错
            FileInputStream fis = new FileInputStream("xxx.txt");
            FileOutputStream fos = new FileOutputStream("yyy.txt");
            //下面这行纯粹为了测试用,可以没有的
            MyClose mc = new MyClose();
        ){
            int b;
            //读一行就写(复制)一行,读写就是这样完成的,当然你可以拆开,往里面加逻辑。
            while((b = fis.read()) != -1) {
                fos.write(b);
            }
        }
    }

    public static void demo1() throws FileNotFoundException, IOException {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream("xxx.txt");
            fos = new FileOutputStream("yyy.txt");
            
            int b;
            while((b = fis.read()) != -1) {
                fos.write(b);
            }
        }finally {
            try{
                if(fis != null)
                    fis.close();
            }finally { //try fianlly的嵌套目的是能关一个尽量关一个
                if(fos != null)
                    fos.close();
            }
        }
    }

}

class MyClose implements AutoCloseable {
    public void close() {
        System.out.println("我关了");
    }
}

6.11 demo1()方法main主函数里面的方法,是两套。
demo1()方法主要是java jdk1.6以前版本使用的方法

先解释demo1()方法,虽然里面的套路我们一般不这么用,但这样的确是最严谨的写法,常常会在面试中用到,当然,如果你平时都这么写,肯定是最好的
-demo1()方法main主函数两个方法的目的都是读取文件,输出(复制)新的文件。
-demo1()方法,如果去掉里面的try/finally,也是可以的,因为异常已经在方法那里直接抛出来了,这样做相当于try/finally里面镶嵌了一套try/finally
-demo1()方法,之所以这样做,主要是为了处理3个方面的问题,:

        1. fis和 fos报错。
        2. read()和write()报错。
        3.  fis.close();和   fos.close();报错。

为什么要处理这3种报错情况?

第1种,可能存在读取不到指定文件(可能不存在了),或可能存在没法写入(指定目录不存在)。

如果没有最外层的try/finally,假设fis = new FileInputStream("xxx.txt");工作正常,fos = new FileOutputStream("yyy.txt");抛异常,问题来了,第2个fis代表的FileInputStream文键输入流没关!运行不到“fis.close();”代码就嗝屁了。

第2种,于是我们有了最外层的try/finally,那么无论try里面怎么抛异常,fis.close();fos.close();都能在finally里面关掉。

但是try里面还有一个问题,read()和write()抛异常

有人会说,只要文件存在,读和写怎么会抛异常呢,文件里面就算是乱码,那就乱读乱写呗。在windows操作环境下,基本没什么大问题,但比如在linux环境下,文件经常存在可读性、可改性等基本属性,所以很可能即使fis和 fos不抛异常,read()和write()还是会报错的。所以这个也需要写入到try/finally里面。

第3种finally里面又嵌入了try/finally语句

这是为了解决如果第一个fis.close();抛异常(比如数据库奔溃,服务器宕机等,没关成)的话,至少第二个 fos.close();不会因为第一个直接抛异常而终止程序,导致 fos.close();没关,好歹至少关一个是吧。

完毕。

6.12 main主函数,大家不要上去翻代码了,这里直接贴下来:

public static void main(String[] args) throws IOException {
        try(
            //这个xxx.txt,自己在工程目录下新建一个就好,里面自己录入一些英文字母就可以
            //别录入中文,早晚会错
            FileInputStream fis = new FileInputStream("xxx.txt");
            FileOutputStream fos = new FileOutputStream("yyy.txt");
            //下面这行纯粹为了测试用,可以没有的
            MyClose mc = new MyClose();
        ){
            int b;
            //读一行就写(复制)一行,读写就是这样完成的,当然你可以拆开,往里面加逻辑。
            while((b = fis.read()) != -1) {
                fos.write(b);
            }
        }
    }

    class MyClose implements AutoCloseable {
        public void close() {
            System.out.println("我关了");
        }
    }

可以看到,代码简化了不少,用了一个try( ){ }语句,这个是java jdk1.7之后,可以使用的新玩法。
这个语句命令的意思:无论大括号里面做什么,最后小括号里的东西,都会调用自己本来就是实现好的接口中的close()方法。

这里面有个MyClose( )方法,这主要为了我们这个例子测试用,平常不要加这个方法。 这个方法实现了接口AutoCloseable。因为我们的 InputStreamOutputStream类其实都实现了这个接口。

不信我们举个例子,查看FileInputStream类的源代码:

2019-03-09_011845.png
在eclipse里面按住ctrl键,然后挪动鼠标放到FileInputStream类上,点击Open Declaration,这样我们可以查看源代码(查看不了的可以想想办法,百度一下就好了。需要的留言,给教程)。 2019-03-09_012244.png
可以看到,FileInputStream类继承了InputStream类,接着查看InputStream类源代码:
2019-03-09_012401.png
可以看到,InputStream类实现了一个Closeable接口,接着查看Closeable接口源代码:
2019-03-09_012534.png
可以看到,Closeable类继承了AutoCloseable类,,接着查看AutoCloseable类源代码:
/*
 * Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 */

package java.lang;

/**
 * An object that …… …… non-I/O-based forms.
 *
 * @author Josh Bloch
 * @since 1.7
 */
public interface AutoCloseable {
    /**
     * Closes this resource, relinquishing any …………  if this resource cannot be closed
     */
    void close() throws Exception;
}

这里不适合截图,直接放上来源代码,里面大段注释被我省略号了。

可以看到这个@since 1.7,是java jdk1.7版本之后才实现的接口,里面就一个方法close( )

其实我们只要某个方法实现了这个AutoCloseable抽象类,就相当于拥有了可以自闭 的技能。

我们用一个MyClose( )方法,实现抽象方法AutoCloseable类后,重写里面的close( )方法,不仅能够自闭 ,还可以告诉别人它自闭 了。所以在try 语句里面小括号就会自动调用 InputStream类、OutputStream类和MyClose( )方法中自带的自闭技能 关闭自己。

6.2 细节来了。

6.21 大家注意到:

              int b;
            //读一行就写(复制)一行,读写就是这样完成的,当然你可以拆开,往里面加逻辑。
            while((b = fis.read()) != -1) {
                fos.write(b);
            }

read()方法 返回值为什么是int
首先,上面两句话是事实,我们下面这么多分析,只是为了理解为什么java要这么设计。

如果是byte 类型,那就是1个字节(8位)为单位读取,
就像下面这样:
00010100 00100100 01000001 11111111 0000000
这样,byteread() 5次才能全部读完。但是,当我们读到11111111 的时候,出事了。因为它就是一个byte类型的-1,分析如下:
10000001  byte类型的-1的原码
11111110  -1的反码
11111111  -1的补码
如果每次读取都返回byte,有可能在读到中间的时候遇到111111111,那么这11111111是byte类型的-1,我们的程序是遇到-1就会停止不读了,后面的数据就读不到了。

如果是int 类型,那就是4个字节为单位读取,当读到11111111的时候,它会在前面补上3个字节,补上24个0凑足4个字节,变成下面这样:
00000000 00000000 00000000 11111111 这样,,这个数就会变成一个正的255。
这样可以保证整个数据读完,而结束标记的-1就是int类型
read( )方法 是1个字节1个字节地读,所以每次读都会去补上24个0凑足4个字节。

我知道你上面可能还是看不懂,没关系,至少可以知道是这么回事,o(╯□╰)o实在没办法解释得更加通俗了。

接着继续深入:
上面说了,read( )方法 会给每次读都会去补上24个0凑足4个字节,那写的时候岂不是要出错?
不要担心,因为我们其实读取文件是用read( )方法读取的,我们的 write( )方法 会在每次写的时候,自动去掉前面的24个0,一样会保证数据的原样性。( write( )方法 一次写出也是1个字节)

FileOutputStream fos = new FileOutputStream("bbb.txt"); //如果没有bbb.txt,会创建出一个
        //虽然写出的是一个int数,但是在写出的时候会将前面的24个0去掉,所以写出的是一个byte
        fos.write(97);
        fos.write(98);
        fos.write(99);
        fos.close();

这里的结果是,会在bbb.txt文件里面存入“abc”,解释如上,所以如果手动写入,大可不必自己先在想输入的字节面前傻乎乎的手动添0

6.22 如下声明:

   FileOutputStream fos = new FileOutputStream("yyy.txt");

FileOutputStream输出流在创建对象的时候,如果没有这个文件,会帮我们创建出来新的文件,如果有现成的,会把里面的内容清空,把新的内容写进去。

这里也有一个细节,有人可能注意到,到底什么时候,哪条语句,执行了清空(新建)的命令。
没错,就是FileOutputStream fos = new FileOutputStream("yyy.txt");这句。而且是所有的输出流都是这样,不只是FileOutputStream字节输出流。

重点来了:如果想读取同一个文件,经过一番逻辑处理后把数据又存回原文件,千万不要声明FileInputStream指向文件,接着声明FileOutputStream指向同一个文件,这时候因为你的声明导致那个文件里面的内容已经被清空了。
举例说:下面这样是不行的,运行结果是null。因为aaa的文件在输出流声明之后里面已经没东西了。

public static void demo() throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream("aaa.txt");
        FileOutputStream fos = new FileOutputStream("aaa.txt");
        byte[] arr = new byte[fis.available()];
        fis.read(arr);                        
        fos.write(arr);                                             
        
        fis.close();
        fos.close();
}

下面这样就没问题了:

public static void demo() throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream("aaa.txt");
        byte[] arr = new byte[fis.available()];
        fis.read(arr); 
        FileOutputStream fos = new FileOutputStream("aaa.txt");
        fos.write(arr);                                             
        
        fis.close();
        fos.close();
}

这个例子也让我们体验到流使用时(对,凡是各种语言中,不仅仅是java语言,只要是涉及流这种定义的),常遵循的一个原则——晚开早关晚开早关晚开早关,重要的话说三遍,什么时候用,什么时候再开流,不用了就及时关掉。


6.23 接着说写的事情。前面说到:FileOutputStream输出流在创建对象的时候,如果没有这个文件,会帮我们创建出来新的文件,如果有现成的,会把里面的内容清空,把新的内容写进去。
那么如何把里面原有的内容不删掉,直接在后面添加就好?

FileOutputStream fos = new FileOutputStream("bbb.txt",true);    //如果没有bbb.txt,会创建出一个
        fos.write(97);  
        fos.write(98);
        fos.write(99);
        fos.close();

看懂了没有,只需要多一个true

6.254 拷贝图片也是这样:

//创建输入流对象,读取,关联致青春.mp3
public static void demo() throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream("狂狼.mp3");    
        //创建输出流对象,写出,关联copy.mp3
        FileOutputStream fos = new FileOutputStream("copy.mp3");
        int b;
        while((b = fis.read()) != -1) {
            fos.write(b);
        }
        fis.close();
        fos.close();
}

6.25 字节流一次读写一个字节复制音频、视频等,效率太低,怎么办?

// 本方法不推荐使用,因为有可能会导致内存溢出
public static void demo() throws FileNotFoundException, IOException {
        //创建输入流对象,关联狂狼.mp3
        FileInputStream fis = new FileInputStream("狂狼.mp3");
        //创建输出流对象,关联copy.mp3
        FileOutputStream fos = new FileOutputStream("copy.mp3");
        //创建与文件一样大小的字节数组
        byte[] arr = new byte[fis.available()];
        //将文件上的字节读取到内存中     
        fis.read(arr);
        //将字节数组中的字节数据写到文件上                                  
        fos.write(arr);                                             
        
        fis.close();
        fos.close();
}

这里用了一个available() 方法,想想也知道这个就是可以查询得知输入流文件大小的方法。

为什么不推荐使用呢,因为如果我们读取例如200MB大小的文件,这个没什么关系,如果是一个20GB的压缩包呢,当我们在创建数组arr的时候,内存根本放不下。至少,我们平常见到的电脑,其运行内存一般也就4GB、8GB,16GB都算高配了。

优化一下,我们可以这样:

public static void demo() throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream("xxx.txt");
        FileOutputStream fos = new FileOutputStream("yyy.txt");
        
        byte[] arr = new byte[2];
        int len;
        while((len = fis.read(arr)) != -1) {
            fos.write(arr,0,len);
        }
        
        fis.close();
        fos.close();
    }

这里大家可能要说了——又变了,你个渣男,说好的一开始就给的完整版,都等着套模板了,你居然还要变……

这里write( )方法多了个变化,意思是:一次读取arr长度的字节内容,偏移量为0个字节,读取len个长度。,从0个字节的位置开始写入arr数组里面的内容,从前头开始数len长度的个数。
看不懂具体了解这个方法请移步java中文开发说明文档
总之,这是为了处理最后剩下需要读取不足arr数组长度的时候,将末尾会自动补足的0给去掉,保证数据的原样性。

当然,我们还有最终优化,因为这样做仅仅比读取1一个字节提高了1倍的效率。

                FileInputStream fis = new FileInputStream("狂狼.mp3");
        FileOutputStream fos = new FileOutputStream("copy.mp3");
        
        byte[] arr = new byte[1024 * 8];
        int len;
        while((len = fis.read(arr)) != -1) {                //如果忘记加arr,返回的就不是读取的字节个数,而是字节的码表值
            fos.write(arr,0,len);
        }
        
        fis.close();
        fos.close();

看到了没有,这里把arr数组变成1024的整数倍,因为计算机的字节倍数就是1024的倍数(比如1KB = 1024B),这样的效率就很高了,当然,你可以定义更好的处理逻辑。

7.0 缓冲字节流

其实就是在 FileInputStream类和 FileOutputStream类外面包了两层皮,不信你去查看源码或者查看继承关系。
BufferedInputStream
BufferedOutputStream

7.1 首先说明的是缓冲流的思想

A:缓冲思想

B:BufferedInputStream

C:BufferedOutputStream

看懂了没有,其实缓冲流其实和上面6.25小点里面的自定义数组是一个尿性。

所以能用缓冲流就用缓冲流,毕竟读取也快,写出也快。

7.2 代码相当简单:
        //创建文件输入流对象,关联狂狼.mp3
        FileInputStream fis = new FileInputStream("狂狼.mp3");
        //创建缓冲区对fis装饰
        BufferedInputStream bis = new BufferedInputStream(fis);
        //创建输出流对象,关联copy.mp3
        FileOutputStream fos = new FileOutputStream("copy.mp3");
        //创建缓冲区对fos装饰
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        int b;
        while((b = bis.read()) != -1) {     
            bos.write(b);
        }
        //只关装饰后的对象即可
        bis.close();
        bos.close();

如果你觉得还是多了两行代码,明明已经变复杂了,其实我们一般写成这样:

        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("狂狼.mp3"));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.mp3"));
        
        int b;
        while((b = bis.read()) != -1) {
            bos.write(b);
        }
        bis.close();
        bos.close();

这充分证明了我不是渣男

这里还有一个疑问,那如果我像6.25小点里面的自定义数组和用带Buffered的读取哪个更快?

7.3 细节来了。

7.31 我们知道,在这些IO流操作里面,可能大家在学习的时候,可能是培训老师,可能是某本书,会告诉你每次记得——最后放进去一个flush( )方法,目的是确保缓冲流的所有数据都会写进去,别丢数据。

但是!其实你不用也没关系……只要你有下面的操作就可以了:

        bis.close();
        bos.close();

flush( )方法

close( )方法

所以,到底什么时候用flush( )方法?

public static void demo() throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream("yyy.txt");
        byte[] arr = new byte[4];
        int len;
        while((len = fis.read(arr)) != -1) {
            System.out.println(new String(arr,0,len));
        }
        
        fis.close();
    }

当然,"yyy.txt"里面现在存了一些中文在里面,自己随便测吧,总之当一次读取4个字节的时候,刚好断开点是一个中文的上半个字节的话,乱码就来了。

7.33 字节流写出中文的问题
那么写会有什么问题么?答案是写出中文不会有算命会造成乱码的问题!写不会有什么问题,写不会有什么问题,重要的话说三遍。就像下面这样:

public static void demo() throws FileNotFoundException, IOException {
        FileOutputStream fos = new FileOutputStream("zzz.txt");
        fos.write("我读书少,你不要骗我".getBytes());
        fos.write("\r\n".getBytes());
        fos.close();
    }

随便测试,不会出错的。

END

上一篇 下一篇

猜你喜欢

热点阅读