Java基础系列:缓冲流

820 阅读10分钟

小伙伴们,我们认识一下。

俗世游子:专注技术研究的程序猿

前言

前面一章我们对文件(File)和IO流进行了了解,分别介绍了:

上面这些是重点,我们一定要掌握

还重点强调了关于IO流的流向问题: 已程序为参照物

  • 从文件到程序是输入流
  • 从程序到文件是输出流

如果不清楚的话,建议去上一节看看

这里我先给大家看一张图,上面罗列了一些可能会用到的一些流

IO流汇总

下面我们来一个个的介绍其他的流

处理流

在IO流中,存在一些流,是对基础输入流/输出流进行一层包装,通过这些流,我们在通过基础流来处理文件的时候可以提高读取/写入的效率,关于这种流将其称之为处理流

流都是成对出现的,像之前的:

  • InputStream - OutputStream
  • Reader - Writer

所以我在下面介绍的时候也就成对介绍了

字节流转字符流

前面,我们说过输入流处理需要数据源,流数据源可以来自文件,网络等等任意的存在。

虽然任意一种数据源我们都可以采用字节流来进行处理,不过我们在追求功能能用的同时也可以适当的追求下效率嘛,对不对^_^

而且在某一种场景下,只能获取到字节流,而无法获取字符流

比如后面会聊到的:Socket,在Socket中只能得到字节流

如果我们在通过流进行处理的时候,如果我们能够确定数据源过来的是字符集的话,那么我们在处理的时候就可以通过该处理类进行包装,提高处理效率,这就是我们以下要介绍的包装类:

  • InputStreamReader
  • OutputStreamWriter

先看一张图:

处理流

这两个类是专门用来转换字节流的类,下面我们来看看具体的实现方式

File file = new File(System.getProperty("user.dir") + "/study-java/src/main/java/zopx/top/study/jav/_file/InputStreamReaderDemo.java");

/**
         * 常规写法:
         */
FileInputStream inputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream("b.txt");

/**
 * 因为读取是读取文本文件,是字符集数据,所以我们可以通过处理流进行转换
 */

InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);

System.out.println(inputStreamReader.read());

int len = 0;
//        byte[] buffer = new byte[1024];
char[] buffer = new char[1024];
while ((len = inputStreamReader.read(buffer)) > 0) {
    outputStreamWriter.write(new String(buffer, 0, len));
}

outputStreamWriter.flush();
outputStreamWriter.close();
inputStreamReader.close();
// 最后关闭基础流,其实也可以不用管,在处理流关闭的时候会关闭基础流
fileOutputStream.close();
inputStream.close();

System.getProperty("user.dir"):可以得到当前工作目录,如果我们想要输出java的系统参数,可以通过当前方法来处理:

System.getProperties().list(System.out);

这里在通过InputStreamReader来转换流的时候,可以指定字符编码,如果不指定的话是这样的:

  • Java虚拟机的默认字符集,如果Java虚拟机的默认字符集是null的话,那么就采用UTF-8
