Java 有关 IO 流的使用总结


Java 常用的 IO 流主要有 3 种:字节流、字符流、转换流。其中对于字节流和字符流,其内部又分为低级流和高级流。

对于 Java 常用的 IO 流来说,我们重点掌握高级流就可以了,低级流在实际工作中很少用到。低级流的绝大部分用途,就是实例化后作为参数传递给高级流,高级流封装了低级流之后,提供了很多实用的方法,不但简化了我们对 IO 流的编码操作,也极大的提高的读写效率,所以我们在实际工作中,尽量使用高级流。

下面我们就一起演示一下这 3 种 IO 流(字节流、字符流、转换流)的使用方式吧。


一、字节流

字节流有两个抽象基类:

  • InputStream:这个抽象类是字节输入流的父类
  • OutputStream:这个抽象类是字节输出流的父类

在介绍字节流之前,我们首先要明确一点:字节流操作类是以内存为主角的,具体细节如下:
字节流的 Input (输入、读操作) :是指数据输入到内存,或者内存读取数据。
字节流的 Output (输出、写操作):是指内存输出数据,或者数据从内存写入到硬盘文件等。

理解了 Input 和 Output 的概念后,下面介绍字节流的低级流和高级流的使用,就比较容易理解了。

字节流的低级流:FileInputStream 和 FileOutputStream
字节流的高级流(字节缓冲流):BufferedInputStream 和 BufferedOutputStream

下面我们使用代码,以复制文件为例,演示一下这两种字节流的使用方法:

import java.io.*;

