Java基础进阶 IO流之字节流

1、IO流

1.1、概述

之前学习的File类它只能操作文件或文件夹,并不能去操作文件中的数据。真正保存数据的是文件,数据是在文件中。而File类它只是去对文件本身做操作,不能对文件中的数据进行操作。
如果要操作文件中的数据,这时必须使用Java中提供的IO流技术完成。
IO可以实现数据的传输,把数据比作是像水一样在流动。
IO:input/output
IO流:它的功能就是专门操作文件中的数据。

2.2、IO流分类

1)按流向分:
输入流:读取数据,把持久设备的数据读取到内存中。
输出流:写出数据,把内存的数据写出到持久设备。
2)按数据类型分:
计算机中一切数据都是:字节数据。
字符数据:底层还是字节数据,但是可以根据某些规则,把字节变成人们认识的文字、符号等等。
字节流:数据在持久设备上都是以二进制形式保存的。二进制就是字节数据。Java就给出了字节流可以直接操作字节数据。

字节输入流:InputStream
儿子:XxxInputStream
字节输出流:OutputStream
儿子:XxxOutputStream

字符流:读取字符数据。数据在设备上是以二进制形式表示,但是有些二进制合并在一起可以表示一些字符数据。

字符输入流:Reader
儿子:XxxReader

字符输出流:Writer
儿子:XxxWriter

IO流的分类如下图所示:


IO流的分类

说明:
1)字节流可以对任意类型的文件按照字节进行读和写的操作;
例如:图片、视频、文本文件、word文档、mp3等。
2)字符流只能对文本类型的文件进行操作;
问题1:文本类型的文件是什么文件?
只要可以使用记事本打开并看得懂的文件就是文本文件。
例如:.java文件、.txt等文件。
而字符流只能操作文本类型的文件,也就是说如果一个文件可以使用记事本打开并能够看懂,那么这个文件就可以使用字符流来操作,否则其他的文件都得使用字节流进行操作。

注意:字节流也可以操作文本文件。

2 字节流

2.1、字节流介绍

字节流:它是以字节为单位,读写数据的。

读写:
读是从持久设备上给程序读取数据。(硬盘-------》内存)
写是把程序中的数据写到持久设备上。(内存-------》硬盘)
不管是字节流还是字符流,他们都有读和写的操作。
字节流:它分成字节输入流和字节输出流。
字节输入流:从持久设备上把数据读取到程序中。
字节输出流:把程序中的数据写到持久设备上。
所有的数据都可以使用字节流操作。
常见的数据保存形式:记事本、word文档、图片、音频、视频、压缩文件等

2.2、字节输出流(掌握)

之前我们在学习其他类的时候,例如异常、集合都有顶层父类或者顶层接口。那么在字节流中也有顶层父类,规定了字节输入流或字节输出流的基本操作行为。

字节流:
字节输出:Output 字节输出流:OutputStream
字节输入:Input 字节输入流:InputStream


1.png

OutputStream:它是字节输出流的顶层父类。它可以把字节数据写给JVM,JVM在交给操作系统,操作系统把数据写到持久设备上。

OutputStream类中的函数如下所示:


2.png

注意: 学习IO流,我们是在使用Java代码操作Java以外的其他设备。不管操作中是否有问题,最后都要断开Java程序和这些设备之间的连接。

   close方法是关闭Java和其他设备之间的连接。
   write方法是把数据写到Java关联的设备中
   write(byte[] b ) 把这个b字节数组中的所有数据写到关联的设备中(设备包括文件、网络或者其他任何地方)。
   write(byte[] b , int off , int len ) 把b字节中的数据从下标off位置开始往出写,共计写len个
   write(int b ) 把这个b数据写到关联的设备中。 

OutputStream:它是抽象类,不能创建对象,这里我们需要使用OutputStream类的子类FileOutputStream的对象把数据写到文件中。


3.png

构造方法如下所示:


4.png

FileOutputStream(Filefile)
创建一个向指定 File 对象表示的文件中写入数据的文件输出流。

FileOutputStream(String name)
创建一个向具有指定名称的文件中写入数据的输出文件流。

需求1:将一个字节byte数组{97,98,65,66}中的数据写到D:\test1\1.txt 的文件中。

分析:
构造函数:
FileOutputStream(File file)
创建一个向指定 File 对象表示的文件中写入数据的文件输出流。


5.png

FileOutputStream(String name)
创建一个向具有指定名称的文件中写入数据的输出文件流。

步骤:
1)创建一个测试类FileOutputStreamDemo;
2)在这个类中定义一个method_1函数,在这个函数中创建FileOutputStream类的对象out,分别使用上述两种构造函数创建,并在构造函数的参数中指定目录路径D:\test1\1.txt;
3)定义一个字节byte数组b,并存入值;
4)使用字节输出流对象out调用write()函数,将字节数组中的数据写到指定的文件中;
5)使用对象out调用close()函数关闭流资源;

//需求1:将一个字节byte数组中的数据写到D:\\test1\\1.txt 的文件中。
    public static void method_1() throws IOException {
        // 创建字节输出流对象 FileOutputStream(String name) 
        /*
         *  public FileOutputStream(String name) throws FileNotFoundException 
         *  {
                this(name != null ? new File(name) : null, false);
            }
         */
        //经常用 底层帮我们将字符串封装成了File类的对象
        OutputStream out = new FileOutputStream("D:\\test1\\1.txt");
        //FileOutputStream(File file) 
        //创建File类的对象 
    /*  File file=new File("D:\\test1\\1.txt");
        //创建输出流对象  不经常使用
        FileOutputStream out = new FileOutputStream(file);*/
        //创建一个字节数组
        byte[] b={97,98,65,66};
        //将字节数组中的数据写到指定的文件中
        out.write(b);
        //关闭输出流
        out.close();
}

