15【IO流增强】

追求适度,才能走向成功;人在顶峰,迈步就是下坡;身在低谷,抬足既是登高;弦,绷得太紧会断;人,思虑过度会疯;水至清无鱼,人至真无友,山至高无树;适度,不是中庸,而是一种明智的生活态度。

导读:本篇文章讲解 15【IO流增强】,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文


上一篇14【IO流基础】

下一篇16【TCP、UDP、网络通信】

目录【JavaSE零基础系列教程目录】



day15【IO流增强】

一、转换流

1.1 字符编码和字符集

1.1.1 编码与解码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

1.1.2 字符集与编码

  • 字符集(charset)字符集简单来说就是指字符的集合,例如所有的英文字母是一个字符集,所有的汉字是一个字符集,当然,把全世界所有语言的符号都放在一起,也可以称为一个字符集。计算机中的字符包括文字、图形符号、数学符号等;

拿汉字中的“汉”这个字符来说,我们看到的这个“汉”其实是这个字符的一种具体表现形式,是它的图像表现形式,而且它是用中文(而非拼音)书写而成,使用宋体外观;因此同一个字符的表现形式可能有无数种,把每一种的表现形式下的同一个字符都纳入到字符集中,会使得字符集过于庞大;

因此字符集中的字符,都是指唯一存在的抽象字符,而忽略了它的具体表现形式。在给定一个抽象字符集合中的每个字符都分配了一个整数编号之后,这个字符集就有了顺序,就成为了编码字符集。同时,这个编号,可以唯一确定到底指的是哪一个字符(哪一种具体形式)

1.1.3 字符集与编码的关系

字符集与编码的关系图:

在这里插入图片描述

在早期,字符集与编码是一对一的。有很多的字符编码方案,一个字符集只有唯一一个编码实现,两者是一一对应的。比如 GB2312,这种情况,无论你怎么去称呼它们,比如“GB2312编码”,“GB2312字符集”,说来说去其实都是一个东西,可能它本身就没有特意去做什么区分,所以无论怎么说都不会错。

到了 Unicode,变得不一样了,唯一的 Unicode 字符集对应了三种编码:UTF-8,UTF-16,UTF-32。字符集和编码等概念被彻底分离且模块化,其实是 Unicode 时代才得到广泛认同的。

在这里插入图片描述

从上图可以很清楚地看到,

1、编码是依赖于字符集的,就像代码中的接口实现依赖于接口一样;

2、一个字符集可以有多个编码实现,就像一个接口可以有多个实现类一样。

  • 为什么 Unicode 这么特殊?

搞出新的字符集标准,无外乎是旧的字符集里的字符不够用了。Unicode 的目标是统一所有的字符集,囊括所有的字符,因此再去整什么新的字符集就没必要了。

但如果觉得它现有的编码方案不太好呢?在不能弄出新的字符集情况下,只能在编码方面做文章了,于是就有了多个实现,这样一来传统的一一对应关系就打破了。

在这里插入图片描述

从上图可以看出,由于历史方面的原因,你还会在不少地方看到把 Unicode 和 UTF-8 混在一块的情况,这种情况下的 Unicode 通常就是 UTF-16 或者是更早的 UCS-2 编码。

1.2 编码的问题

当文本写入时的编码与读取时的编码不一致时就会出现乱码的现象;

准备两个文件,一个采用GB2312编码,一个采用UTF-8编码,使用Notepad++编辑器可编辑:

在这里插入图片描述

准备Java代码分别读取两个文件:

package com.dfbz.demo01;

import java.io.FileReader;
import java.io.IOException;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_读取其他编码的文件 {
    public static void main(String[] args) throws IOException {
        // 该文件是采用GBK编码的
        FileReader fr = new FileReader("C:\\Users\\Horizon\\Desktop\\hello.txt");
        int read;
        while ((read = fr.read()) != -1) {
            // 打印出现乱码
            System.out.print((char)read);
        }
        fr.close();
    }
}

发现在读取GB2312时出现中文乱码:

在这里插入图片描述

