IO流(操作文件内容): 字节流

IO简单概述

  File对象可以表示存在的文件或文件夹,也可以表示不存在的。我们想要得到文件的内容怎么办? 
  File只是操作文件,文件的内容如何处理就需要使用io流技术了
  例如在D盘下有一个名称为a.txt的文本文件. 想要通过Java程序读出来文件中的内容, 需要使用IO流技术. 同样想要将程序中的数据,保存到硬盘的文件中,也需要IO流技术

IO解决问题 : 解决设备与设备之间的数据传输问题(硬盘 -> 内存 内存 -> 硬盘)

  • 读和写文件文件示例
public class IODemo {
    public static void main(String[] args) throws FileNotFoundException, IOException {
        writFileTest();
        readFileTest();
    }

    private static void writFileTest() throws FileNotFoundException,
            IOException {
        // 创建文件对象
        File file = new File("c:\\a.txt");
        // 创建文件输出流
        FileOutputStream fos = new FileOutputStream(file);
        fos.write('g');
        fos.write('z');
        fos.write('i');
        fos.write('t');
        fos.write('c');
        fos.write('a');
        fos.write('s');
        fos.write('t');
        fos.close();
    }

    private static void readFileTest() throws FileNotFoundException,
            IOException {
        // 创建文件对象
        File file = new File("c:\\a.txt");
        // 创建文件输入流
        FileInputStream fis = new FileInputStream(file);
        // 有对多长,就读多少字节。
        for (int i = 0; i < file.length(); i++) {
            System.out.print((char) fis.read());
        }
        fis.close();
               // 当完成流的读写时,应该通过调用close方法来关闭它,
               // 这个方法会释放掉十分有限的操作系统资源.
               // 如果一个应用程序打开了过多的流而没有关闭它们,那么系统资源将被耗尽
    }
}
  • IO流简介 : (Input / Output)
    • I/O类库中使用“流”这个抽象概念; “流”屏蔽了实际的I/O设备中处理数据的细节。IO流用来处理设备之间的数据传输(设备是指硬盘、内存、键盘录入、网络等) Java用于操作流的对象都在IO包中。IO流技术主要用来处理设备之间的数据传输
    • 由于Java用于操作流的对象都在IO包中。所以使用IO流需要导包 import java.io.*
    • IO流的分类 :
      • 流按操作数据类型的不同分为两种:字节流与字符流
      • 流按流向分为:输入流,输出流(以程序为参照物,输入到程序,或是从程序输出)

字节流

  • 什么是字节流 ?
    计算机中都是二进制数据,一个字节是8个2进制位.字节可以表示所有的数据,比如文本,音频,视频.图片,都是作为字节存在的.也就是说字节流处理的数据非常多。
    在文本文件中存储的数据是以我们能读懂的方式表示的。而在二进制文件中存储的数据是用二进制形式表示的。
    我们是读不懂二进制文件的,因为二进制文件是为了让程序来读取而设计的。
    例如: Java的源程序(.java源文件)存储在文本文件中, 可以使用文本编辑器阅读, 但是Java的类(字节码文件)存储在二进制文件中, 可以被Java虚拟机阅读.
    二进制文件的优势在于它的处理效率比文本文件高
    我们已经知道File对象封装的是文件或者路径属性,但是不包含向(从)文件读(写)数据的方法。
    为了实现对文件的读和写操作需要学会正确的使用Java的IO创建对象。
    • 字节流的抽象基类:
      • 输入流 : java.io.InputStream
      • 输出流 : java.io.OutputStream
    • 特点 : 字节流的抽象基类派生出来的子类名称都是以其父类名作为子类名的后缀(如 : FileInputStream, ByteArrayInputStream等)
    • 说明 : 字节流处理的单元是一个字节,用于操作二进制文件(计算机中所有文件都是二进制文件)
输入字节流
  • 输入字节流: InputStream 所有输入字节流的基类(抽象类)
    • FileInputStream 读取文件的输入字节流。
    • BufferedInputStream 缓冲输入字节流。 该类内部其实就是维护了一个8kb字节数组而已。 该类出现的目的是为了提高读取文件数据的效率。
  • 输入流读取方式1 : read()方法, 一次读取一个字节,读到文件末尾返回-1; 仔细查看api文档发现read方法如果读到文件的末尾会返回-1。那么就可以通过read方法的返回值是否是-1来控制我们的循环读取