注意:
问题1:构造函数执行的时候,做了什么?
A:判断目标文件所在的目录路径在硬盘上是否存在,如果路径不存在,那么就会报系统找不到指定的路径的异常FileNotFoundException。


6.png

B:如果目标文件所在的路径存在了,也就是说D:\test1,那么接下来就会判断目标文件1.txt在指定的目录中D:\test1中是否存在:
如果不存在,替我们创建1.txt文件出来,并将数据写到1.txt文件中;
如果这个文件1.txt已经存在,这时用新的数据覆盖掉原来文件中的数据;
C:调用系统资源,关联目标文件;

举例:其实我们平常使用记事本或者其他软件打开.txt文件的时候都是在调用系统资源,进行关联目标文件。

问题2:为什么要close?
A:流关闭后,就会释放系统资源;
B:流关闭,就变成了垃圾数据,这样可以被垃圾回收器回收,免得没有用的资源浪费空间;

问题3:关于FileOutputStream输出流的另一个构造函数


7.png

FileOutputStream(File file)如何使用?
其实对于FileOutputStream(String name) 这个构造函数在底层帮我们将字符串封装成了File类的对象,作为程序员我们不用再创建File类的对象了,所以开发中建议使用FileOutputStream(Stringname)这个构造函数。

需求2:使用字节输出流把字符串数据”hello,编程”写到硬盘D:\test1\2.txt上;
1)在上述测试类中在创建一个method_2()函数;
2)在这个函数中创建FileOutputStream类的对象out,并在构造函数的参数中指定目录路径D:\test1\2.txt;
3)定义一个字符串s=”hello,狗哥”;
4)使用字符串对象s调用String类中的函数将字符串转成字节数组;
5)使用字节输出流对象out调用write()函数,将字节数组中的数据写到指定的文件中;
6)使用对象out调用close()函数关闭流资源;

//需求2:使用字节输出流把字符串数据”hello,狗哥”写到硬盘上;
    public static void method_2() throws IOException {
        //创建输出流对象
        OutputStream out = new FileOutputStream("D:\\test1\\2.txt");
        //定义一个字符串
        String s="hello,狗哥";
        /*
         * 使用输出流对象调用write函数将数据写到硬盘上指定的文件中
         * 将字符串s转换为字节数组
         */
//      out.write("hello 编程语言".getBytes());
        out.write(s.getBytes());
        //关闭资源
        out.close();
    }

需求3:演示write(int b) 了解使用谨慎
1)在上述测试类中在创建一个method_3()函数;
2)在这个函数中创建FileOutputStream类的对象out,并在构造函数的参数中指定目录路径D:\test1\3.txt;
3)使用字节输出流对象out调用write()函数,将整数97,353写到指定的文件中;
4)使用对象out调用close()函数关闭流资源;

//演示write(int b)函数
    public static void method_3() throws IOException {
        // 创建输出流对象
        OutputStream out=new FileOutputStream("D:\\test1\\3.txt");
//      OutputStream out1=new FileOutputStream("D:\\test1\\3.txt");
        //使用输出流对象调用write函数写出整数
        /*
         * 字节输出流中的write方法每调用一次,只能写出一个字节数据。
         * 如果指定的数据较大,这个时候它只会把这个数据中最低位上的1个字节数据写到文件中
         * 97  :00000000 00000000 00000000 01100001 
         * 353 :00000000 00000000 00000001 01100001
         */
//      out.write(353);//a
//      out.write(97);//a
        /*
         * public void write(byte[] b,int off,int len)
         * b表示字节数组
         * off表示从下标为off开始
         * len表示写到文件中的字节个数
         */
        byte[] b={97,98,65,66};
        out.write(b, 0, 2);//写出结果是:ab
        //关闭资源
        out.close();
//      out1.write(b);
//      out.close();
//      out.write(97);//a
//      System.out.println(Integer.toBinaryString(353));
}

说明:
1)字节输出流中的write方法每调用一次,只能写出一个字节数据。
如果指定的数据较大,这个时候它只会把这个数据中最低位上的1个字节数据写到文件中

例如:353输出的结果是97.
           0000 0000 0000 0000 0000 0000 0110 0001  97
           0000 0000 0000 0000 0000 0001 0110 0001  353

2)public void write(byte[] b,intoff,int len)
b表示字节数组
off表示从下标为off开始
len表示写到文件中的字节个数
3)如果使用完一个输出流,关闭之后,那么不能继续使用这个输出流向该文件中继续写入数据,否则会报异常。只能重新再创建一个新的输出流,继续向该文件中写数据,这样后写入的数据会覆盖之前书写的数据;

2.3、追加数据和换行

数据追加问题:
通过以上演示我们发现一个问题,就是流一旦关闭,再次写数据,会覆盖文件中原来的数据。流不关闭,一次性写多次,也会追加。

如何解决?
使用FileOutputStream类中的其余构造函数:
FileOutputStream(File file, booleanappend) 创建一个向指定 File 对象表示的文件中写入数据的文件输出流。
FileOutputStream(String name, boolean append) 创建一个向具有指定name 的文件中写入数据的文件输出流。