这是因为IDEA默认情况下都是采用UTF-8进行编码与解码,平常我们在操作时感觉不到编码的问题;而我们手动编辑了一个文本文件以GB2312的编码格式保存,此时再使用UTF-8编码进行读取就出现乱码问题;

1.3 转换输入流

转换流java.io.InputStreamReader,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

1.3.1 构造方法

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

示例代码:

InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");

1.3.2 使用转换输入流读取

package com.dfbz.demo01;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws IOException {
        // 定义文件路径,文件为gbk编码
        String filename = "C:\\Users\\Horizon\\Desktop\\hello.txt";
        // 创建流对象,指定GBK编码(默认UTF-8编码)
        InputStreamReader isr = new InputStreamReader(new FileInputStream(filename), "GB2312");

        // 定义变量,保存字符
        int read;

        // 使用指定编码字符流读取,正常解析
        while ((read = isr.read()) != -1) {
            // 输出正常
            System.out.print((char) read);
        }
        isr.close();
    }
}

1.4 转换输出流

转换流java.io.OutputStreamWriter ,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

1.4.1 构造方法

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

示例代码:

OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("out.txt") , "GBK");

1.4.2 使用转换输出流写文件

package com.dfbz.demo01;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03 {
    public static void main(String[] args) throws IOException {
        // 定义文件路径
        String filename = "osw.txt";

        // 创建流对象,指定GBK编码
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filename), "UTF-8");

        // 写出数据
        osw.write("你好");    // 保存为4个字节
        osw.close();
    }
}

Tips:UTF-8编码表中一个中文汉字占3个字节,GBK则占2个字节;

二、缓冲流

计算机访问外部设备或文件,要比直接访问内存慢的多。如果我们每次调用read()方法或者write()方法访问外部的设备或文件,CPU就要花上最多的时间是在等外部设备响应,而不是数据处理。为此,我们开辟一个内存缓冲区的内存区域,程序每次调用read()方法或write()方法都是读写在这个缓冲区中。当这个缓冲区被装满后,系统才将这个缓冲区的内容一次集中写到外部设备或读取进来给CPU。使用缓冲区可以有效的提高CPU的使用率,能提高整个计算机系统的效率。

2.1 缓冲流的分类

缓冲流是针对4个顶层父类的流的增强,分为:

输入缓冲流 输出缓冲流
字节缓冲流 BufferedInputStream BufferedOutputStream
字符缓冲流 BufferedReader BufferedWriter

缓冲流在读取/写出数据时,内置有一个默认大小为8192字节/字符的缓冲区数组,当发生一次IO时读满8192个字节再进行操作,从而提高读写的效率。

2.2 字节缓冲流

2.2.1 构造方法

  • public BufferedInputStream(InputStream in) :创建一个 新的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的缓冲输出流。

示例代码:

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("bos.txt"));

2.2.2 使用字节缓冲流

我们分别使用普通流和缓冲流对一个大小为10.4MB的文件进行拷贝,查看两种方案所花费的时间;

  • 使用普通流:
package com.dfbz.demo01;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws FileNotFoundException {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                FileInputStream fis = new FileInputStream("D:\\apache-tomcat-8.0.43.zip");
                FileOutputStream fos = new FileOutputStream("D:\\apache-tomcat-8.0.43_bak.zip")
        ) {
            // 读写数据
            int b;
            while ((b = fis.read()) != -1) {
                fos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("普通流复制时间:" + (end - start) + " 毫秒");
    }
}

运行结果:

在这里插入图片描述

  • 使用缓冲流拷贝:
package com.dfbz.demo01;

import java.io.*;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws FileNotFoundException {
        // 记录开始时间
        long start = System.currentTimeMillis();
        // 创建流对象
        try (
                BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\apache-tomcat-8.0.43.zip"));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\apache-tomcat-8.0.43_bak.zip"));
        ){
            // 读写数据
            int b;
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
    }
}

在这里插入图片描述

可以看出,缓冲流拷贝文件的效率比普通流高太多太多,因此我们在做大文件拷贝时应该尽量选用缓冲流;