// 根据read方法返回值的特性,如果独到文件的末尾返回-1,如果不为-1就继续向下读
private static void showContent(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        int len = fis.read();
        while (len != -1) {
            System.out.print((char)len);
            len = fis.read();

        }
        // 使用完关闭流
        fis.close();
    }
  • 输入流读取方式2 : 使用 read(byte[] b)使用缓冲区(关键是缓冲区大小的确定)使用read方法的时候,流需要读一次就处理一次,可以将读到的数据装入到字节数组中,一次性的操作数组,可以提高效率
// 使用字节数组存储读到的数据
    private static void showContent2(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        // 通过流读取内容
        byte[] byt = new byte[5];
        int len = fis.read(byt);
        for (int i = 0; i < byt.length; i++) {
            System.out.print((char) byt[i]);
        }

        // 使用完关闭流
        fis.close();
    }
  • 输入流读取方式3 : 使用read(byte[] b,int off,int len), b显然是一个byte类型数组,当做容器来使用; off,是指定从数组的什么位置开始存字节; len,希望读多少个, 其实就是把数组的一部分当做流的容器来使用。告诉容器,从什么地方开始装要装多少
/**
     * 把数组的一部分当做流的容器来使用
     * read(byte[] b,int off,int len)
     */
    private static void showContent3(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        // 通过流读取内容
        byte[] byt = new byte[1024];
        // 从什么地方开始存读到的数据
        int start = 5;
        
        // 希望最多读多少个(如果是流的末尾,流中没有足够数据)
        int maxLen = 6;

        // 实际存放了多少个
        int len = fis.read(byt, start, maxLen);

        for (int i = start; i < start + maxLen; i++) {
            System.out.print((char) byt[i]);
        }

        // 使用完关闭流
        fis.close();
    }
  • 输入流读取方式4 : 使用缓冲(提高效率),并循环读取(读完所有内容)总结 : 读完文件的所有内容。很显然可以使用普通的read方法,一次读一个字节直到读到文件末尾。为了提高效率可以使用read(byte[] byt);方法就是所谓的使用缓冲提高效率。我们可以读取大文本数据测试(大于1K的文本文件)
/**
     * 使用字节数组当缓冲
     * */
    private static void showContent5(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        byte[] byt = new byte[1024];
        int len = fis.read(byt);
        System.out.println(len);
        String buffer = new String(byt, 0, len);
        System.out.print(buffer);
    }

注意:如何将字节数组转成字符串? 可以通过创建字符串对象即可。
发现:一旦数据超过1024个字节,数组就存储不下。如何将文件的剩余内容读完?我们可以通过通过循环保证文件读取完

    /**
     * 使用字节数组当缓冲
     * */
    private static void showContent7(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        byte[] byt = new byte[1024];
        int len = 0;
        while ((len = fis.read(byt)) != -1) {
            System.out.println(new String(byt, 0, len));
        }
    }
输出字节流
  • 输出字节流: OutputStream 所有输出字节流的基类(抽象类)

    • FileOutputStream : 向文件输出数据的输出字节流
    • BufferedOutputStream : 缓冲输出字节流. 该类出现的目的是为了提高向文件写数据的效率. 该类内部其实也是维护了一个8kb的字节数组而已
  • 输出流写出方式1 : 使用write(int b)方法,一次写出一个字节. 在C盘下创建a.txt文本文件

public class IoTest2 {
    public static void main(String[] args) throws IOException {
        String path = "c:\\a.txt";
        writeTxtFile(path);
    }

    private static void writeTxtFile(String path) throws IOException {
        // 1:打开文件输出流,流的目的地是指定的文件
        FileOutputStream fos = new FileOutputStream(path);

        // 2:通过流向文件写数据
        fos.write('j');
        fos.write('a');
        fos.write('v');
        fos.write('a');
        // 3:用完流后关闭流
        fos.close();

    }
}
  • 当c盘下的a.txt不存在会怎么样?

    • 测试:将c盘下的a.txt文件删除,发现当文件不存在时,会自动创建一个,但是创建不了多级目录。
    • 注意:使用write(int b)方法,虽然接收的是int类型参数,但是write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略
  • 输出流写出方式2 : 使用write(byte[] b),就是使用缓冲.提高效率

public class IoTest2 {
    public static void main(String[] args) throws IOException {
        String path = "c:\\a.txt";
        writeTxtFile(path);
    }