说明:
A:上述构造函数中如果第二个参数append为true,那么就在已经存在的文件末位追加数据,如果为false,那么就不会文件中已经存在的数据末尾追加数据,而是将原来的数据给覆盖;
B:如果指定的文件在硬盘上不存在,也会创建这个文件;
需求:使用字节输出流把字符串数据”你好吗”写到硬盘上,要求不能覆盖文件中原有的数据;

分析和步骤:
1)使用new关键字调用FileOutputStream类的构造函数创建输出流对象fos;
2)使用对象fos调用write函数向指定的文件添加数据;
3)关闭资源;

//演示向文件末尾追加数据 
    //需求:使用字节输出流把字符串数据”你好吗”写到硬盘上,要求不能覆盖文件中原有的数据;
    public static void method_1() throws IOException {
        //创建输出流对象 
        FileOutputStream fos = new FileOutputStream("D:\\test1\\4.txt", true);
        /*
         * 对于以上构造函数进行说明:
         * 如果第二个参数为true,那么就会在已经存在的文件中的末尾处追加数据,如果这个文件4.txt不存在
         * 那么就会创建这个文件
         * 如果第二个参数为false,那么向文件添加数据的时候就会覆盖原来的数据
         */
        //向文件中添加数据
        fos.write("你好吗".getBytes());
        fos.write("我叫狗哥".getBytes());
        //关闭资源
        fos.close();
    }

数据换行问题:
我们如果想换行,可以在数据的末尾加:\r\n
但是:\r\n是windows系统识别的换行符。不同的操作系统,换行符可能会不相同的。我们的代码扩展性就变差了。

解决方案:
如果我能根据系统来获取对应的换行符,就可以跨平台。如何从系统获取换行符呢?

System类中的方法:
public staticProperties getProperties() 获取系统中所有的属性这个函数的返回值是 Properties 类,这个类实现了Map接口,所以getProperties()这个函数获得系统中所有的属性并以键值对形式存在。在众多系统属性中我们想要的是行分隔符,就是类似\r\n,那么行分隔符的键是line.separator,也就是说通过这个键就可以获得系统对应的行分隔符。

这里还得需要借助一个函数通过以上的键line.separator获得行分隔符的值,这个函数就是getProperty()
static String getProperty(String key) 获取指定键指示的系统属性。
代码如下所示:

/*
     * 换行演示
     * 我们如果想换行可以在添加数据的末尾书写\r\n 就可以实现换行
     * 但是\r\n属于Windows系统中特有的方法,不能在其他系统中使用,也就是说不能
     * 跨平台,这样代码的扩展性就变差了
     * 解决办法:
     *  如果我们能够根据不同的系统获取系统对应的行分隔符,那么这样代码就可以跨平台了,
     * 那么如何获得系统的的属性,行分隔符呢?
     * 通过System类,调用这个类中的函数getProperties()
     */
    public static void method_2() throws IOException {
        //创建输出流对象
        FileOutputStream fos = new FileOutputStream("D:\\test1\\5.txt", true);
        //向文件中写入数据
//      fos.write("hello 上海传智\r\n".getBytes());
        //表示获得系统中所有的属性
        /*Properties properties = System.getProperties();
        System.out.println(properties);*/
        //获得系统中的行分隔符
        String separator = System.getProperty("line.separator");
        //向文件中写出数据
        fos.write(("狗哥真帅哈哈"+separator).getBytes());
        //关闭资源
        fos.close();
    }

2.4、字节输入流(掌握)

之前学习的是输出流对象,是用来从内存中向文件(硬盘)中写入数据。如果想要从文件(硬盘)中向内存中读取数据,需要使用输入流对象:InputStream。

2.4.1、InputSteam介绍

字节输入流:InputStream:


8.png
java.lang.Object
   |----java.io.InputStream:属于抽象类。是IO中所有的字节输入流的父类。
                    该类中定义了所有字节输入流的共性功能 

InputStream类中的共性功能:

9.png

说明:close(): 关闭字节输入流对象。

10.png

说明:
1)read():调用一次read,就可以从关联的文件中读取一个字节数据,并返回这个字节数据。
2)read():方法可以从关联的文件中读取数据。所有read方法如果读取到文件的末尾,都会返回-1。遇到-1就代表文件中的数据已经被读取完毕。
3)read(byte[] b) :调用一次,读取多个字节数据,把读到的字节数据保存在传递的b字节数组中。返回字节数组中读取的字节个数。注意啦:这个返回值不是数组长度。

由于InputStream类是抽象类,不能创建这个类的对象,但是如果想使用这个类中的函数,那必须得创建这个类的对象,如果想要创建对象,那么只能创建InputStream类的子类。

由于我们这里是操作文件的,所以我们需要创建FileInputStream类的对象。

2.4.2、FileInputStream介绍


11.png

2.4.3、每次读单个字节
演示:字节输入流读取数据,一次读一个字节。
构造函数:
FileInputStream(File file)
FileInputStream(String name)
读取功能:int read()
先使用输入流对象,从文件中读取数据,每调用一次read()方法,可以从硬盘文件中读取一个字节数据,把这个字节数据保存在一个int类型的变量中。然后判断读取到的这个数据也就是这个int类型的变量是否是-1,如果不是-1,说明当前没有读取到文件的末尾。如果是-1,说明当前已经读取到文件的末尾。

int类型的变量中就保存着当前读取到的那个字节数据,后续步骤可以对这个数据进行相关处理。

输入流的使用步骤:
A:创建输入流,关联源文件;
B:读取数据;
C:释放资源;