2.3 字符缓冲流

在字节缓冲流中,内部维护了一个8192大小的一个字节数组,字符流则是内部维护了一个8192大小的字符数组;在一次IO时读取8192个字符,提升读取/写入性能。另外,字符缓冲流在普通流的基础上添加了一些独特的方法,让我们读取/写出字符更加方便;

2.3.1 构造方法

  • public BufferedReader(Reader in) :创建一个 新的缓冲输入流。
  • public BufferedWriter(Writer out): 创建一个新的缓冲输出流。

示例代码:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));

// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

2.3.2 常用方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。

newLine方法示例代码如下:

package com.dfbz.demo02;

import java.io.*;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws IOException {
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("test.txt"));
        // 写出数据
        bw.write("我是");
        // 写出换行
        bw.newLine();

        bw.write("中国");
        bw.newLine();

        bw.write("人");
        bw.newLine();

        // 释放资源
        bw.close();
    }
}

readLine方法示例代码如下:

package com.dfbz.demo02;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws IOException {
        // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("test.txt"));
        // 定义字符串,保存读取的一行文字
        String line  = null;
        // 循环读取,读取到最后返回null
        while ((line = br.readLine())!=null) {
            System.out.println(line);
        }
        // 释放资源
        br.close();
    }
}

三、序列流

3.1 序列化概述

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。看图理解序列化:

在这里插入图片描述

3.2 对象输出流

java.io.ObjectOutputStream 类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。

3.2.1 构造方法

  • public ObjectOutputStream(OutputStream out) : 创建一个指定OutputStream的ObjectOutputStream。

示例代码:

FileOutputStream fileOut = new FileOutputStream("goods.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);

3.2.2 对象的序列化

一个对象要想序列化,必须满足两个条件:

  • 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用transient 关键字修饰。

写出对象方法:

  • public final void writeObject(Object obj) : 将指定的对象写出。

准备一个对象:

package com.dfbz.demo01;

import java.io.Serializable;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Goods implements Serializable {

    // 标题
    private String title;

    // 价格
    private Double price;

    // 库存
    private transient Integer store;        // transient修饰的成员不会被序列化

    public Goods() {
    }

    public Goods(String title, Double price, Integer store) {
        this.title = title;
        this.price = price;
        this.store = store;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    public Integer getStore() {
        return store;
    }

    public void setStore(Integer store) {
        this.store = store;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "title='" + title + '\'' +
                ", price=" + price +
                ", store=" + store +
                '}';
    }
}

测试代码:

package com.dfbz.demo01;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) {
        Goods goods = new Goods("赣南脐橙",20.0D,20000);
        try {
            // 创建序列化流对象
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("goods.txt"));
            // 写出对象
            oos.writeObject(goods);     // title、price被序列化,store没有被序列化。
            // 释放资源
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.3 对象输入流

ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。

3.3.1 构造方法

  • public ObjectInputStream(InputStream in) : 创建一个指定InputStream的ObjectInputStream。

3.3.2 对象的反序列化

如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream读取对象的方法:

  • public final Object readObject() : 读取一个对象。
package com.dfbz.demo01;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) {
        Goods goods = null;
        try {
            // 创建反序列化流
            FileInputStream fis = new FileInputStream("goods.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            // 读取一个对象
            goods = (Goods) ois.readObject();
            // 释放资源
            fis.close();
            ois.close();
        } catch (IOException | ClassNotFoundException e) {
            // 捕获其他异常
            e.printStackTrace();
        }
        
        System.out.println(goods.toString());   // Goods{title='赣南脐橙', price=20.0, store=null}
        
    }
}

3.3.3 序列化版本号

当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。发生这个异常的原因如下:

  • 1)该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 2)该类包含未知数据类型
  • 3)该类没有可访问的无参数构造方法

Serializable 接口给需要序列化的类,提供了一个序列版本号。serialVersionUID 该版本号的目的在于验证序列化的对象和对应类是否版本匹配。

package com.dfbz.demo01;