public class ByteIOTest {
    public static void main(String[] args) {
        //低级流逐个字节复制
        long start1 = System.currentTimeMillis();
        try (FileInputStream fis = new FileInputStream("D:\\天蓬风云录.avi");
             FileOutputStream fos = new FileOutputStream("D:\\宫廷内斗史01.avi")) {
            int b;
            while ((b = fis.read()) != -1) {
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end1 = System.currentTimeMillis();
        System.out.println("低级流【逐个字节】复制文件耗时:" + (end1 - start1) + "毫秒");

        //----------------------------------------

        //低级流采用数组复制
        long start2 = System.currentTimeMillis();
        try (FileInputStream fis = new FileInputStream("D:\\天蓬风云录.avi");
             FileOutputStream fos = new FileOutputStream("D:\\宫廷内斗史02.avi")) {
            byte[] bArr = new byte[1024];
            int len;
            while ((len = fis.read(bArr)) != -1) {
                fos.write(bArr, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end2 = System.currentTimeMillis();
        System.out.println("低级流【采用数组】复制文件耗时:" + (end2 - start2) + "毫秒");

        //----------------------------------------

        //高级流逐个字节复制
        long start3 = System.currentTimeMillis();
        //低级字节流对象,作为高级字节流实例化的参数
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("D:\\天蓬风云录.avi"));
             BufferedOutputStream bos = new BufferedOutputStream(
                     new FileOutputStream("D:\\宫廷内斗史03.avi"))) {
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end3 = System.currentTimeMillis();
        System.out.println("高级流【逐个字节】复制时间:" + (end3 - start3) + "毫秒");

        //----------------------------------------

        //高级流采用数组复制
        long start4 = System.currentTimeMillis();
        //低级字节流对象,作为高级字节流实例化的参数
        try (BufferedInputStream bis = new BufferedInputStream(
                new FileInputStream("D:\\天蓬风云录.avi"));
             BufferedOutputStream bos = new BufferedOutputStream(
                     new FileOutputStream("D:\\宫廷内斗史04.avi"))) {
            byte[] bArr = new byte[1024];
            int len;
            while ((len = bis.read(bArr)) != -1) {
                bos.write(bArr, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        long end4 = System.currentTimeMillis();
        System.out.println("高级流【采用数组】复制时间:" + (end4 - start4) + "毫秒");
    }
}

/*
打印出的结果如下所示:
低级流【逐个字节】复制文件耗时:13394毫秒
低级流【采用数组】复制文件耗时:29毫秒
高级流【逐个字节】复制时间:12毫秒
高级流【采用数组】复制时间:3毫秒
*/

上面要复制的文件(天蓬风云录.avi)总大小为 1.13M,从代码的执行结果就能够很轻松的对比出执行效率:
【低级字节流】即使采用数组进行字节缓冲,其性能效率也比【高级字节流】逐个字节复制要慢一倍以上。

其实高级字节流(字节缓冲流)即使采用逐个字节复制,其底层也是采用数组进行缓冲处理的。在不考虑底层实现的情况下,我们在使用高级流复制文件时,继续使用数组缓冲字节,其执行效率又能实现极大的提高(从上面的执行结果就很容易发现)。

在 JDK8 (包含 JDK8) 以后,代码中使用 try 的小括号包裹 IO 流对象,可以不用考虑 IO 流的关闭,代码执行出 try 的代码块后,IO 流会自动释放关闭(即使 IO 流对象在执行过程中出现异常,也会自动关闭 IO 流),这个特性比较类似于 C# 语言中的 Using 关键字,使用起来比较方便。


二、字符流

字符流主要用于读写字符,因此这就涉及到字符集,中文版 Windows 操作系统的默认字符集是 GBK,除此之外我们常见的字符集有 ASCII、GB2312,Unicode 等字符集,其中最常用的就是 Unicode 中的 UTF-8 编码。字符流相比字节流而言,比较容易理解,使用很频繁,而且非常容易。

字符流有两个抽象类:

  • Reader:这个抽象类是读取字符流的父类
  • Writer:这个抽象类写入字符流的父类

字符流的两个低级流为:FileReader 和 FileWriter
字符流的两个高级流(字符缓冲流)为:BufferedReader 和 BufferedWriter

还是那句话,低级流一般很少用,主要用途就是作为高级流的实例化参数,我们经常使用的是高级流。字符流的低级流,在实际测试中,其执行效率同样也是远远低于字符高级流(字符缓冲流),这里就进行代码演示了。这里只演示 BufferedReader 和 BufferedWriter 这两个类的使用。

BufferedReader 有一个非常常用的特有的方法:

方法名 说明
String readLine() 读去文件中的一行文字。 不包括任何行终止字符,如果已经到达流的结尾,则返回 null

BufferedWriter 有一个非常常用的特有的方法:

方法名 说明
void newLine() 写一个换行符,换行符会根据操作系统而定。windows 是 \r\n,linux 是 \r ,mac 是 \n

代码示例如下:

import java.io.*;

//将一个文本文件的内容,写到另一个文本文件中
public class CharIOTest {
    public static void main(String[] args) {
        try (BufferedReader br = new BufferedReader(new FileReader("d:\\天蓬风云录.txt"));
             BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\天蓬宫廷历险记.txt"))) {

            String line;
            //一次读取一行,直到文件末尾为止
            while((line = br.readLine()) != null){
                bw.write(line);
                bw.newLine(); //写一个换行符
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

另外 BufferedReader 也可以代替 Scanner 等待用户在控制台中输入一行字符串,建议使用 BufferedReader ,毕竟是 BufferedReader 是比较高级的类,执行效率比 Scanner 高,功能比 Scanner 要强大一些。具体细节这里就不演示了,这里这列出两个代码实现方式:
import java.io.*;
import java.util.Scanner;

public class CharIOTest {
    public static void main(String[] args) {
        //使用 Scanner 实现用户输入与控制台的交互
        try (Scanner sc = new Scanner(System.in);
             BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\人员信息01.txt"))) {
            System.out.println("请录入姓名:");
            String username = sc.next();
            System.out.println("请录入年龄:");
            String userage = sc.next();

            //将【姓名】和【年龄】写入文件中
            bw.write(username);
            bw.newLine();
            bw.write(userage);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //----------------------------------------

        //使用 BufferedReader 实现用户输入与控制台的交互
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
             BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\人员信息02.txt"))) {
            System.out.println("请输入你的姓名:");
            String name = br.readLine();
            System.out.println("请输入你的年龄:");
            String age = br.readLine();

            //将【姓名】和【年龄】写入文件中
            bw.write(name);
            bw.newLine();
            bw.write(age);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、字符转换流

字符转换流的作用就是:

  • 在读取字符文件时,可以按照指定的字符集编码进行读取,具体的类是:InputStreamReader
  • 在写入字符文件时,可以按照指定的字符集编码进行写入,具体的类是:OutputStreamWriter

我们在上面的所有代码示例中,使用的都是 IDEA 工具,从 IDEA 工具的右下角可以发现,默认使用的是 UTF-8 编码:

如上图所示:如果你新建一个 aaa.txt 文件,用记事本打开,录入一些中文字符,保存的时候使用 ANSI 编码保存,ANSI 编码是 Windows 系统的默认编码,对于中文的 Windows 操作系统,其默认编码是 GBK ,因此采用 ANSI 编码保存,也就是采用 GBK 保存所录入的中文字符。

然后运行上图中的代码,生成一个 bbb.txt 文件,打开 bbb.txt 就会发现是乱码(无论你使用记事本代开,还是用 Notepad++ 等其它第三方软件打开,无论你怎么调整编码,显示的都是乱码),导致 bbb.txt 是乱码的原因,就是 bbb.txt 是 GBK 编码格式保存的,Java 程序是以 UTF-8 读取的,读取编码错误。

怎么解决以上问题呢?答案就是我们的 Java 程序需要使用 GBK 读取 aaa.txt 文件(这一点很重要,读取的时候必须采用正确的字符集编码,否则后续无论使用什么字符集编码写入 bbb.txt 文件,都是乱码)

InputStreamReader 和 OutputStreamWriter 的构造方法为:

方法名 说明
InputStreamReader(InputStream in) 使用默认字符编码创建 InputStreamReader 对象
InputStreamReader(InputStream in,String chatset) 使用指定的字符编码创建 InputStreamReader 对象
OutputStreamWriter(OutputStream out) 使用默认字符编码创建 OutputStreamWriter 对象
OutputStreamWriter(OutputStream out,String charset) 使用指定的字符编码创建 OutputStreamWriter 对象

下面我们采用 GBK 编码读取 aaa.txt 文件,采用 GB2312 格式写入 bbb.txt 文件,解决 bbb.txt 文本内容乱码问题:
import java.io.*;

public class CharIOTest {
    public static void main(String[] args) {

        /*
        //下面的这段代码,采用的是 UTF-8 读取 aaa.txt,采用 UTF-8 写入 bbb.txt
        //由于读取 aaa.txt 采用的是 GBK 编码,所以 UTF-8 读取出来本身就是乱码
        //所以导致生成的 bbb.txt 无论用什么软件打开,无论如何转换 bbb.txt 编码格式,都显示乱码

        try (BufferedReader br = new BufferedReader(
                new FileReader("d:\\aaa.txt"));
             BufferedWriter bw = new BufferedWriter(
                     new FileWriter("d:\\bbb.txt"))) {

            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        */

        //----------------------------------------

        //采用 InputStreamReader 以 GBK 编码读取 aaa.txt
        //采用 OutputStreamWriter 以 GB2312 编码生成 bbb.txt
        //传入的参数:字符集编码字符串不区分大小写,比如:gbk 和 GBK 是一样的效果
        //以下代码解决了 bbb.txt 乱码问题
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream("d:\\aaa.txt"), "gbk"));
             BufferedWriter bw = new BufferedWriter(
                     new OutputStreamWriter(new FileOutputStream("d:\\bbb.txt"), "gb2312"))) {

            String line;
            while ((line = br.readLine()) != null) {
                bw.write(line);
                bw.newLine();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        //上面演示的代码,在生成 bbb.txt 时,你可以使用任意字符集编码,
        //只要你读取 aaa.txt 的时候,使用的是正确的字符集编码读取就行。
    }
}

到此为止,有关 Java 的常用 IO 流操作,已经演示的差不多了,以上代码都经过测试无误,希望大家能够轻松看懂。
最后的总结就是:处理字符文件,优先使用字符流和转换流;处理音频、视频、图片等文件可以使用字节流,字节流是万能流。



相关