注意:
1)字节输入流,构造函数执行时,如果源文件不存在,那么抛出异常!!;
2)由于输入流读取的是文件中的字节数据,所以要求输入流指定的一定是文件,不能是文件夹,否则会报异常;

分析和步骤:
1)使用new关键字调用FileInputStream类的构造函数创建指定路径D:\test1\1.txt的输入流对象in;
2)使用输入流对象in调用read()函数开始读取文件,返回一个int类型的整数,并输出最后返回值;
3)多次调用read()函数引起代码重复,我们可以考虑使用循环来实现;
4)循环条件是返回值是-1;

/*
 * 演示:字节输入流读取数据,一次读一个字节。
 * 注意:由于输入流读取的是文件中的字节数据,所以要求输入流指定的一定是文件,否则会报异常
 * FileNotFoundException
 * 读取功能:
 * int read():表示读取下一个字节并返回
 * 输入流的使用步骤:
 * 1)创建输入流;
 * 2)读取数据;
 * 3)关闭输入流;
 */
public class FileInputStreamDemo {
    public static void main(String[] args) throws IOException {
        // 创建输入流的对象 java.io.FileNotFoundException: D:\test1 (拒绝访问。) 系统找不到指定的文件
        FileInputStream fis = new FileInputStream("D:\\test1\\1.txt");
        //使用输入流对象调用read()函数一次读一个字节数据
        //第一次读取
        int i = fis.read();//i表示读取的字节数据,如果是-1说明文件的结尾
        //输出读取的字节数据
        System.out.println((char)i);
        //第二次读取
        int i2 = fis.read();
        System.out.println((char)i2);
        //第三次读取
        int i3 = fis.read();
        System.out.println((char)i3);
        //第四次读取
        int i4 = fis.read();
        System.out.println((char)i4);
        //第五次读取
        int i5 = fis.read();
        System.out.println(i5);//-1
        //第六次读取
        int i6 = fis.read();
        System.out.println(i6);//-1
        //关闭资源
        fis.close();
    }
}

通过上述代码发现,在使用输入流对象fis调用read()函数的时候,出现多次调用的情况,这样也会导致代码重复,在开发中尽量简化代码的书写,所以对上述代码还得进一步优化:
终极版代码单个读取数据的代码模板如下所示:

   /*
         * 通过书写代码发现上述代码重复太多,我们可以考虑使用循环来解决上述代码重复性的问题
         * 问题:循环的循环条件是什么?
         * 读取到文件结尾,即-1则结束,所以可以让读取的结果是-1结束读取文件
         */
        //读取数据
        int i = fis.read();
        //循环控制读取文件数据的次数
        while(i!=-1)
        {
            //说明文件中还有数据,可以继续读取,输出读取的数据
            System.out.println((char)i);
            //修改循环条件 可以理解为一个光标,读取一次,i的值改变一次
            i=fis.read();
          }

2.4.4、复制文件方式1练习
需求:每次读1个字节来完成复制练习1:
复制D:\test\1.txt里面的数据到F:\2.txt文件中

分析:
数据源:一个文本文件,这里使用字节流,读取,所以FileInputStream
目的地:一个文本文件,这里使用字节流,写出,所以FileOutputStream

思路:
A:创建一个输入流,关联源文件
B:创建一个输出流,关联目标文件
C:读取数据,读取硬盘中指定文件中的数据内容,直到读取的返回值是-1
D:写出数据,将上述每次读取的数据内容都写入到目标文件F:\2.txt中
E:释放资源

package cn.xuexi.inputstream.demo;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/*
 * 复制D:\\test\\1.txt里面的数据到F:\\2.txt文件中
 * 分析:
 * 数据源:D:\\test\\1.txt,使用字节流,读入,FileInputStream
 * 目的地:F:\\2.txt,使用字节流,写出,FileOutputStream
 * 思路:
 *  1)创建输入流对象,关联数据源文件;
 *  2)创建输出流对象,关联目的地文件;
 *  3)读取数据,读取硬盘中指定文件中的数据,直到读取文件的末尾处;
 *  4)写出数据,将上述每次读取的文件中的内容写到目标文件中;
 *  5)关闭资源;
 */
public class FileInputStreamTest {
    public static void main(String[] args) throws IOException {
        //1)创建输入流对象,关联数据源文件;
        FileInputStream fis = new FileInputStream("D:\\test\\1.txt");
        //2)创建输出流对象,关联目的地文件
        FileOutputStream fos = new FileOutputStream("F:\\2.txt");
        //3)读取数据,读取硬盘中指定文件中的数据,直到读取文件的末尾处;
        int b=0;
        while((b=fis.read())!=-1)
        {
            //说明数据源文件中还有数据,将每次读取的数据写到目的地文件中
            fos.write(b);
        }
        //关闭资源
        fis.close();
        fos.close();
    }
}

2.4.5、每次读字节数组
演示:字节输入流,一次读一个字节数组。
读取功能:
1)int read(byte[] buf) 读取数据到数组:buf中,返回的是读取到的字节的个数len.
定义的字节数组是用来存储从底层文件中读取到的多个字节数据;
2)在把读取的字节个数保存在len中。len中保存的是真正给字节数组中读取的字节个数,如果读取到文件末尾,也会返回-1;
一般这个数组的长度都会定义成1024的整数倍。
使用循环重复的从文件中读取数据,每次最多可以从文件中读取1024个字节数据
需求:在D:\test\1.txt这个目录的1.txt文件中书写几个字符串,如下所示:
hello
world
Java
字样,然后使用字节输入流一次读一个字节数组来读取上述路径中的1.txt文件中的数据,将每次读取的数据输出打印到控制台。