import java.io.Serializable;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Goods implements Serializable {

    // 序列化版本号
    private static final long serialVersionUID = 1L;

    // 标题
    private String title;

    // 价格
    private Double price;
    
    private transient Integer store;        // transient修饰的成员不会被序列化
}

Tips:对象在序列化时,如果指定了序列号,那么在反序列化时会默认读取到上次序列化时的序列号,即使类已经修改过也没有关系;

3.4 序列化案例

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。

3.4.1 案例分析

  1. 把若干学生对象 ,保存到集合中。
  2. 把集合序列化。
  3. 反序列化读取时,只需要读取一次,转换为集合类型。
  4. 遍历集合,可以打印所有的学生信息

3.4.2 案例实现

package com.dfbz.demo02;

import com.dfbz.demo02.Book;

import java.io.*;
import java.util.ArrayList;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws Exception {
        // 创建 学生对象
        Book book1 = new Book("《西游记》", "吴承恩");
        Book book2 = new Book("《三国演义》", "罗贯中");
        Book book3 = new Book("《水浒传》", "施耐庵");
        Book book4 = new Book("《红楼梦》", "曹雪芹");

        ArrayList<Book> bookList = new ArrayList<>();
        bookList.add(book1);
        bookList.add(book2);
        bookList.add(book3);
        bookList.add(book4);

//        serialize(bookList);
        deserialize(bookList);
    }

    public static void serialize(ArrayList<Book> bookList) throws IOException {
        // 创建 序列化流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
        // 写出对象
        oos.writeObject(bookList);
        // 释放资源
        oos.close();
    }

    public static void deserialize() throws Exception {
        // 反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
        // 读取对象,强转为ArrayList类型
        ArrayList<Book> list = (ArrayList<Book>) ois.readObject();

        System.out.println(list);
    }
}

class Book {
    private String name;
    private String author;

    public Book() {
    }

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

四、打印流

4.1 打印流概述

打印流java.io.PrintStream类是OutputStream的一个子类,因此也是属于字节输出流。打印流的功能主要是将数据打印(输出)到控制台,方便我们输出的;

我们平时向控制台输出内容都是借助打印流来完成的:

在这里插入图片描述

4.2 打印流的使用

4.2.1 构造方法

  • public PrintStream(String fileName) : 使用指定的文件名创建一个新的打印流。

示例代码:

PrintStream ps = new PrintStream("ps.txt")

4.2.2 打印流输出内容

  • public void println(String x):将指定的字符串输出
  • public void println(int x):将指定的整形输出

我们之前通过System.out获取到的就是一个打印流,因此打印流的方法不多赘述;

示例代码:

package com.dfbz.demo01;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("ps.txt");

        // 打印并换行
        ps.println("我是");
        ps.println("中国");
        ps.println("人");

        ps.close();
    }
}

4.3 标准输入输出流

4.3.1 标准输入流

标准输入流是一个缓冲字节输入流(BufferedInputStream),他默认指向的是控制台(键盘),通过System.in获取;

我们之前在用Scanner的时候,构造方法如下:

在这里插入图片描述

System.in获取的就是标准输入流(指向键盘),因此Scanner总是接受我们键盘输入的数据,我们可以更改Scanner读取的流对象:

package com.dfbz.demo01;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02 {
    public static void main(String[] args) throws FileNotFoundException {

        Scanner sc = new Scanner(new FileInputStream("ps.txt"));

        String str = sc.next();
        System.out.println(str);

        str=sc.next();          // 此时读取的不是控制台的数据,而是ps.txt中的数据
        System.out.println(str);

        str=sc.next();
        System.out.println(str);

        str=sc.next();              // 读取到结尾 抛出java.util.NoSuchElementException
        System.out.println(str);
    }
}

也可以更改标准输入流:

package com.dfbz.demo01;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo03 {
    public static void main(String[] args) throws FileNotFoundException {
        // 更改标准输入流        
        System.setIn(new FileInputStream("StandardInputStream.txt"));
    }
}

4.3.2 标准输出流