    private static void writeTxtFile(String path) throws IOException {
        // 1:打开文件输出流,流的目的地是指定的文件
        FileOutputStream fos = new FileOutputStream(path);

        // 2:通过流向文件写数据
        byte[] byt = "java".getBytes();
        fos.write(byt);
        // 3:用完流后关闭流
        fos.close();
    }
}
  仔细查看a.txt文本文件发现上述程序每运行一次,老的内容就会被覆盖掉。     
  那么如何不覆盖已有信息,能够往a.txt里追加信息呢
  查看API文档, 发现FileOutputStream类中的构造方法中有一个构造可以实现追加的功能 FileOutputStream(File file, boolean append) 第二个参数append为 true,则将字节写入文件末尾处,而不是写入文件开始处
private static void writeTxtFile(String path) throws IOException {
        // 1:打开文件输出流,流的目的地是指定的文件
        FileOutputStream fos = new FileOutputStream(path,true);

        // 2:通过流向文件写数据
        byte[] byt = "java".getBytes();
        fos.write(byt);
        // 3:用完流后关闭流
        fos.close();
    }
字节流文件拷贝
  • 引入:
    • 字节输入输出流综合使用 : 通过字节输出流向文件中写入一些信息,并使用字节输入流把文件中的信息显示到控制台上
public class IoTest3 {
    public static void main(String[] args) throws IOException {
        String path = "c:\\b.txt";
        String content = "hello java";

        writeFile(path, content);

        readFile(path);
    }

    public static void writeFile(String path, String content)
            throws IOException {
        // 打开文件输出流
        FileOutputStream fos = new FileOutputStream(path);
        byte[] buffer = content.getBytes();
        // 向文件中写入内容
        fos.write(buffer);
        // 关闭流
        fos.close();

    }

    public static void readFile(String path) throws IOException {
        FileInputStream fis = new FileInputStream(path);
        byte[] byt = new byte[1024];
        int len = 0;
        while ((len = fis.read(byt)) != -1) {
            System.out.println(new String(byt, 0, len));
        }
        // 关闭流
        fos.close();

    }
}
  • 注意输出流的细节 : 这个输出流显然只适合小数据的写入,如果有大数据想要写入,我们的byte数组,该如何定义?上述案例中我们将输入流和输出流进行和综合使用,如果尝试进输出流换成文本文件就可以实现文件的拷贝了.

什么是文件拷贝?很显然,先开一个输入流,将文件加载到流中,再开一个输出流,将流中数据写到文件中。就实现了文件的拷贝

  • 字节流拷贝文件实现
    1. 读一个字节写一个字节read 和write
public class IoTest3 {

    public static void main(String[] args) throws IOException {

        String srcPath = "c:\\a.txt";
        String destPath = "d:\\a.txt";
        copyFile(srcPath, destPath);
    }

    public static void copyFile(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 读取和写入信息
        int len = 0;
        while ((len = fis.read()) != -1) {
            fos.write(len);
        }

        // 关闭流
        fis.close();
        fos.close();
    }
}
`` 拷贝音频, 视频文件 ``
public class IoTest3 {

    public static void main(String[] args) throws IOException {

        String srcPath = "c:\\秋.jpg";
        String destPath = "d:\\秋.jpg";
        copyFile(srcPath, destPath);
    }

    public static void copyFile(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 读取和写入信息
        int len = 0;
        while ((len = fis.read()) != -1) {
            fos.write(len);
        }

        // 关闭流
        fis.close();
        fos.close();
    }
}
  1. 使用字节数组作为缓冲区
public static void copyFile2(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 读取和写入信息
        int len = 0;

        // 使用字节数组,当做缓冲区
        byte[] byt = new byte[1024];
        while ((len = fis.read(byt)) != -1) {
            fos.write(byt);
        }

        // 关闭流
        fis.close();
        fos.close();
    }
- 问题: 使用缓冲(字节数组)拷贝数据, 为什么拷贝后的文件大于源文件?
- 测试该方法, 打开文件发现拷贝后的文件和拷贝前的源文件不同, 拷贝后的文件要比源文件多一些内容问题就在于我们使用的容器, 这个容器我们是重复使用的, 新的数据会覆盖掉老的数据, 显然最后一次读文件的时候,容器并没有装满, 出现了新老数据并存的情况. 所以最后一次把容器中数据写入到文件中就出现了问题
- 如何避免?使用 `` FileOutputStream 的write(byte[] b, int off, int len) `` : b 是容器,off是从数组的什么位置开始,len是获取的个数,容器用了多少就写出多少
public static void copyFile2(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 读取和写入信息
        int len = 0;

        // 使用字节数组,当做缓冲区
        byte[] byt = new byte[1024];
        while ((len = fis.read(byt)) != -1) {
            fos.write(byt, 0, len);
        }

        // 关闭流
        fis.close();
        fos.close();
    }