分析和步骤:
1)创建一个输入流对象,和D:\test\1.txt文件进行关联;
2)定义的字节byte数组b,这个字节数组的长度是5,主要是用来存储从底层文件中读取到的多个字节数据;
3)用来记录当前给byte数组中读取的字节个数的变量,int len = 0;
4)先执行fis.read()函数从底层读取数据,然后会把数据保存在我们传递的参数b数组中。返回值定义一个int类型的变量len记录着读取到字节数组中的字节数;
5)输出记录的读取到字节数len和将字节数组转换后的字符串数据,将字节转换为字符串可以使用Arrays.toString(b)或者String类的构造函数;
6)由于字节数组长度是5,所以需要多次读取,按照上述操作多次读取1.txt文件中剩余的数据,将结果输出到控制台上面;

/*
 * 演示:字节输入流,一次读一个字节数组。
 * int read(byte[] b) 表示定义一个byte数组,每次读取的字节都存储到这个数组中
 */
public class FileInputStreamDemo1 {
    public static void main(String[] args) throws IOException {
        // 创建输入流对象,关联源文件
        FileInputStream fis = new FileInputStream("D:\\test\\1.txt");
        //第一次读取
        //定义一个数组保存字节数据
        byte[] b=new byte[5];
        //读取数据 将数据保存到字节数组中
        int len = fis.read(b);
        //输出len
        System.out.println("len="+len);
        //输出字节数组中的内容
//      System.out.println(Arrays.toString(b));
        System.out.println(new String(b));//hello
         //第一次读取结果:
            /* 
              len=5
            hello
            */
          //第二次读取
         len = fis.read(b);
        //输出len
        System.out.println("len="+len);
        //输出字节数组中的内容
        System.out.println(new String(b));
         //第二次读取结果:
        /*
            len=5
            \r\n
            wor
         */

        //第三次读取
        len = fis.read(b);
        //输出len
        System.out.println("len="+len);
        //输出字节数组中的内容
        System.out.println(new String(b));
         //第三次读取结果:
        /*
         *  len=5
            ld\r\n
            J
         */
        //第四次读取
        len = fis.read(b);
        //输出len
        System.out.println("len="+len);
        //输出字节数组中的内容
        System.out.println(new String(b));
         //第四次读取结果:
        /*
         在1.txt文件中,如果最后的数据Java有回车换行,那么会输出如下所示数据:
            len=5
            ava\r\n
         在1.txt文件中,如果最后的数据Java没有回车换行,那么会输出如下所示数据:
             len=3
            ava
            J 
         */
    }
}

说明:
1)通过上述代码发现返回值len表示读取到的字节数,而不是字节数组的长度。如果读取为5个字节数,那么返回5,即,len等于5。如果读取到的字节数是3,那么返回3,即len等于3。如果文件中没有要读取的数据,则返回-1。
2)上述代码中当第四次读取的时候有问题,如果文件中最后一个数据后面没有回车换行,那么应该只打印ava,为什么会打印:
ava
J
呢?
原因如下图所示:


13.png

为了解决上述代码出现的问题,我们更希望看到当我们读取几个字节数据,我们就输出几个字节数据,所以这里我们不能在使用new String(b)构造函数,我们应该使用new String(b,int offset,int length)构造函数,这样做就不会出现上述问题。

说明:new String(b,0,len):
       创建一个字符串对象,把b数组中的数据转成字符串,从0位置开始,共计转len个。
       从0位置开始,因为每次调用输入流的read方法的时候,把数据给byte数组中保存
       这时真正是从byte数组的0位置开始存储读取的每个字节数据。
       len是byte数组中的保存的真正的读取的字节个数。

这样做就可以做到我们读取几个字节数据,我们就输出几个字节数据的目的。

代码如下所示:

public class FileInputStreamDemo1 {
    public static void main(String[] args) throws IOException {
        // 创建输入流对象,关联源文件
        FileInputStream fis = new FileInputStream("D:\\test\\1.txt");
        //第一次读取
        //定义一个数组保存字节数据
        byte[] b=new byte[5];
        //读取数据 将数据保存到字节数组中
        int len = fis.read(b);
//      System.out.println(new String(b,0,len));//hello
        System.out.print(new String(b,0,len));//hello
        //第二次读取
         len = fis.read(b);
        //输出len
//      System.out.println("len="+len);
        //输出字节数组中的内容
//      System.out.println(new String(b,0,len));
        System.out.print(new String(b,0,len));
        //第三次读取
        len = fis.read(b);
        //输出len
//      System.out.println("len="+len);
        //输出字节数组中的内容
//      System.out.println(new String(b,0,len));
        System.out.print(new String(b,0,len));
        //第四次读取
        len = fis.read(b);
        //输出len
//      System.out.println("len="+len);
        //输出字节数组中的内容
//      System.out.println(new String(b,0,len));
        System.out.print(new String(b,0,len));
         //关闭资源
        fis.close();
      }
}

上面的代码重复太多了,考虑用循环。

问题来了:循环的接收条件是什么呢?
结束条件:末尾返回-1
终极版代码如下所示:

//定义一个数组
//      byte[] b=new byte[5];
        //终极版代码模板
        byte[] b=new byte[1024];//数组长度一般是1024的整数倍
        //定义一个变量保存读取字节的个数
        int len=0;
        //fis.read(b)表示读取的数据都存放到byte数组中了,len表示读取字节数
        while((len=fis.read(b))!=-1)//一定要传递参数数组b
        {
            System.out.print(new String(b,0,len));
        }
         //关闭资源
        fis.close();

2.4.6、复制文件方式2练习:
需求:每次读字节数组来完成复制练习2:
复制D:\test\1.txt中的数据到F:\2.txt文件中。

分析和步骤:
1)创建输入流,关联源文件D:\test\1.txt;
2)创建输出流,关联目标文件F:\2.txt;
3)读写数据;
4)关闭资源;

/*
 * 需求:每次读字节数组来完成复制练习2:
    复制D:\\test\\1.txt到F:\\2.txt
 */
public class FileInputStreamTest1 {
    public static void main(String[] args) throws IOException {
        // 创建输入流对象,关联源文件
        FileInputStream fis = new FileInputStream("D:\\test\\1.txt");
        //创建输出流对象,关联目标文件
        FileOutputStream fos = new FileOutputStream("F:\\2.txt");
        //读取数据
        byte[] b=new byte[1024];
        int len=0;
        while((len=fis.read(b))!=-1)
        {
            //写数据
            fos.write(b, 0, len);
        }
        //关闭资源
        fis.close();
        fos.close();
    }
}

注意:
1)使用InputStream类中的read()函数表示每次读单个字节,然后使用OutputStream类中的write(int b)函数把读出来的字节写入到新的文件中;
2)使用InputStream类中的read(byte[] b)函数表示每次读字节数组,然后使用OutputStream类中的write(byte[] b,int off,int len)函数把读出来的字节数组中的数据写入到新的文件中;
上述两种办法虽然都可以实现,但是是有区别的,第二种字节数组效率要高,开发中优先考虑第二种,两种读写文件方式的区别如下图所示:


14.png

复制文件的时候注意:
1、 需要定义中间的变量(容器),让字节输入流和输出流可以联系起来。然后输入流读取数据,输出流把读取到的数据写到文件中。
2、 在读取数据的时候,调用read() 和read( 字节数组 )有严格的区别。
3、 在写出数据的时候,write(字节数组) 和 write( 字节数组,0 ,长度 )有区别。

2.5、复制文件综合练习

需求:
演示复制文件:把D盘下的1.mp3 拷贝到F盘下的1.mp3。

分析和步骤: 每次读1个字节来完成复制
1)创建测试类CopyFile,在这个类中创建两个函数method_1和method_2;
2)在method_1()函数中演示复制文件一次读写一个,在这个函数中创建输入流对象fis和输出流对象fos,分别指定源文件位置D:\1.mp3和目标文件位置F:\1.mp3;
3)获得开始复制的系统时间的毫秒值start;
4)使用while循环控制读文件的次数,使用输入流对象fis调用read()函数一次一次读,使用输出流对象fos调用write()函数一次一次写;
5)获取复制结束的时间end;
6)结束时间end减去开始复制的时间start,关闭输入和输出流;

//演示一次读取一个字节
    public static void method_1() throws IOException {
        //创建输入流对象 关联源文件
        FileInputStream fis = new FileInputStream("D:\\1.mp3");
        //创建输出流对象 关联目标文件
        FileOutputStream fos=new FileOutputStream("F:\\1.mp3");
        //获取开始复制的时间
        long start = System.currentTimeMillis();
        //定义变量保存字节
        int ch=0;
        while((ch=fis.read())!=-1)
        {
            //写数据
            fos.write(ch);
        }
        //获取结束的时间
        long end = System.currentTimeMillis();
        System.out.println("复制时间是:"+(end-start));//复制时间是:38406
        //关闭资源
        fis.close();
        fos.close();
    }

需求:
演示复制文件:把D盘下的1.mp3 拷贝到F盘下。

分析和步骤:每次读字节数组来完成复制
1)步骤和上述步骤大致相同,只是需要定义一个整数变量len来记录当前给byte数组中读取的字节个数的;
2)定义的字节数组buf是用来存储从底层文件中读取到的多个字节数据;
3)使用read(buf)函数读取文件的时候需要传入一个数组参数buf;
4)每读取一次使用输出流对象调用write()函数向文件中写数据;

//演示一次读取一个字节数组
    public static void method_2() throws IOException {
        // 创建输入流对象 关联源文件
        FileInputStream fis = new FileInputStream("D:\\1.mp3");
        //创建输出流对象 关联目标文件
        FileOutputStream fos = new FileOutputStream("F:\\1.mp3");
        //获取开始复制的时间
        long start = System.currentTimeMillis();
        //定义数组  读数据
//      byte[] b=new byte[8192];
        byte[] b=new byte[1024];
        int len=0;
        while((len=fis.read(b))!=-1)
        {
            //写数据
            fos.write(b, 0, len);
        }
        //获取结束复制的时间
        long end = System.currentTimeMillis();
        System.out.println("复制时间是:"+(end-start));//复制时间是:15
        //关闭资源
        fis.close();
        fos.close();
        
    }

2.6、总结输入流读取文件的模版代码(掌握)

1、一次读取一个字节的模版代码
          1)、创建输入流对象
                 FileInputStream  fis = new FileInputStream( “文件” );
