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

1.0 为什么要写这个。
  • 在当初学java语言的时候,其实感觉这算是最难的基础部分内容之一,因为字符流和字节流的存在,还有缓冲字节流、字符流,选择太多,不同的限制,导致使用的时候根本不知道:
1. 到底什么情况下怎么写
2. 什么方案和代码,写会没有什么大的问题。
  • 所以在这里写下相关知识点,所谓授人以渔,更要授人以 鳞、 鲤、 鲫、 鲸、 鳍、 鳌、 鳃、 鳜、 鲈、 鲑、 鲧、 鲲、 鲱、 鲰、 鲵、 鲋、 鳟、 鳒、 鲥、 鲝、 鲹、 鲭、 鲉、 鲽、 鳀、 鲐、 鲠、 鳑、 鳛、 鲞、 鲬、 鳇、 鲢、 鲮、 鳐、 鲔……
3.本来一个内容就打算写一篇的,说我写得太长了,不许我发布,所以拆成两篇,查阅本篇的朋友请结合另一篇一同参考,谢谢。

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

2.0 概念
  • 2.1 IO流用来处理设备之间的数据传输
  • 2.2 Java对数据的操作是通过流的方式
  • 2.3 Java用于操作流的类都在IO包中
  • 2.4流按流向分为两种:输入流,输出流。
  • 2.5流按操作类型分为两种
    • 字节流 : 字节流可以操作任何数据,因为在计算机中任何数据都是以字节的形式存储的
    • 字符流 : 字符流只能操作纯字符数据,比较方便。
3.0 IO流常用父类
  • 字节流的抽象父类:
    • InputStream
    • OutputStream
  • 字符流的抽象父类:
    • Reader
    • Writer
4.0 说到这里 ,你肯定觉得烦,因为上面讲地的确都是废话。
  • 字节流读取英文、数字、视频、音频、图片等超级好用的,速度快,这个是它存在的最大用处。

  • 字符流就是涉及中文输入,如果用字节流,就会出现把一个字符,人为地拆开成两个字节,然后就各种乱码。

  • 所以,当你知道自己需要传英文、数字、视频、音频、图片等不涉及中文传输的时候,用字节流比较好,规模小,耗时小,占用资源少,效率高
    当涉及中文传输时,那是不是用字符流才是王道,不出错才是爸爸呢?

    错! 如果在只读或着只写中文的情况下,才建议用用字符流,如果读就是为了写(比如复制,上传存储,发送给对方文件等),那还是用字节流会比较好(后面会解释)。
    再重复一遍,就是除了一种特殊情况,不然还是用字节流处理就行了。(这种情况是只读或着只写

  • 字符流有缺点:不可以拷贝非纯文本的文件!
    比如说,用字符流读取一张图片,或者某个网址,因为在读的时候会将字节转换为字符,在转换过程中,可能找不到对应的字符,就会用?代替,写出的时候会将?字符转换成字节写出去,如果是?字符直接写出,写出之后的文件就乱了,完全看不了。

5.0 IO程序代码只有3步

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

    1. 使用前,导入IO包中的类
  • 2.使用时,进行IO异常处理
  • 3.使用后,释放资源
  • 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类的源代码:

【Java中级】1.0 Java核心之IO流(一)——生猛理解字节流_第1张图片
2019-03-09_011845.png

在eclipse里面按住ctrl键,然后挪动鼠标放到 FileInputStream类上,点击 Open Declaration,这样我们可以查看源代码(查看不了的可以想想办法,百度一下就好了。需要的留言,给教程)。

【Java中级】1.0 Java核心之IO流(一)——生猛理解字节流_第2张图片
2019-03-09_012244.png

可以看到, FileInputStream类继承了 InputStream类,接着查看 InputStream类源代码:
【Java中级】1.0 Java核心之IO流(一)——生猛理解字节流_第3张图片
2019-03-09_012401.png

可以看到, InputStream类实现了一个 Closeable接口,接着查看 Closeable接口源代码:
【Java中级】1.0 Java核心之IO流(一)——生猛理解字节流_第4张图片
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要这么设计。

  • 字节输入流可以操作任意类型的文件,比如图片音频等,这些文件底层都是以二进制形式的存储的,我们这些文件肯定会有结束标记,而且我们可以去简单设计代码测试一下,最后的返回值是-1。
    所以我们的while语句 里面用读到最后会是-1 作为判断条件,表示文件读完了。

如果是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:缓冲思想

  • 字节流一次读写一个数组的速度明显比一次读写一个字节的速度快很多。
  • 这是加入了数组这样的缓冲区效果,java本身在设计的时候,也考虑到了这样的设计思想(装饰设计模式,下面会解释这个设计模式),所以提供了字节缓冲区流

B:BufferedInputStream

  • BufferedInputStream 内置了一个缓冲区(数组)
  • BufferedInputStream 中读取一个字节时
  • BufferedInputStream 会一次性从文件中读取8192个, 存在缓冲区中, 返回给程序一个
  • 程序再次读取时, 就不用找文件了, 直接从缓冲区中获取
  • 直到缓冲区中所有的都被使用过, 才重新从文件中读取8192个

C:BufferedOutputStream

  • BufferedOutputStream 也内置了一个缓冲区(数组)
  • 程序向流中写出字节时, 不会直接写到文件, 先写到缓冲区中
  • 直到缓冲区写满, 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的读取哪个更快?

  • 定义小数组如果是8192个字节大小和Buffered比较的话
  • 定义小数组会略胜一筹,因为读和写操作的是同一个数组
  • 而Buffered操作的是两个数组

7.3 细节来了。

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

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

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

flush( )方法

  • 用来刷新缓冲区的,刷新后可以再次写出

close( )方法

  • 用来关闭流释放资源的的,如果是带缓冲区的流对象的close()方法,不但会关闭流,还会再关闭流之前刷新缓冲区,关闭后不能再写出

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

  •   如果缓冲流还想接着用,比如聊天软件,我发一段话给你,总不能等我凑够8192个字节后,再一波发给对方看。肯定发一次就清空一次缓冲流。
  •   但是如果调用close( )方法,那不好意思,说完这句都没法再说了,因为流已经关闭了。所以往往是这种情况下就要用flush( )方法,发出去后,没关系,想发还可以继续发,想收的,先把收到的及时清空,显示给对方看,再继续收。


    7.32 字节流读取中文的问题
    总有人问,那字节流就搞不定中文的问题了?不,是真搞不定……
    我们知道1个中文占用1个字符,等于2个字节。
    字节流在读中文的时候有可能会读到半个中文,造成乱码,可以用下面的方法测试:
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();
    }

随便测试,不会出错的。

  • 字节流直接操作的字节,所以写出中文必须将字符串转换成字节数组
  • 写出回车换行 write("\r\n".getBytes());
    只是读写中文的话(中间不进行任何操作),虽然读出来会乱码,但是写出来后又会恢复原样,当然,这时候写的时候就不需要用什么getBytes()方法了。

END

你可能感兴趣的:(【Java中级】1.0 Java核心之IO流(一)——生猛理解字节流)