- 使用缓冲拷贝视频,可以根据拷贝的需求调整数组的大小,一般是1024的整数倍。发现使用缓冲后效率大大提高
字节流的异常处理

当我们读和写打开流, 关闭流的时候都会出现异常, 异常出现后, 后面的代码就不会执行. 假设打开和关闭流出现了异常, 那么close方法就不会再执行

public class IoTest4 {
    public static void main(String[] args) throws IOException,
            InterruptedException {
        String path = "c:\\b.txt";
        readFile(path);
    }

    private static void readFile(String path) throws IOException,
            InterruptedException {
        FileInputStream fis = new FileInputStream(path);
        byte[] byt = new byte[1024];
        int len = fis.read(byt);
        System.out.println(new String(byt, 0, len));
        // 让程序睡眠,无法执行到close方法。
        Thread.sleep(1000 * 10);
        fis.close();
    }
}
在执行该程序的同时我们尝试去删除b.txt文件。如果在该程序没有睡醒的话,我们是无法删除b.txt 文件的
因为b.txt还被该程序占用着,这是很严重的问题,所以一定要关闭流。
目前我们是抛出处理,一旦出现了异常,close就没有执行,也就没有释放资源
那么为了保证close的执行该如何处理呢? 那么就需要使用try{} catch(){}finally{}语句
try中放入可能出现异常的语句,catch是捕获异常对象,fianlly是一定要执行的代码
public class IoTest4 {
    public static void main(String[] args) throws IOException,
            InterruptedException {
        String path = "c:\\b.txt";
        readFile(path);
    }

    private static void readFile(String path) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(path);
            byte[] byt = new byte[1024];
            int len = fis.read(byt);
            System.out.println(new String(byt, 0, len));
        } catch (IOException e) {
            // 抛出运行时异常
            throw new RuntimeException(e);
        } finally {
            // 把close方法放入finally中保证一定会执行
            // 先判断是否空指针
            if (fis != null) {
                try {
                    fis.close();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
  • 文件拷贝的异常处理
public static void copyFile(String srcPath, String destPath) {

        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(srcPath);
            fos = new FileOutputStream(destPath);

            byte[] byt = new byte[1024 * 1024];
            int len = 0;
            while ((len = fis.read(byt)) != -1) {

                fos.write(byt, 0, len);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

    }
  • 注意:在最后的close代码中可能会有问题,两个close,如果第一个close方法出现了异常,并抛出了运行时异常,那么程序还是停止了。下面的close方法就没有执行到。那么为了保证close的执行,将第二个放到fianlly中即可
public static void copyFile(String srcPath, String destPath) {

        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(srcPath);
            fos = new FileOutputStream(destPath);

            byte[] byt = new byte[1024 * 1024];
            int len = 0;
            while ((len = fis.read(byt)) != -1) {

                fos.write(byt, 0, len);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {

            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }

            }
        }

    }
字节缓冲流
  • Java其实提供了专门的字节流缓冲来提高效率 : BufferedInputStreamBufferedOutputStream
  • BufferedInputStreamBufferedOutputStream可以通过减少读写次数来提高输入和输出的速度
    它们内部有一个缓冲区, 用来提高处理效率
    查看API文档,发现可以指定缓冲区的大小
    其实内部也是封装了字节数组。没有指定缓冲区大小,默认的字节是8192
    显然缓冲区输入流和缓冲区输出流要配合使用
    首先缓冲区输入流会将读取到的数据读入缓冲区,当缓冲区满时调用flush方法,缓冲输出流会将数据写出
  • 注意:当然使用缓冲流来进行提高效率时,对于小文件可能看不到性能的提升。但是文件稍微大一些的话,就可以看到实质的性能提升了
public class IoTest5 {
    public static void main(String[] args) throws IOException {
        String srcPath = "c:\\a.mp3";
        String destPath = "d:\\copy.mp3";
        copyFile(srcPath, destPath);
    }

    public static void copyFile(String srcPath, String destPath)
            throws IOException {
        // 打开输入流,输出流
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);

        // 使用缓冲流
        BufferedInputStream bis = new BufferedInputStream(fis);
        BufferedOutputStream bos = new BufferedOutputStream(fos);

        // 读取和写入信息
        int len = 0;

        while ((len = bis.read()) != -1) {
            bos.write(len);
        }

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

什么情况使用字节流 : 读取到数据不需要经过编码或者解码的情况情况下这时候使用字节流(比如 : 图片数据)

推荐阅读更多精彩内容