注意:
1)FileInputStream类的构造函数的参数一定要是个文件路径,不能是文件夹路径,因为FileInputStream类是用来操作文件的,是读取文件中的内容;
2)在构造函数中指定的文件在硬盘上一定要存在,否则会抛异常;
          2)、定义变量,记录每次读取到的字节数据
                 int b= 0;
          3)、使用循环读取数据
                 while(  ( b = fis.read() ) !=-1  ){
       //处理读取到的数据,数据在b中。
     }
          4)、关闭流对象
                 fis.close(); 
2、一次读取多个字节数据的模版代码(掌握)
          1)、创建输入流对象
                 FileInputStream  fis = new FileInputStream( “文件” ); 
          2)、定义变量,记录读取到的字节个数
                 int len = 0;         
          3)、定义数组保存读取到的多个字节数据
                 byte[] buf = newbyte[1024*n];            
          4)、使用循环读取数据
                     while(  (  len= fis.read( buf ) ) !=-1 ){
             //处理读取到的数据,数据在buf中,buf中共计有效的数据是len个
           }
          5)、关闭流对象
                 fis.close(); 

2.7、IO流中的异常处理模版(了解)

我们使用Java程序,操作的是Java程序以外的其他设备上的数据,都有可能发生异常问题。
我们在书写的Java程序读写其他设备上的数据时,都要考虑异常问题。这些异常一般开发中都要开发者自己处理掉,不能直接往出抛。

说明:
1)对于输入流FileInputStream来说,硬盘上一定要有文件,否则会报找不到文件异常;对于输入流read()函数来说,假设有文件,但是文件没有可读的权限,那么对于输入流对象调用read()函数也会报异常;对于关闭流的函数close()来说,也同样有异常;
2)对于输出流FileOutputStream来说,指定的路径一定是硬盘上的文件不能是文件夹,否则会报异常;对于输入流write()函数来说,假设有文件,但是文件没有可写的权限,那么对于输出流对象调用write()函数也会报异常;对于关闭流的函数close()来说,也同样有异常;
3)流的关闭动作,必须被执行。但是如果在流关闭之前,已经出异常,那么流的关闭动作无法执行。
必须被执行的动作,放到finally中;把关闭动作,放到finally代码块中后,流的定义也必须放到try块的外面,否则看不到;在finally中关闭流的时候要先判断流是否为null,如果不判断那么流对象调用close()函数有可能报空指针异常。

IO流读写数据的模版代码:
分析和步骤:
1)定义一个测试类IOExceptionDemo 在这个类中定义两个函数method_1()和method_2()分别书写读数据和写数据的模版处理代码;
2)在method_1()函数中定义一个FileInputStream 类的对象fis=null;
3)在try-catch-finally代码块中创建FileInputStream 类的对象并指定读数据的文件D:\1.txt;
4)使用一次性读取一个字节数据来完成读数据;
5)在finally代码块中在关闭流之前要先判断流对象是否存在;
6)在method_2()函数中定义一个FileOutputStream 类的对象fos=null;
7)在try-catch-finally代码块中创建FileOutputStream 类的对象并指定读数据的文件D:\2.txt;
8)使用输出流对象fos调用write()函数向指定的文件中写数据;
9)在finally代码块中在关闭流之前要先判断流对象是否存在;

/*
 * 字节流处理异常代码模板
 */
public class IOExceptionDemo {
    public static void main(String[] args) {
        method_2();
    }
    //读数据的处理异常的模板代码
    public static void method_1() {
        FileInputStream fis=null;
        try {
            //创建输入流对象
            //这里的fis对象只能在try的大括号里面使用,不能在其他地方使用
            fis = new FileInputStream("D:\\test\\1.txt");
            //定义数组
            byte[] b=new byte[1024];
            //定义变量
            int len=0;
            while((len=fis.read(b))!=-1)
            {
                System.out.println(new String(b,0,len));
            }
        } catch (IOException e) {
            System.out.println("读数据异常了");
            //后期在写项目的时候,抓到异常了,这里需要写日志文档
        }finally
        {
            if(fis!=null)
            {
                try {
                    //不管程序是否有异常都得需要关闭资源
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //写数据的处理异常的模板代码
    public static void method_2() {
        FileOutputStream fos=null;
        try {
            // 创建输出流对象
            fos = new FileOutputStream("D:\\test\\1.txt");
            fos.write(97);
        } catch (IOException e) {
            System.out.println("写数据异常了");
        }finally
        {
            if(fos!=null)
            {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.8、字节流小节

1、字节输出流:
     A:使用输出流给文件中写数据的时候,如果文件不存在,会创建这个文件,如果文件存在,会覆盖掉文件中原有的数据。
     B:如果不想覆盖掉原来文件中的数据,在创建输出流的时候,可以指定布尔值为true,这样就会在文件的末尾追加数据。
可以使用构造函数FileOutputStream(String name, boolean append)来实现。
    C:write( int b ) 它只能写出这个int数据中最低位的1个字节数据。
     D:换行
    //获得系统中的行分隔符
    Stringseparator = System.getProperty("line.separator");
     //向文件中写出数据
     fos.write(("黑旋风真帅哈哈"+separator).getBytes()); 

2、字节输入流:
   它不会创建文件,同时要求输入流关联的一定是文件,不能是文件夹。
   字节输入流有自己的读取数据的模版代码。
   一次读取一个字节数据,或一次读取多个字节数据。
 3、流的操作:
   不管是输入流还是输出流,在操作完之后都要关闭流。 

2.9、字节流缓冲区介绍(了解)

通过以上演示发现,一次读很多字节比一次读一个字节快。Java的设计者肯定知道这一点。
Java专门提供了一类流,可以实现高效的读取。这种流,就是在内部维护了一个缓冲区数组。
字节流缓冲区:包括字节输入流缓冲区和字节输出流缓冲区。
字节输入流缓冲区:
BufferedInputStream:

15.png

说明:
BufferedInputStream:它就是一个缓冲区,它内部维护一个数组,可以从底层读取多个数据。

16.png

构造方法摘要
BufferedInputStream(InputStream in)
创建一个新的缓冲输入流,用来将数据写入指定的底层输入流。

问题:为什么缓冲区流需要接收一个普通字节流呢?
缓冲区流是为了 高效而设计的,缓冲区流本身仅仅是维护了一个数组。不具备读和写的功能。真正的读写还是要依赖普通的字节流。

缓冲区字节流和以前字节流读写文件的做法区别如下图所示:


17.png

说明:
1)使用缓冲区输入流的时候,后期我们需要数据的时候,不直接和文件进行交互,而是向缓冲区索要数据;
2)缓冲区只能临时存储数据,它不能从底层读取数据,从底层读取数据还得需要输入流;
3)使用缓冲区的目的只是为了提高读写的效率,先使用FileInputStream输入流将硬盘上的文件读取到缓冲区中,然后在从缓冲区中的数组中取出数据存放到我们之前定义好的数组中,因为两个数组都是在内存中,这样交互数据会更快一点;
4)而我们之前都是从硬盘上直接将数据读取到定义好的数组中,这样效率会低点;

分析和步骤:
1)定义一个测试类BufferedInputStreamDemo;
2)在这个类中的main函数中创建一个可以直接和文件交互的输入流对象in,并指定路径D:\test\123.txt;
3)创建一个缓冲区对象bufIn,需要指定可以从底层读取数据的流对象;
4)创建一个byte类型的数组buf,大小是1024,定义一个整数变量len=0;
5)使用缓冲区对象bufIn调用read()函数读取数据并存储到定义好的数组buf中,并转换为字符串,输出;
6)关闭流;

