11_Java中的IO流体系详解

0. IO流概述

  • Java 的 I/O 操作主要是指使用 Java 进行输入、输出操作,绝大多数传统 I/O 操作

    类都在 java.io 包中。流(stream)是指在计算机的输入与输出直接运动的数据序列。

    通过把不同类型的数据源之间的数据序列抽象为流,用统一的方式进行表示、处理。

  • File类:

    File类可以定位文件:进行删除、获取文本本身信息等操作。
    但是不能读写文件内容。

  • 读写文件数据需要使用Java中的IO流,如字节流与字符流等

1. File类

  • File类在包java.io.File下、代表操作系统的文件对象(包括文件、文件夹)
  • File类提供了诸如:定位文件,获取文件本身的信息、删除文件、创建文件(文件夹)等功能

1.1 创建File类对象

  • File类中的构造器:
方法名称说明
public File(String pathname)根据文件路径创建文件对象
public File(String parent, String child)从父路径名字符串和子路径名字符串创建文件对象
public File(File parent, String child)根据父路径对应文件对象和子路径名字符串创建文件对象

注意:

  1. java中写文件路径时需要使用俩个反斜杠,一个代表转义,也可以只使用一个正斜杠
  2. File对象中的路径可以定位文件和文件夹
  3. File封装的对象仅仅是一个路径名,这个路径可以是存在的,也可以是不存在的
  4. 同时java中同样支持绝对路径与相对路径,用于定义模块中的文件,是相对到工程下的,所以相对路径一般从模块名开始,示例:
File file = new File(“模块名\\src\\a.txt”);

1.2 File类常用方法

抽象路径就是定义时使用的路径

  • 判断文件类型与文件信息的方法:
方法名称说明
public boolean isDirectory()测试此抽象路径名表示的File是否为文件夹,文件夹不存在时也返回false
public boolean isFile()测试此抽象路径名表示的File是否为文件,文件不存在时也返回false
public boolean exists()测试此抽象路径名表示的File是否存在
public String getAbsolutePath()返回此抽象路径名的绝对路径字符串
public String getPath()获取我们定义时的路径
public String getName()返回由此抽象路径名表示的文件或文件夹的名称,带后缀
public long lastModified()返回文件最后修改的时间毫秒值
public long length()返回该文件的大小,字节数
String getParent()返回此 File 对象所对应目录(最后一级子目录)的父路径名

对于lastModified()方法,返回的是时间毫秒值,我们可以通过SimpleDateFormat对象来转化为具体时间,示例:

long time = file.lastModified();
System.out.println(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time));
  • 创建文件与删除文件的方法:
方法名称说明
public boolean createNewFile()创建一个新的空的文件,几乎不用这个方法,因为使用IO流时会自动帮我们创建文件
public boolean mkdir()只能创建一级文件夹,既可以在一个已经存在的文件夹里再创建一个文件夹(一级),如果在不存在的文件夹里创建文件夹会失败(多级)
public boolean mkdirs()可以创建多级文件夹,因此一般使用这个
public boolean delete()删除由此抽象路径名表示的文件或空文件夹(为了安全),即使在文件被占用时也可以删

注意:
delete删除方法是不进回收站直接删除的,并且默认只能删除文件与空文件夹

以上创建方法都是创建File类对象时写一个不存在的路径文件,再使用这个对象的createNewFile()等方法创建

删除方法是创建File对象后调用这个对象的delete方法,就能将定义时的路径文件删除

  • File类的遍历方法:
方法名称说明
public String[] list()获取当前目录下所有的"一级文件名称"到一个字符串数组中去返回。
public File[] listFiles()获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点),会将文件和文件夹都放进这个File数组中

注意:

  1. 当调用者不存在时返回null(即定义File对象时的路径文件夹不存在)
  2. 当调用者是文件时也返回null
  3. 当调用者是空文件夹时返回长度为0的数组
  4. 当调用者是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏内容
  5. 当调用者是一个需要权限才能进入的文件夹时,返回null

1.3 文件搜索(递归)

  • 文件搜索是一种非规律化递归
  • 使用文件搜索遍历某目录,打印其中的文件名,并统计文件夹个数与文件个数:
package com.exp4.prj4.s1file;

import java.io.File;

public class FileRecursionList1 {

    public static int dirNum = -1;
    public static int fileNum;

    public static void main(String[] args) {
        File file = new File("D:\\eclipse\\lab");
        print(file);
        System.out.println("目录个数:" + dirNum);
        System.out.println("文件个数:" + fileNum);
    }