标准输出流是一个打印流(PrintStream),他默认指向的是控制台,通过System.out获取标准输出流,因此我们之前总是使用System.out.println往控制台输出内容;

我们也可以更改标准输出流,让其不在输出到控制台,而是输出到我们制定的地方:

package com.dfbz.demo01;

import java.io.*;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo04 {
    public static void main(String[] args) throws FileNotFoundException {

        // 修改标准输出流
        System.setOut(new PrintStream("StandardOutputStream.txt"));

        // 此时输出到StandardOutputStream.txt文件中,而不是控制台
        System.out.println("我是中国人");
        System.out.println("犯我中华者");
        System.out.println("虽远必诛");
    }
}

五、其他流

5.1 字节数组流

我们之前的所有流最终的流向(指向)都是文件,通过FileInputStream/FileOutputStream关联一个文件,从文件进行读取/写入;但有些情况下我们可以将流直接指向内存,对内存的某块区域进行读写操作;

Java中提供有ByteArrayInputStreamByteArrayOutputStream,分别是字节数组输入流和字节数组输出流;

Tips:对于字节数组输入/输出流,关闭该流无效,关闭此流后依旧可以正常读写

5.1.1 ByteArrayInputStream

  • ByteArrayInputStream:内置一个字节数组缓冲区,调用read方法对其缓冲区进行读取,并记录读取到的索引,提供索引标记、重置位置等功能;
    • 相关成员变量:
      • buf[]:由数据流的创建者提供的字节数组。
      • count:字节数组的长度
      • mark:数据读取时,标记读取的位置,需要调用mark(int readAheadLimit) 方法进行标记
      • pos:读取的字节索引位置
    • 相关方法:
      • read():从此输入流中读取下一个字节并返回,当流到达末尾时(pos到达末尾时),返回-1
      • read(byte b[], int off, int len) : 从输入流中off的位置读取len个字节到b[]数组中,返回实际读取的字节数
      • mark(int readAheadLimit):对当前的pos位置进行标记,传递的readAheadLimit参数是无意义的;
      • reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置

– 测试代码:

package com.dfbz.demo01_字节数组流;

import java.io.ByteArrayInputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_ByteArrayInputStream {

    public static void main(String[] args) throws Exception {
        // 创建一个内存字节输入流(从内存中读取数据)
        ByteArrayInputStream bis = new ByteArrayInputStream("abc".getBytes());

        // 读取一个字节
        int b = bis.read();
        System.out.println((char) b);                // a

        bis.close();            // 内存字节流不需要关闭,即使关闭了依旧可以继续使用

        byte[] data = new byte[1024];
        bis.read(data, 0, 2);

        System.out.println(new String(data, 0, 2));           // bc

    }

    public static void method() throws Exception {
        // 创建一个内存字节输入流(从内存中读取数据)
        ByteArrayInputStream bis = new ByteArrayInputStream("abcd".getBytes());           // pos=0,count=4,mark=0
        System.out.println(bis);


        int data = bis.read();              // pos=1,count=4,mark=0
        System.out.println((char) data);    // a

        data = bis.read();                  // pos=2,count=4,mark=0
        System.out.println((char) data);    // b

        bis.mark(1);          // pos=2,count=4,mark=2

        data = bis.read();                  // pos=3,count=4,mark=2
        System.out.println((char) data);    // c

        // 将pos设置为mark值
        bis.reset();                        // pos=2,count=4,mark=2

        data = bis.read();                  // pos=3,count=4,mark=2
        System.out.println((char) data);    // c

        data = bis.read();                  // pos=4,count=4,mark=2
        System.out.println((char) data);    // d

        data = bis.read();                  // pos=4,count=6,mark=2
        System.out.println(data);           // -1

        bis.close();
    }
}