/*
 * 演示字节输入流缓冲区
 */
public class BufferedInputStreamDemo {
    public static void main(String[] args) throws IOException {
        //创建一个可以直接和文件进行交互的输入流对象
        FileInputStream fis = new FileInputStream("D:\\test\\123.txt");
        //创建输入流缓冲区,指定可以从底层读取数据的流对象
        BufferedInputStream bufin = new BufferedInputStream(fis);
        //创建数组
        byte[] b=new byte[1024];
        //定义一个变量
        int len=0;
        while((len=bufin.read(b))!=-1)
        {
            System.out.println(new String(b,0,len));
        }
        //关闭资源
        bufin.close();
    }
}

说明:上述代码关闭缓冲区流就可以了,不用手动关闭字节流了,因为在底层已经关闭字节流了。

字节输出流缓冲区:
BufferedOutputStream:


18.png

BufferedOutputStream:它可以把需要写出的数据,写到自己的缓冲区中,当缓冲区写满,或者我们手动调用flush方法,或者最后我们关流,才会把缓冲区中的数据一次性的写到底层文件中。

构造方法摘要:
BufferedOutputStream(OutputStreamout)
创建一个新的缓冲输出流,以将数据写入指定的底层输出流。

问题:为什么缓冲区流需要接收一个普通字节流呢?
缓冲区流是为了 高效而设计的,缓冲区流本身仅仅是维护了一个数组。不具备读和写的功能。真正的读写还是要依赖普通的字节流。

分析和步骤:
1)定义一个测试类BufferedOutputStreamDemo;
2)在这个类中的main函数中创建一个可以直接和文件交互的输出流对象fos,并指定路径D:\out.txt;
3)创建一个缓冲区输出流对象bufOut;
4)使用缓冲区输出流对象bufOut调用write()函数向指定文件中写入指定的内容;
5)使用缓冲区输出流对象bufOut调用close()函数关闭缓冲区输出流,这样就可以将指定的数据写入到指定的文件中;

/*
 * 演示字节输出流缓冲区
 */
public class BufferedOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        //创建可以和底层文件交互的流对象
        FileOutputStream fos = new FileOutputStream("D:\\test\\out.txt");
        //创建输出流缓冲区对象
        BufferedOutputStream buffOut = new BufferedOutputStream(fos);
        //写数据
        buffOut.write("hello 狗哥哈哈".getBytes());
        //关闭缓冲区输出流
        //buffOut.close();
         //将缓冲区中的数据刷新到输出流中
         buffOut.flush();
    }
}

注意:
flush:刷新缓冲区,把缓冲区中的有效数据刷到底层文件中,刷新完成之后,缓冲区流还可以继续使用。
close:关闭流和底层文件之间的关联,一旦流关闭了,就不能在使用

小结:
字节流缓冲区对象属于IO流中的高效流,可以提高文件的读写效率。

缓冲区对象读写的原理:
BufferedInputStream对象:该流需要使用输入流对象读取字节数据,把读取到字节数据暂时存储在缓冲区对象内部的数组(缓冲区)中,当内部的缓冲区数组存满了后或不需要再读取数据了,就可以从内部的缓冲区数组中获取字节数据。
BufferedOutputStream对象:该流需要使用输出流对象写入字节数据,把要写入的数据转换为字节数据后暂时存储到缓冲区数组中,当内部缓冲区数组存满后或者关闭流资源或者刷新缓冲区时就可以从缓冲区数组取出字节数据写入到文件中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容