    public static void print(File file){
        if(file != null){
            if (file.isDirectory()){
                dirNum++;
                File[] files = file.listFiles();
                if (files != null && files.length > 0){
                    for (File f : files) {
                        print(f);
                    }
                }
            }else{
                fileNum++;
                System.out.println(file);
            }
        }

    }

}
  • 遍历某目录寻找目标文件夹,速度可能比系统搜索速度快:
package com.exp4.prj4.s1file;

import java.io.File;

public class Search {

    public static void main(String[] args) {
        // 2. 调用函数开始寻找
        searchFile(new File("D:\\"), "druid.properties");
    }

    /**
     * 1. 定义方法递归遍历某文件夹,寻找目标文件
     * @param file 文件夹对象
     * @param fileName 目标文件名称
     */
    public static void searchFile(File file, String fileName){
        // 3. 判断是否为空以及是否是文件夹
        if (file != null && file.isDirectory()){
            // 4. 获取一级目录对象
            File[] files = file.listFiles();
            // 5. 判断数组对象是否为空以及是否为空数组
            if (files != null && files.length > 0){
                for (File f : files) {
                    // 6. 如果是文件直接判断是不是目标文件
                    if (f.isFile()){
                        if (f.getName().contains(fileName)) {
                            System.out.println("已找到文件,文件路径为: ");
                            System.out.println(f.getAbsolutePath());
                        }
                    }else {
                        // 7. 负责继续递归
                        searchFile(f, fileName);
                    }
                }
            }
        }else{
            System.out.println("传入File对象有误");
        }

    }

}

2. 字符集

2.1 概述

计算机底层不可以直接存储字符的。计算机中底层只能存储二进制(0、1),而且二进制可以转换成十进制

因此我们对字符进行十进制编号,再交给计算机底层转换为二进制编号进行存储

这套编号规则就是字符集

字符>=字节,一字节=1B,一个字节由八位二进制数组成(8bit)

2.2 ASCII字符集

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码):包括了数字、英文、符号,表示不了中文

ASCII使用1个字节存储一个字符,一个字节是8位,总共可以表示128个字符信息,对于英文,数字来说是够用的

2.3 GBK字符集

中文window系统默认的码表。兼容ASCII码表,也包含了几万个汉字,并支持繁体汉字以及部分日韩文字

**注意:**GBK是中国的码表,一个中文以两个字节的形式存储。但不包含世界上所有国家的文字,并且英文还是采用一个字节进行存储

2.4 Unicode编码

  • unicode(又称统一码、万国码、单一码)是计算机科学领域里的一项业界字符编码标准。
    容纳世界上大多数国家的所有常见文字和符号
  • Unicode只是理论上的一种概念,具体实现是UTF-8等. 所以Unicode会先通过UTF-8,UTF-16,以及 UTF-32的编码成二进制后再存储到计算机,其中最为常见的就是UTF-8。

注意:

  1. Unicode是万国码,以UTF-8编码后一个中文一般以三个字节的形式存储
  2. UTF-8也要兼容ASCII编码表
  3. 编码前和编码后的字符集需要一致,否则会出现中文乱码
  4. 尽量使用UTF-8,因为是国际标准

2.5 编码与解码

java中的编码与解码都是通过String实现的,String提供了一套用于编码与解码的API(方法)

2.5.1 String的编码

编码就是将指定文字转换为字节

String提供了以下方法供使用者对字符进行编码:

方法名称说明
byte[] getBytes()使用平台的默认字符集(UTF-8)将该 String编码为一系列字节,将结果存储到新的字节数组中
byte[] getBytes(String charsetName)使用指定的字符集(如"GBK")将该 String编码为一系列字节,将结果存储到新的字节数组中,该方法会抛一个异常,怕你字符集名称写错

示例:

String s = "abc生是种花家人";
byte[] bytes = s.getBytes(); // 会默认使用UTF-8解码
System.out.println(bytes.length);
// 因为UTF-8中文使用三个字节,因此以上会输出(3+18=21)
System.out.println(Arrays.toString(bytes));
// 结果:[97, 98, 99, -25, -108, -97, -26, -104, -81, -25, -89, -115, -24, -118, -79, -27, -82, -74, -28, -70, -70]
byte[] bytes2 = s.getBytes("GBK"); // 使用GBK解码
System.out.println(bytes2.length);
// 因为GBK中文使用俩个字节,因此以上会输出(3+12=15)
System.out.println(Arrays.toString(bytes2));
// 结果:[97, 98, 99, -55, -6, -54, -57, -42, -42, -69, -88, -68, -46, -56, -53]