5.1.2 ByteArrayOutputStream

  • ByteArrayOutputStream:该类实现了将数据写入字节数组的输出流。并内置缓冲区,当数据写入缓冲区时,缓冲区会自动增长。
    • 相关成员变量:
      • buf[]:内置的缓冲区,默认大小32
      • count:缓冲区中有效字节数
    • 相关方法:
      • write(int b):将指定的字节写入此字节数组输出流。
      • write(byte[] b, int off, int len):从指定的字节数组写入 len字节,从偏移量为 off开始,输出到这个字节数组输出流。
      • public void reset():将此字节数组输出流的count字段重置为零,以便丢弃输出流中当前累积的所有输出。 可以再次使用输出流,重用已经分配的缓冲区空间。
      • public int size():返回缓冲区的当前大小。
      • public String toString():使用平台的默认字符集将缓冲区内容转换为字符串解码字节。
      • public String toString(String charsetName):通过使用命名的charset解码字节将缓冲区的内容转换为字符串。
      • public byte[] toByteArray():创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中。
      • public void writeTo(OutputStream out):将此字节数组输出流的完整内容写入指定的输出流参数

– 测试代码:

package com.dfbz.demo01_字节数组流;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_ByteArrayOutputStream {
    public static void main(String[] args) throws Exception {
        // 创建字节数组输出流
        ByteArrayOutputStream bos = new ByteArrayOutputStream(100);     // 内置byte[]缓冲区,默认大小为32

        // 写出一个字节到缓冲区
        bos.write(97);

        // 写出一个字节数组
        bos.write("你好".getBytes());

        // 根据偏移量来写出数据
        bos.write("abcd".getBytes(), 0, 2);          // ab

        // 获取缓冲区中的数据(0~count的数据)
        byte[] data = bos.toByteArray();            // aabche

        System.out.println(new String(data, 0, data.length));

        // 将0~count之间的数据转换为字符串
        System.out.println(bos.toString());         // 根据平台默认的编码表将字节转换为字符串

        System.out.println(bos.toString("UTF-8"));      // 指定编码表将字节数据转换为字符串

        bos.reset();            // 将count置为0

        System.out.println(bos.toString());         // count已经变为0,因此输出空

        // 重新写入数据到内置的字节数组
        bos.write("大家好".getBytes());

        bos.writeTo(new FileOutputStream("00.txt"));
    }
}

5.2 数据流

5.2.1 数据流概述

文本格式的数据对于人类而言显得很方便,但是它并不像以二进制格式传递数据那样高效。数据流能更精确的操作Java中的数据类型并以适当的方式将Java基本数据类型输出或输入,数据流提供了存取所有Java基本数据类的方法;

由于IO操作数据的时候,操作的都是字节,即使写出的是一个int数,但真正写出的却是这个int数的字节,数据流能很好的指定写出的数据类型,由于写出的是Java的数据类型,所以在文件上写出的是乱码,但程序能读懂

Tips:数据输入流允许应用程序从底层输入流读取原始Java数据类型。

  • 使用普通字节流的问题:
package com.dfbz.demo02_数据流;

import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_普通字节流的问题 {
    public static void main(String[] args) throws Exception{

        FileOutputStream fos=new FileOutputStream("01.txt");

        // 向文件写出一个字节
        fos.write(800);
        fos.close();

        /*
            由于write写出的时候默认转成字节(二进制)
            int为4个字节 前面会补0  最后写出的时候只会写出一个字节
            997超过了一个字节的取值范围 会将超过一个字节的数砍掉  保留后8位
            997的二进制是: 00000000 00000000 00000011 00100000
            最后将 00100000 保存到了文件
            而 00100000 并没有被编码表编码,没有对应的字符,因此在文件中显示乱码
         */

        FileInputStream fis=new FileInputStream("01.txt");

        // 将读取到的字节转换为int数
        int x=fis.read();
        System.out.println(x); 	            //  打印 00100000 的十进制  32
    }
}

在针对精确数据类型的操作时,普通流将会变得非常不方便;

5.2.2 数据流的使用

Java中数据流分为数据输入流和数据输出流,分别对应DataInputStreamDataOutputStream;数据流能很好的针对精确的数据进行读取或写出;