public static Charset defaultCharset() {
    if (defaultCharset == null) {
        synchronized (Charset.class) {
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

上面就是通过处理流将字节流转换成字符流的处理过程,其实只要将字节流转换过来之后,一系列的处理操作也就和字符流的处理方法一样了

缓冲字节流

缓冲流是我们在实际操作中为了提升性能的另一种处理流,在基础流中,读写文件会直接调用底层读写方法,频繁调用底层方法也会造成性能的消耗,所以java在此基础上实现了缓冲流,通过对底层读写方法的扩展,提高性能上的提升

我们来看看基础IO流的读写方法

// FileInputStream

private native int readBytes(byte b[], int off, int len) throws IOException;

public int read(byte b[]) throws IOException {
    return readBytes(b, 0, b.length);
}

// FileOutputStream
public void write(byte b[], int off, int len) throws IOException {
    writeBytes(b, off, len, append);
}
private native void writeBytes(byte b[], int off, int len, boolean append)
        throws IOException;

缓冲流的读写方法

// BufferedInputStream
public synchronized int read(byte b[], int off, int len)
        throws IOException
{
    getBufIfOpen(); // Check for closed stream
    if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
        throw new IndexOutOfBoundsException();
    } else if (len == 0) {
        return 0;
    }

    int n = 0;
    for (;;) {
        int nread = read1(b, off + n, len - n);
        if (nread <= 0)
            return (n == 0) ? nread : n;
        n += nread;
        if (n >= len)
            return n;
        // if not closed but no bytes available, return
        InputStream input = in;
        if (input != null && input.available() <= 0)
            return n;
    }
}

// BufferedOutputStream
public synchronized void write(byte b[], int off, int len) throws IOException {
    if (len >= buf.length) {
        /* If the request length exceeds the size of the output buffer,
               flush the output buffer and then write the data directly.
               In this way buffered streams will cascade harmlessly. */
        flushBuffer();
        out.write(b, off, len);
        return;
    }
    if (len > buf.length - count) {
        flushBuffer();
    }
    System.arraycopy(b, off, buf, count, len);
    count += len;
}

为什么说缓冲流可以提高性能?

  • 我们通过查看BufferedOutputStream::write方法查看,在write()方法内部会进行判断,如果不满足条件,那么会将我们外层写入的数据存储在protected byte buf[];中,对比这种数据我们可以通过flush()方法刷新然后调用底层write()方法将数据写入到指定文件中
  • 这样就可以减少调用底层方法的次数,从而提高性能

下面我们来看看具体实现方式

private static void bufferIO() throws Exception {
    BufferedInputStream bufis = new BufferedInputStream(new FileInputStream(System.getProperty("user.dir") + "/study-java/src/main/java/zopx/top/study/jav/_file/InputStreamReaderDemo.java"));
    BufferedOutputStream bufos = new BufferedOutputStream(new FileOutputStream("c.txt"));

    int len = 0;
    byte[] buffer = new byte[1024];

    while ((len = bufis.read(buffer)) != -1) {
        bufos.write(buffer, 0, len);
    }
    bufis.close();
    // 这也就是为什么在这里需要调用flush()的原因
    bufos.flush();
    bufos.close();
}

如果看源码的话,我们可以从注释中得到他们的相关信息

  • 在创建BufferedInputStream的时候,会创建一个内部缓冲区数组,当读取或跳过流中的字节时,根据需要从包含的输入流中重新填充内部缓冲区,一次填充许多字节
  • 而通过BufferedOutputStream,应用程序可以将字节写入底层输出流,而不必为写入的每个字节引起对底层系统的调用。

缓冲读写流

缓冲读写流和缓冲字节流效果是一样的,我们来具体看实现:

private static void bufferRead() throws Exception {
    BufferedReader br = new BufferedReader(new FileReader(FILE_NAME));
    BufferedWriter bw = new BufferedWriter(new FileWriter("d.txt"));

    // 读取整行内容
    String line = "";
    while ((line = br.readLine()) != null) {
        bw.write(line);
        // 换行
        bw.newLine();
    }

    br.close();
    bw.flush();
    bw.close();
}

和之前对比的不同点在于

  • BufferedReader除了可以通过char[]来读取内容外,还允许读取整行内容:readLine()
  • 如果我们通过调用readLine()读取内容的时候,在通过BufferedWriter写入的时候,需要调用newLine()来换行,如果缺少这步的话,那么整体数据会写在一行上

这样就方便了很多

打印流

Java中还存在这么一类流,我们经常用,但是很多时候我们没有很注意过,那就是打印流,对应类也就是:

  • System.out
  • System.in

如果这样子来看的话可能会更直观一点:

PrintStream out = System.out;
InputStream in = System.in;

这种方式的体现相信大家都明白:所有内容都是输出在控制台上

PrintStream中为我们提供了方便地打印各种数据值的表示形式的功能

相信还有这种场景,在控制台进行交互的时候我们也会使用到该类,我们来看一个小例子:

// 从控制台输入
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
    // 得到输入内容
    String next = scanner.next();
    System.out.println(next);
}

其他类型的流

结合上面最开始的图,目前还有三组流没有介绍,下面我们分别来介绍一下:

ByteArrayInputStream/ByteArrayOutputStream

ByteArrayInputStream是一个包含内部缓冲区的流,该缓冲区中包含可以从流中读取到的字节。

ByteArrayOutputStream的数据被写入字节数组。 缓冲区随着数据写入而自动增长

我们来看下主要构造

// 核心点
protected byte buf[];
protected int pos;

下面是具体的使用方式:

使用方式和之前讲到的流差不多

ByteArrayInputStream inputStream = new ByteArrayInputStream("mr.sanq学习记录资料完善".getBytes());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

int len = 0;
byte[] buffer = new byte[1024];
while ((len = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, len);
}
inputStream.close();
outputStream.close();

System.out.println(outputStream.toString());
outputStream.writeTo(System.out);

和之前流的不同点在于:

  • ByteArrayInputStream的数据来源只能是byte[]
  • ByteArrayOutputStream通过调用write()方法,会将数据存入其内部属性protected byte buf[],并且会自动增长数据长度,并且可以通过writeTo()方法来指定输出流
  • 而且ByteArrayOutputStreamclose()之后也会可以通过toString()或者toByteArray()来输出数据内容

说白了,就是在操作字节数组

ObjectInputStream/ObjectOutputStream

对基本数据类型和对象进行反序列化/序列化

在文件存储或者是网络传输过程中,可以传递文本、图片、视频、音频等文件,这些文件都是通过二进制数组来传递的,那么如果我们想要传递对象数据呢,能不能实现?

答案当然是肯定的,这里就要先聊一下序列化和反序列化

首先先介绍一下什么序列化和反序列化?

  • 序列化: 是将对象的状态信息转换为可以存储或传输的形式的过程
  • 反序列化:程序从文件或者网络得到序列化之后的数据之后对内容重组得到对象状态的过程

可以说,我们寻常定义的Java对象,只能存储在JVM内存中,当JVM停机的时候,内存被清空,当前对象就不会存在。如果通过序列化并结合ObjectOutputStream就可以将对象转换成流的形式存储在指定的文件中,这样就可以让对象永久保存,只需要通过ObjectInputStream将存储的流对象反序列化成Java对象

基于这种特性,我们可以通过该方式实现:

  • 永久保存对象
  • 将对象进行网络传输

在Java中如果想要实现序列化,只需要实现一个接口:

  • Serializable

这里先来看个案例:

public class Student implements Serializable {
    public Long id;
    public String name;

    public Student() {
    }

    public Student(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

这样就实现了对象的序列化。

我们看看有什么用

上面的实现方式很简单,但是其关键API在于:ObjectInputStream和ObjectOutputStream

我们看具体使用:

private static void io() throws Exception {
    // 写入到指定的文件中
    // 写入成功不用打开看,这不是人能看懂的
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("a.tmp"));
    outputStream.writeObject(new Student(1L, "张三"));
    outputStream.writeUTF("里斯");
    outputStream.writeBoolean(true);
    outputStream.writeInt(1);
    outputStream.close();

    // 从文件中读取对应数据
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("a.tmp"));
    Object o = inputStream.readObject();
    String name = inputStream.readUTF();
    boolean b = inputStream.readBoolean();
    int i = inputStream.readInt();
    inputStream.close();
    System.out.println(o);
    System.out.println(name);
    System.out.println(b);
    System.out.println(i);
}

ObjectIO

ObjectInputStreamObjectOutputStream为我们提供了很多相对应的方法:

Object Write

Object read

我们可以通过这些方法将我们想要存储的类型数据都存储起来。 但是这里要注意一点的是:

  • writeXX()和readXX()的顺序必须一致,否则会报错

下面我们来总结一下该流对象:

  • 如果需要将对象通过IO流进行传输,那么就必须要是实现序列化接口
  • 如果在序列化的时候有个别字段不需要实例化,那么我们可以通过transient来进行修饰,比如:密码

在后面聊到网络的时候,我们可以尝试通过当前流在网络来传递对象

DataInputStream/DataOutputStream

数据输入流允许应用程序以与机器无关的方式从基础输入流中读取原始Java数据类型。 应用程序使用数据输出流来写入数据,以后可以由数据输入流读取

该组流和上面介绍的ObjectInputStream的使用方式差不多,API方法也很相近。下面我们来看看实际操作:

static String str = "ddsad";
private static void io() throws Exception {

    DataOutputStream outputStream = new DataOutputStream(new FileOutputStream("b.tmp"));
    outputStream.writeUTF("卡卡卡");
    outputStream.writeBoolean(true);
    outputStream.writeBytes(str);
    outputStream.writeLong(2);

    outputStream.flush();
    outputStream.close();

    DataInputStream inputStream = new DataInputStream(new FileInputStream("b.tmp"));
    System.out.println(inputStream.readUTF());
    System.out.println(inputStream.readBoolean());
    byte[] buff = new byte[str.length()];
    inputStream.read(buff);
    System.out.println(new String(buff));
    System.out.println(inputStream.readLong());

    inputStream.close();
}

几乎涵盖了ObjectInputStream和ObjectOutputStream所支持的方法,这里就不过多介绍了

文档

上面方法只列出了一点点,如果要查看更多的方法的话推荐查看官方文档:

我就不一一列了,大家通过以下汇总部分,用到那个就去搜一下

IO流文档集