2.5.2 String的解码

解码:把字节转换为对应的字符(中文)

String的提供了以下构造器供使用者实现对字符的解码:

构造器说明
String(byte[] bytes)通过使用平台的默认字符集(UTF-8)解码指定的字节数组来返回字符
String(byte[] bytes, String charsetName)通过指定的字符集解码指定的字节数组来返回字符
String(byte[] bytes, int off, int len)按照指定顺序参数 ,off 为起始下标,参数 len 为长度,对bytes中的数据进行解码

2.6 总结

  • 英文和数字等在任何国家的字符集中都占1个字节
  • GBK字符中一个中文字符占2个字节
  • UTF-8编码中一个中文1般占3个字节

3. IO流概述

  • IO流也称为输入、输出流,就是用来读写数据
  • I表示intput,是数据从硬盘文件读入到内存的过程,称之输入,负责读
  • O表示output,是内存程序的数据从内存到写出到硬盘文件的过程,称之输出,负责写
  • IO流体系结构:

在这里插入图片描述

IO流按方向分:输入流,输出流

按流中的数据最小单位分:字节流,字符流

其中字节流可以操作所有类型的数据,因为一切数据都是以字节组成,我们一般使用字节流操作音视频文件,不操作文本文件

而字符流只能操作文本文件,我们也一般使用字符流操作文本文件(包括.java文件,.txt文件等)

高级流对象的创建一般需要包装一个低级流对象进去

高级流也称处理流,低级流也称节点流

节点流(低级流)是指直接操作数据读写的流,如FileInputStream,FileReader等
处理流(高级流)是指对已存在的流进行封装使功能更加强大灵活的流,如BufferedInputStream,BufferedReader,PrintWriter等
处理流和节点流体现了Java的装饰设计模式

因此IO流总共可以分为四大类:

  • 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中去的流称为字节输入流
  • 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流称为字节输出流
  • 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流称为字符输入流
  • 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流称为字符输出流

流的概念:在IO流中,不管是字节还是字符,都可以当水一样在内存中流动,因此有的read()方法一次只能读取一个字节,就相当于一滴水一滴水的流动,一滴水一滴水的读取,因此流对象可以比作管道,用于接数据的字节数组可以比作桶

流使用步骤:

  1. 创建流管道(对象),如一个文件字节输入流管道,与源文件接通
  2. 一般我们创建流对象时,会使用多态的写法,将父类类型的引用指向子类对象(子类对象到父类类型)
  3. 调用方法
  4. 释放资源

4. 字节流

先看一下字节流的类结构:

在这里插入图片描述

4.1 字节流父类

InputStream是字节输入流的顶级父类,抽象类,其中定义的方法有:

方法名作用
int read()从输入流中读取一个字节,把它转换为 0-255 之间的整数返回,返回十进制下的这个字节
int read(byte[] b)从输入流中读取若干字节,保存到参数 b 指定的字节数组中
int read(byte[] b, int off, int len)从输入流中读取若干个字节,把它们保存到参数 b 指定的字节数组中。参数 off 指定在字节数组中开始保存数据的起始下标,参数 len 指定读取的字节数目
void close()关闭输入流
int available()返回可以从输入流中读取的字节数目
skip(long n)从输入流中跳过参数 n 指定数目的字节
boolean markSupported(), void mark(int readLimit), void reset()用于重复读入数据

OutputStream是字节输出流的顶级父类,抽象类,其中定义的方法有:

方法名作用
void write(int b)向输出流写出一个字节(作为 int 类型的 b 只取低位字节)
void write(byte[] b)把参数 b 指定的字节数组中的所有字节写到输出流
void write(byte[] b,int off, int len)把参数 b 指定的字节数组中的若干字节写到输出流,参数 off 为起始下标,参数 len 为长度
void close()关闭输出流,会自动刷新数据
void flush()OutputStream 类本身的 flush 方法不执行任何操作,其一些带缓冲区的子类覆盖了 flush 方法,该方法强制把缓冲区内的数据写到输出流中,即刷新数据的方法

注意:

写数据必须使用flush()方法刷新数据,不然数据可能还在内存中缓冲程序就已经结束了

4.2 文件字节输入流:FileInputStream

  • 作用:以内存为基准,把磁盘文件中的数据以字节的形式,一个一个字节或者一个一个字节数组或者一次读完一个文件,将数据读取到内存
  • 构造器:
构造器说明
public FileInputStream(File file)创建字节输入流管道与源文件对象接通
public FileInputStream(String pathname)创建字节输入流管道与源文件路径接通
  • 常用方法:
方法名称说明
public int read()每次读取一个字节返回,如果字节已经没有可读的返回-1
public int read(byte[] buffer)每次读取一个字节数组到buffer中,返回读取的字节数,如果字节已经没有可读的返回-1

其中第二个方法的使用注意点:

  • 读取字节多少取决于字节数组大小(拿水装桶直到桶满)
  • 如果读到最后无法将桶装满,例如数组长度为三,倒数第二次读取内容为[abc],此时还剩下bc俩个没读,那bc会取代ab,使得字节数组内容变为[bcc],中文亦然,因此可能出现中文乱码问题
  • 可以将读取完的字节数组使用String进行编码得到内容
  • 为了防止上面的无法将桶装满问题,可以使用String(byte[] bytes, int off, int len)构造器,指定读取数组的区间,其中len可以通过该方法的返回值获得)

其中每次只读取一个字节存在问题:

  • 性能较低
  • 读取中文字符输出时,无法避免乱码问题

而每次只读取一个字节数组存在的问题:

  • 虽然性能得到了提升,但是仍然无法避免读取中文时的乱码问题

    例如数组长度只为2,此时就无法在UTF-8的情况下读取中文

  • 因此我们以后只使用这个方法进行字节的复制进行文件拷贝,当需要读取字节内容时使用字符流

一次读取一个文件:

  • 为了解决中文乱码问题,我们可以通过定义一个和文件大小一样大的字节数组,但这种做法也并不推荐,因为有的文件太大,而数组最大长度是int型的, 如果文件过大,定义的字节数组可能引起内存溢出
  • 示例:
File file = new File("D://a.txt");
InputStream is = new FileInputStream(file);
byte[] bytes = new byte[(int)file.length()];
// 因为数组长度只能是int,而length()方法返回long,所以需要强制转换
  • 在JDK9之后官方提供了一个方法可以读取文件所有字节:
方法名称说明
public byte[] readAllBytes() throws IOException直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回
  • 示例:
InputStream is = new FileInputStream("D://a.txt");
byte[] bytes = is.readAllBytes();

代码示例:

public class Demo {

    public static void main(String[] args) throws Exception {
        InputStream is = new FileInputStream("D://a.txt");
        // 演示一个一个字节的读取,但以后几乎不使用这种方法
        int b;
        while ((b = is.read()) != -1){
            System.out.print((char)b);
        }

        System.out.println();
        System.out.println("-----------------------");

        InputStream is2 = new FileInputStream("D://a.txt");
        // 演示一个又一个字节数组的读取,但以后解析文本内容不使用这个方法,使用字符流
        // 这种方法一般用于复制字节
        byte[] bytes = new byte[3];
        int len; // 用于接收返回的读取字节数
        while ((len = is2.read(bytes)) != -1){
            System.out.print(new String(bytes,0, len));
        }
        // 关闭流:
        is2.close();
        is.close();
    }
}

4.3 文件字节输出流:FileOutputStream

  • 作用:以内存为基准,把内存中的数据以字节的形式写出到磁盘文件中去的流
  • 看上去用字节流写输出也不会乱码,但使用字符流效率更高
  • 构造器:
构造器说明
public FileOutputStream(File file)创建字节输出流管道与源文件对象接通
public FileOutputStream(File file,boolean append)创建字节输出流管道与源文件对象接通,可追加数据,使用输出流会默认先将文件原有内容清空再写数据,写true表示追加数据管道,就不会清空原有数据了
public FileOutputStream(String filepath)创建字节输出流管道与源文件路径接通
public FileOutputStream(String filepath,boolean append)创建字节输出流管道与源文件路径接通,可追加数据,使用输出流会默认先将文件原有内容清空再写数据,写true表示追加数据管道,就不会清空原有数据了
  • 常用方法:
方法名称说明
public void write(int a)写一个字节出去
public void write(byte[] buffer)写一个字节数组出去
public void write(byte[] buffer , int off , int len)写一个字节数组的一部分出去。参数 off 为起始下标,参数 len 为长度
flush()刷新流,使用这个方法之后还可以继续写数据
close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据

注意:

  • 写数据目标文件可以不存在,输出流会自动帮我们创建文件
  • 写数据可以直接写字节十进制编号,写字节,也可以写字符
  • 使用一个一个字节写数据的方法时不能写中文(因为中文三个字节)
  • 用字节数组写中文时需要使用字符串的 getBytes()方法将中文转成字节
  • 换行写\r\n,因为只写\n 的话Linux识别不出来,示例:
OutputStream is = new FileOutputStream("D://a.txt"); // 目标文件可以不存在
// 一个一个字节写:
os.write('98');
os.write('a');
os.write('我'); // 识别不出来,乱码,因为中文有三个字节
// 使用字节数组写中文:
byte[] bytes = "我是中国人".getBytes();
os.write(bytes);
// 写换行符:
os.write("\r\n".getBytes(StandardCharsets.UTF_8));

4.4 文件拷贝

因为任何文件的底层都是字节,拷贝是一字不漏的转移字节,只要前后文件格式、编码一致没有任何问题

因此我们对于字节流的使用经常是用来进行文件的拷贝

而读取文本文档我们一般不使用字节流而使用字符流

当我们需要进行文件夹的拷贝时就需要进行遍历了,将文件拷贝,将文件夹创建

思路:

  1. 创建文件字节输入流对象与文件字节输出流对象
  2. 定义一个字节数组,一遍读数据一边写数据,数组长度建议1024,1kb时读取速度较快
  3. 文件拷贝要精准,所以需要记录传递的字节数,定义一个变量记录读取的字节数,再将len字节写入,就可以防止写入过多
  4. 关闭资源

示例:

public class Demo {

    public static void main(String[] args) throws Exception {
        InputStream is = new FileInputStream("D:/a.txt");
        OutputStream os = new FileOutputStream("D://b.txt");

        byte[] bytes = new byte[1024];
        int len; // 使用len记录读取的字节数,再将len字节写入,防止写入过多
        while ((len = is.read(bytes)) != -1){
            os.write(bytes,0,len);
        }
        os.close();
        is.close();
    }
}

4.5 使用try/catch/finally关闭资源

  • 因为finally代码块是最终一定要执行的,而读写文件都会报异常,为了代码的健壮性,我们可以在代码执行完毕的最后在finally块释放资源,同时使代码更加简洁易懂

  • JDK 7和JDK9中都简化了资源释放操作,在JDK7之后我们可以将流对象的定义放在try()块中的括号里,这样子无论有没有出异常都会自动帮我们释放资源

  • 而JDK9中我们可以先在try/catch块前定义对象,只需在try()块的括号中放入对象名就会自动帮我们释放资源

    但这个方法多少有点鸡肋,定义在前面,但是try/catch语句块之后对象资源都被释放了,我们先定义了又有什么用,后面又用不了,因此我们一般不用这个方法

    或许在流对象是方法传递进来的参数时使用这个方法有点用

注意:

  • JDK 7 以及 JDK 9的()中只能放置资源对象,否则报错
  • 什么是资源呢?
  • 资源都是实现了Closeable/AutoCloseable接口的类对象:
public abstract class InputStream implements Closeable {}
public abstract class OutputStream implements Closeable, Flushable{} 
  • JDBC中的数据库连接对象,执行SQL对象等都实现了这个接口,因此也可以使用这种方法释放资源
  • 我们也可以定义一些实现了这个Closeable/AutoCloseable接口的类,再使用try()的()放置对象释放资源
  • 定义在try()中的对象会被默认加上final修饰

代码示例:

public class Demo {