DataInputStream类中提供有如下方法:

  • public boolean readBoolean():从输入流中读取一个布尔值
  • public byte readByte():从输入流中读取一个Byte值
  • public short readShort():从输入流中读取一个Byte值
  • public char readChar():从输入流中读取一个Char值
  • public int readInt():从输入流中读取一个Integer值
  • public long readLong():从输入流中读取一个Long值
  • public float readFloat():从输入流中读取一个Float值
  • public double readDouble():从输入流中读取一个Double值

  • 数据流测试:
package com.dfbz.demo02_数据流;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_数据流测试 {
    public static void main(String[] args) throws Exception{

        // 创建字节输出流
        DataOutputStream dos=new DataOutputStream(new FileOutputStream("01.txt"));

        // 创建字节输入流
        DataInputStream dis=new DataInputStream(new FileInputStream("01.txt"));

        // 写出3个int数
        dos.writeInt(800);
        dos.writeInt(801);
        dos.writeInt(802);

        /*
         * writeInt方法写出的是一个int数  为4个字节  那么文件此时大小为4*3=12个字节
         * 此时存到文件上的还是乱码   但是程序能读的懂
         */

        int x=dis.readInt();
        System.out.println(x); 		    // 800

        int y=dis.readInt();
        System.out.println(y);          // 801

        int z=dis.readInt();
        System.out.println(z);          // 802

        dos.close();
        dis.close();
    }
}

5.3 序列流

  • 序列流(SequenceInputStream ):序列流用于关联多个输入流,它从一个有序的输入流集合开始,从第一个读取到文件的结尾,然后从第二个文件读取,依此类推,直到最后一个输入流达到文件的结尾。

比如现在有三个文件【1.txt】、【2.txt】、【3.txt】;现在要把这三个文件按照1、2、3的顺序合并成一个文件输出到 【all.txt】文件中。如果不知道有这个流,大家可能都是自己一个一个文件的去读,自己合并到一个文件中。有了这个流,我们操作起来,代码更加优雅。


  • 测试代码:
package com.dfbz.demo03_序列流;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.SequenceInputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_序列流的使用 {
    public static void main(String[] args) throws Exception {
        FileInputStream fis1 = new FileInputStream("./a.txt");
        FileInputStream fis2 = new FileInputStream("./b.txt");

        FileOutputStream fos = new FileOutputStream("./c.txt");

        // 序列流将a.txt,b.txt依次写入c.txt
        SequenceInputStream sis = new SequenceInputStream(fis1, fis2);

        int data;
        while ((data = sis.read()) != -1) {
            fos.write(data);
        }
        fos.close();
        sis.close();
    }
}
  • 使用Enumeration来关联输入流:
package com.dfbz.demo03_序列流;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.SequenceInputStream;
import java.util.Enumeration;
import java.util.Vector;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_使用Enumeration关联输入流 {
    public static void main(String[] args) throws Exception {
        FileInputStream f1 = new FileInputStream("./a.txt");
        FileInputStream f2 = new FileInputStream("./b.txt");
        FileInputStream f3 = new FileInputStream("./c.txt");
        FileInputStream f4 = new FileInputStream("./d.txt");

        FileOutputStream o1 = new FileOutputStream("./test.txt");

        // 将所要读取的文件全部添加到集合中
        Vector<FileInputStream> v = new Vector();

        v.add(f1);
        v.add(f2);
        v.add(f3);
        v.add(f4);

        Enumeration<FileInputStream> fisList = v.elements();

        //返回此集合的枚举,并放入SequenceInputStream构造
        SequenceInputStream sis = new SequenceInputStream(fisList);

        int len;
        while ((len = sis.read()) != -1) {       // 全部写入test.txt
            o1.write(len);
        }
        o1.close();
        sis.close();
    }
}

5.4 回退流

  • 回退流(PushbackInputStream):在JAVA IO中所有的数据都是采用顺序的读取方式,即对于一个输入流来讲都是采用从头到尾的顺序读取的,如果在输入流中某个不需要的内容被读取进来,我们就要采取办法把不需要的数据处理掉,为了解决这样的处理问题,在JAVA中提供了一种回退输入流(PushbackInputStream),可以把读取进来的某些数据重新回退到输入流的缓冲区之中。