    public static void main(String[] args) {
        try (
                InputStream is = new FileInputStream("D:/a.txt");
                OutputStream os = new FileOutputStream("D://b.txt");
                ){
            byte[] bytes = new byte[1024];
            int len; // 使用len记录读取的字节数,再将len字节写入,防止写入过多
            while ((len = is.read(bytes)) != -1){
                os.write(bytes,0,len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5. 字符流

  • 使用字符流进行文本文档读取时只要读取文档的编码与代码编码一致就不会出现中文乱码问题

照样先看一下字符流的类结构:

在这里插入图片描述

5.1 字符流父类

Reader是字符输入流的顶级父类,抽象类,其中定义的方法有:

方法名作用
int read()从输入流中读取一个字符,以 int 整数返回,读取不到数据(读取完时返回-1)
int read(char[] b)从输入流中读取若干字符,保存到参数 b 指定的字符
int read(char[] b, int off, int len)数组中从输入流中读取若干个字符,把它们保存到参数 b 指定的字符数组中,参数 off 为起始下标,参数 len 为长度
void close()关闭输入流
skip(long n)从输入流中跳过参数 n 指定数目的字符
boolean markSupported(), void mark(int readLimit), void reset()用于重复读入数据

Writer是字符输出流的顶级父类,抽象类,其中定义的方法有:

方法名称作用
void write(int c)向输出流写出一个字符
void write(char[] b)把字符数组中的所有字符写到输出流
void write(char[] b,int off, int len)参数 off 为起始下标,参数 len 为长度
void flush()Writer 类本身的 flush 方法不执行任何操作,其一些带缓冲区的子类覆盖了 flush 方法,该方法强制把缓冲区内的数据写到输出流中,即刷新数据
void close()关闭输出流,会自动刷新数据
Writer append(char c)向当前流追加字符 c,并返回当前字符输出流实例

注意:

  • 使用输出流写数据同样需要刷新数据
  • 字符流使用的字符数组(char[])的长度是字符长度,与文件字节大小是不同的

5.2 文件字符输入流:FileReader

  • 作用:以内存为基准,把磁盘文件中的数据以字符的形式读取到内存中去

  • 构造器:

构造器说明
public FileReader(File file)创建字符输入流管道与源文件对象接通
public FileReader(String pathname)创建字符输入流管道与源文件路径接通
  • 常用方法:
方法名称说明
public int read()每次读取一个字符返回,如果字符已经没有可读的返回-1
public int read(char[] buffer)每次读取一个字符数组,返回读取的字符个数,如果字符已经没有可读的返回-1
int read(char[] b, int off, int len)数组中从输入流中读取若干个字符,把它们保存到参数 b 指定的字符数组中,参数 off 为起始下标,参数 len 为长度

注意:

  • 在循环读取字符时也需要精准记录读取的字符数 ,所以使用加上off和len参数的read方法更好

5.3 文件字符输出流:FileWriter

  • 作用:以内存为基准,把内存中的数据以字符的形式写出到磁盘文件中去的流
  • 构造器:
构造器说明
public FileWriter(File file)创建字符输出流管道与源文件对象接通
public FileWriter(File file,boolean append)创建字符输出流管道与源文件对象接通,设置为true可追加数据
public FileWriter(String filepath)创建字符输出流管道与源文件路径接通
public FileWriter(String filepath,boolean append)创建字符输出流管道与源文件路径接通,设置为true可追加数据
  • 常用方法:
方法名称说明
void write(int c)写一个字符
void write(char[] cbuf)写入一个字符数组
void write(char[] cbuf, int off, int len)写入字符数组的一部分
void write(String str)写一个字符串
void write(String str, int off, int len)写一个字符串的一部分
flush()刷新流,刷新完还可以继续写数据
close()关闭流,释放资源,但是在关闭之前会先刷新流。一旦关闭,就不能再写数据

注意:

  • 使用字符数组写数据较麻烦(要么一个一个中文定义到字符数组中,要么对字符串使用toArrayChar()方法,多此一举),因此我们一般使用参数为字符串的writer方法
  • 只有输出流参数可以是字符串,输入流只能是字符数组(因为要将读取的数据拆分成一个个字符再封装)
  • 字符输出流实现换行:
fw.write(“\r\n”);

5.4 字节流字符流使用总结

  • 字节流适合做一切文件数据的拷贝(音视频,文本)
  • 字节流不适合读取中文内容输出
  • 字符流适合做文本文件的操作(读,写)

6. 缓冲流

6.1 缓冲流概述

  • 缓冲流也称为高效流、或者高级流。之前学习的字节流可以称为原始流。
  • 作用:缓冲流自带缓冲区、可以提高原始字节流、字符流读写数据的性能

高效原因:

缓冲流对象会在内存中创造一个缓冲区数组(缓冲管道),默认大小是8192字节(8kb),将原始流的数据先加载到这个8kb的缓冲区中

这样子我们每次读取数据时都是以内存的形式直接在缓冲区中拿,性能更好

而写入数据的时候也会创建一个8KB缓冲池(数组),数据就直接写入到缓冲池中去,写数据性能极高了。

缓冲流分类:

  • 字节缓冲流
    1. 字节缓冲输入流: BufferedInputStream
    2. 字节缓冲输出流:BufferedOutputStream
  • 字符缓冲流
    1. 字符缓冲输入流:BufferedReader
    2. 字符缓冲输出流:BufferedWriter

其中字符缓冲流都是Reader和Writer的直接子类

而字节缓冲流分别继承了FilterInputStream和FilterOutputStream,这俩个父类的父类的就是InputStream和OutputStrean

6.2 字节缓冲流

  • 构造方法:
构造器说明
public BufferedInputStream(InputStream is)可以把低级的字节输入流包装成一个高级的缓冲字节输入流管道,从而提高字节输入流读数据的性能
public BufferedOutputStream(OutputStream os)可以把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能
  • 在功能上缓冲流并没有变化,依然是read和write方法读写数据等,调用方法时使用缓冲流对象调用方法即可

6.3 字符缓冲流

  • 构造器:
构造器说明
public BufferedReader(Reader r)可以把低级的字符输入流包装成一个高级的缓冲字符输入流管道,从而提高字符输入流读数据的性能
public BufferedWriter(Writer w)可以把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能
  • 在功能上缓冲流并没有太大变化,依然是read和write方法读写数据等,调用方法时使用缓冲流对象调用方法即可
  • 字符缓冲输入流新增了一个功能:
方法说明
public String readLine()读取一行数据返回,如果读取没有完毕,无行可读返回null

读取时使用这个方法更加方便与符合逻辑

  • 字符缓冲输出流新增功能:
方法说明
public void newLine()换行操作

换行执行这个方法会更加方便

注意:

用于字符流中新增的俩个方法是父类中没有定义的方法,因此在创建字符缓冲流对象时不能使用多态的写法

7. 转换流

7.1 字符转换流概述

在我们使用字符流读取文本文件时虽然一般不会产生中文乱码问题,但是当文档编码与代码编码不一致时仍然会产生中文乱码问题

这个时候就需要使用字符转换流

下面以字符输入转换流为例讲解:

转换流会先提取文件(GBK)的原始字节流,原始字节不会存在问题

然后把字节流以指定编码转换成字符输入流,这样字符输入流中的字符就不乱码

而字符输出转换流会用指定编码把字节输出流转换成字符输出流,从而可以指定写出去的字符编码

字符转换流分类:

  1. InputStreamReader:字符输入转换流
  2. OutputStreamWrite:字符输出转换流

其中文件字符输入流FileReader就是继承自InputStreamReader

文件字符输出流FileWrite也是继承自OutputStreamWrite

使用步骤:

  1. 先创建原始字节流对象读取文件字节
  2. 再创建字符转换流对象,将字节流对象与文件编码形式作为参数传入进去
  3. 最后创建字符缓冲流对象,将字符输入转换流对象作为参数传入进去
  4. 使用缓冲流的方法读写数据
  5. 释放资源 ,如果使用手动close()释放资源,那只需要将缓冲流资源释放,把最外层关闭其他的资源也释放

7.2 字符输入转换流:InputStreamReader

  • 作用:可以把原始的字节流按照指定编码转换成字符输入流
  • 构造器:
构造器说明
public InputStreamReader(InputStream is)可以把原始的字节流按照代码默认编码转换成字符输入流。几乎不用,与默认的FileReader一样
public InputStreamReader(InputStream is ,String charset)可以把原始的字节流按照指定编码转换成字符输入流,这样字符流中的字符就不乱码了(重点)

阅读FileReader源码后发现,在FileReader的默认构造器中,就是调用父类InputStreamReader的默认编码构造器,因此我们一般不会使用这个默认编码构造器

FileReader部分源码:

public FileReader(File file) throws FileNotFoundException {
    super(new FileInputStream(file));
}

代码示例:

public class Demo {
    public static void main(String[] args) {
        // 使用转换流读取GBK编码的文件
        try (
                // 先创建字节输入流对象
                InputStream is = new FileInputStream("D:/a.txt");
                // 以GBK编码将字节转换为字符
                Reader isr = new InputStreamReader(is, "GBK");
                // 再将字符流包装到缓冲流中
                BufferedReader br = new BufferedReader(isr);
                ){
            String str;
            while ((str = br.readLine()) != null){
                System.out.println(str);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7.3 字符输出转换流:OutputStreamWriter

控制写出去的字符编码有俩种方式:

  1. 可以把字符以指定编码获取字节后再使用字节输出流写出去,如:
    “我爱你中国”.getBytes(“编码”)

    不推荐,太繁琐

  2. 使用字符输出转换流实现

  • 字符输入转换流作用:可以把字节输出流按照指定编码转换成字符输出流

  • 构造器:

构造器说明
public OutputStreamWriter(OutputStream os)可以把原始的字节输出流按照代码默认编码转换成字符输出流。几乎不用。
public OutputStreamWriter(OutputStream os,String charset)可以把原始的字节输出流按照指定编码转换成字符输出流(重点)

阅读FileWriter源码后发现,在FileWriter的默认构造器中,就是调用父类OutputStreamWriter的默认编码构造器,因此我们一般不会使用这个默认编码构造器

FileWriter部分源码:

public FileWriter(String fileName) throws IOException {
    super(new FileOutputStream(fileName));
}

8.序列化对象

8.1 对象序列化

  • 作用:以内存为基准,把内存中的对象存储到磁盘文件中去,称为对象序列化
  • 使用到的流是对象字节输出流:ObjectOutputStream,构造器:
构造器说明
public ObjectOutputStream(OutputStream out)把低级字节输出流包装成高级的对象字节输出流
  • 对象字节输出流的对象序列化方法:
方法名称说明
public final void writeObject(Object obj)把对象写出去到对象序列化流的文件中去,因为是独有方法,所以不能用多态的写法创建对象
  • 序列化对象的对象要求对象必须实现序列化接口,即Serializable接口

8.2 对象反序列化

  • 作用:以内存为基准,把存储到磁盘文件中去的对象数据恢复成内存中的对象,称为对象反序列化。
  • 使用到的流是对象字节输入流:ObjectInputStream构造器:
构造器说明
public ObjectInputStream(InputStream out)把低级字节输入流包装成高级的对象字节输入流
  • 对象反序列化方法:
方法名称说明
public Object readObject()把存储到磁盘文件中去的对象数据恢复成内存中的对象返回

9. 打印流

  • 作用:打印流可以实现方便、高效的打印数据到文件中去。打印流一般是指:PrintStream,PrintWriter两个类

  • 其中PrintStream属于字节流

    PrintWrite属于字符流

  • 方便原因:可以实现打印什么数据就是什么数据,例如打印整数97写出去就是97,打印boolean的true,写出去就是true,不会出现打97出来的是97对应的’a’这种情况

  • 高效原因:内部已经包装好了缓冲流

9.1 PrintStream

  • 构造器:
构造器说明
public PrintStream(OutputStream os)打印流直接通向字节输出流管道,需要追加数据时使用这个构造器
public PrintStream(File f)打印流直接通向文件对象
public PrintStream(String filepath)打印流直接通向文件路径
public PrintStream(File f, String charset)创建指定编码的打印流对象通向文件对象

观察源码发现,使用String filepath创建PrintStream对象实际上会在构造器内部根据路径创建FileInputStream对象

使用File f创建亦然

  • 常用方法:
方法说明
public void print()打印任意类型的数据出去,而不是写
public void println()带换行
void flush()刷新数据
void close()关闭打印流,会自动刷新数据

9.2 PrintWriter

  • 构造器
构造器说明
public PrintWriter(OutputStream os)打印流直接通向字节输出流管道,需要追加数据时使用这个构造器
public PrintWriter (Writer w)打印流直接通向字符输出流管道
public PrintWriter (File f)打印流直接通向文件对象
public PrintWriter (String filepath)打印流直接通向文件路径
public PrintWriter (String filepath, String charset)创建指定编码的打印流对象通向文件对象
  • 常用方法:
方法说明
public void print()打印任意类型的数据出去,是打印而不是写
public void println()带换行
void flush()刷新数据
void close()关闭打印流,会自动刷新数据

9.3 区别

  • 打印数据功能上是一模一样的,都是使用方便,性能高效(核心优势)
  • PrintStream继承自字节输出流OutputStream,支持写字节数据的方法,即可以调用字节输出流的write()方法写字节,但没有多大意义
  • PrintWriter继承自字符输出流Writer,支持写字符数据出去,即可以调用字符输出流的write()方法写字符,但没有多大意义
  • 因此作为需要打印数据时,使用俩者随便一个都可以

9.4 输出语句重定向

  • 输出语句重定向:就是将平时的打印语句打印位置从控制台改到文件中
  • 使用示例:
PrintStream ps = new PrintStream("文件地址");
System.setOut(ps); // 把系统打印流对象out改成我们的打印流
// 之后再使用打印语句就会打印到该文件中

其实在我们使用的System.out.println中,out就是一个打印流对象!

out的本质:public static final java.io.PrintStream out

因此我们平时使用的打印语句中的println方法和打印流的println方法是一样的

在源码中是在静态代码块中将out初始化为PrintStream对象,并默认打印到控制台中(其实控制台也是一个文件(out文件夹里))


10. 总结

  • InputStream体系结构:

在这里插入图片描述

  • OutputStream体系结构:

在这里插入图片描述

  • Reader体系结构:

在这里插入图片描述

  • Writer体系结构:

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值