回退流中提供的方法如下:

  • public PushbackInputStream(InputStream in):构造方法 将输入流放入到回退流之中。
  • public int read() throws IOException:普通 读取数据。
  • public int read(byte[] b,int off,int len) throws IOException:普通方法 读取指定范围的数据。
  • public void unread(int b) throws IOException:普通方法 回退一个数据到缓冲区前面。
  • public void unread(byte[] b) throws IOException:普通方法 回退一组数据到缓冲区前面。
  • public void unread(byte[] b,int off,int len) throws IOException:普通方法 回退指定范围的一组数据到缓冲区前面。

回退流测试:

package com.dfbz.demo04_回退流;

import java.io.ByteArrayInputStream;
import java.io.PushbackInputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01_回退流测试 {
    public static void main(String[] args) throws Exception {
        String str = "123";

        PushbackInputStream pis = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        int data = pis.read();                       // 1
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 2
        System.out.println((char) data);

        pis.unread("-".getBytes());                 // 回退到流中
        data = pis.read();
        System.out.println((char) data);            // -

        data = pis.read();                          // 3
        System.out.println((char) data);
    }
}
  • 回退流小案例:

需求:在一段数字中截取手机号(11位)

package com.dfbz.demo04_回退流;

import java.io.ByteArrayInputStream;
import java.io.PushbackInputStream;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo02_回退流小案例 {
    public static void main(String[] args) throws Exception {
        String str = "13079016067131350454961897434920916552444009";

        PushbackInputStream push = new PushbackInputStream(new ByteArrayInputStream(str.getBytes()));    // 从内存中读取数据

        System.out.println("读取之后的数据为:");

        int data = 0;
        int count = 1;

        while ((data = push.read()) != -1) {    // 读取内容

            System.out.print((char) data);
            if (count % 11 == 0) {                          // 读完了一个手机号
                push.unread("-".getBytes());                // 放回到缓冲区之中
                data = push.read();    // 再读一遍
                System.out.print((char) data);
            }

            count++;
        }

        System.out.println();
    }
}

5.5 管道流

管道输入与输出实际上使用的是一个循环缓冲数组来实现,这个数组默认大小为1024字节。输入流PipedInputStream从这个循环缓冲数组中读数据,输出流PipedOutputStream往这个循环缓冲数组中写入数据。当这个缓冲数组已满的时候,输出流PipedOutputStream所在的线程将阻塞;当这个缓冲数组首次为空的时候,输入流PipedInputStream所在的线程将阻塞。

  • 测试代码:
package com.dfbz.demo05_管道流;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Scanner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
public class Demo01 {
    public static void main(String[] args) throws Exception{
//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream();

        // 将管道输入流和输出流进行关联
//        pis.connect(pos);           // 输入流连接输出流
//        pos.connect(pis);           // 输出流连接输入流(效果同上)

//        PipedInputStream pis = new PipedInputStream();
//        PipedOutputStream pos = new PipedOutputStream(pis);         // 创建管道输出流的时候就关联好输入流

        PipedOutputStream pos = new PipedOutputStream();
        PipedInputStream pis = new PipedInputStream(pos);           // 创建管道输入流的时候就关联好输出流

        // 写线程(往管道流中写出数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    Scanner scanner = new Scanner(System.in);
                    while (true) {
                        // 接收键盘输入的数据
                        String str = scanner.next();

                        // 往管道流中写
                        pos.write(str.getBytes());
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

        // 读线程(从管道流里面读取数据)
        new Thread() {
            @Override
            public void run() {
                try {
                    int len;
                    byte[] data = new byte[1024];

                    // 从管道流中读取数据
                    while ((len = pis.read(data)) != -1) {
                        System.out.println("管道输出流来数据啦:" + new String(data, 0, len));
                    }
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
            }
        }.start();

    }
}

上一篇14【IO流基础】

下一篇16【TCP、UDP、网络通信】

目录【JavaSE零基础系列教程目录】


版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/131770.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!