Java进阶知识学习:I/O流详细总结

目录,更新ing,学习Java的点滴记录

  目录放在这里太长了,附目录链接大家可以自由选择查看--------Java学习目录

IO流引入

  1. 对于程序语言的设计者来说,创建一个好的输入/输出(I/O)系统是一项非常艰难的任务
  2. 现有的大量不同方案已经说明了这一点,如果你是一个使用Java时间比较长的Coder,就能感觉到I/O系统涵盖了很多方面,如各种I/O源端和与之通信的接收端(文件,控制台,网络连接等),而且还需要以多种不同的方式与它们进行通信(顺序,随机存取,缓冲,二进制,按字符,按行,按字等等)
    Java进阶知识学习:I/O流详细总结_第1张图片
  3. Java类库的设计者一开始通过创建大量的类来解决这个问题.一开始,可能会对Java I/O系统提供如此多的类而感到不知所措(有学过I/O的小伙伴应该能深刻感受到各种InputStream,OutputStream,Reader等等,让人眼花撩换,这些后面会一一详细解释),具有讽刺意味的是,Java I/O设计的初衷是为了避免过多的类.Java 1.0版本以来,Java的I/O类库发生了明显改变,在原来面向字节的类中添加了面向字符和基于Unicode的类.在JDK1.4后,添加了NIO类,添加进来是为了改善性能和功能.因此,在充分理解Java I/O系统以便正确运行之前,我们需要学习相当数量的类.
  4. 对于I/O类库的演化过程还是有必要了解一下的,毕竟,如果缺乏历史的眼光,很快我们就会对什么使用该使用哪些类,以及什么使用不该使用它们而感到迷惑

IO流的基本概念

  1. 流大表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象.流屏蔽了实际的I/O设备中处理数据的细节.
  2. 在JavaAPI中,I/O流类库一般都位于java.io包中,可以从其中读入一个字节序列的对象称为输入流,而可以向其中写入一个字节序列的对象称为输出流.这些字节序列的来源地和目的地可以是文件(绝大多数),也可以是网络连接,内存块等.
  3. Java类库的I/O类分成输入和输出两部分,有一点需要说明一下,输入输出是相对于程序而言,而不是相对于数据源或者目的地
     假设要读取a.txt文件内存,然后输出到b.txt文件中,错误的想法是:我要把a.txt文件内容取出来,然后再输入到b.txt里面,那岂不是从a.txt中拿数据应该是输出流,往b.txt文件里面放数据是输入流吗?这样想就错了.
     可以参照下图,我们要把程序当成判断输入输出的标准,从a.txt取出数据到程序这个中转站中,这是对程序的输入,从程序汇总对数据进行处理后放入b.txt是输出流,所以千万不要搞混了
    Java进阶知识学习:I/O流详细总结_第2张图片
  4. 前面提到的输入输出流的名称可以看成是根据流的方向来划分的,此外根据功能的不同也可以分为节点流和处理流(个人感觉了解一下就行,不算非常重要)
     1) 节点流:可以直接从数据源或目的地读写数据
     2) 处理流:不直接连接数据源或目的地,是通过其他流封装,目的主要是简化操作和提高性能,就比如水从源头要经过自来水处理厂然后再到我们自己家
    在这里插入图片描述

流体系的四大抽象类

  1. 整个Java I/O流体系可以说是由四大抽象类来决定的
    Java进阶知识学习:I/O流详细总结_第3张图片
  2. 说明
     1) 这四个都是抽象类,不能创建实例,只能使用它们的子类
     2) 抽象类InputStream和OutputStream是构成面向字节的输入/输出类层次结构的基础,其他所有字节输入输出流类都继承自这两个基类
     3) 抽象类Reader和Writer是构成面向字符的输入/输出类层次结构的基础,其他所有字符输入输出流类都继承自这两个基类
     4) 继承自InputStream和Reader的类都含有read()的基本方法,用于读取单个字节或者字节数组;继承自OutputStream和Writer的类都含有write()的基本方法,用于写出单个字节或字节数组.
  3. 划分原因
     因为面向字节的流不方便处理以Unicode形式存储的信息,Unicode中每个字符使用2个字节来表示,所以从抽象类Reader和Writer继承出来一个专门用来处理Unicode字符的单独的类层次结构.这些类拥有的读入和写出操作都是基于两字节的Char值,而不是基于byte值的.
  4. 但是这4个类的存在意义其实并不在于频繁使用它们,而是因为别的类可以使用它们,以便提供更加有用的接口.因此,我们很少使用单一的类来创建流对象,而是通过叠加多个对象来提供所期望的功能(这就是装饰器设计模式),相信这一点对于使用过I/O流的小伙伴就不陌生了,创建流对象的构造方法的参数还是一个流对象等等,这样的嵌套太常见了,初学者小白也不必慌,后面都会一一介绍.这也是JavaI/O流让人迷惑的一个原因:创建单一的结果流,却需要创建多个对象
  5. 下面几个小节就分别针对这两个体系结构进行说明,同时为了让大家更加直观的感受一下四大抽象类的继承结构,我手动画了尽可能详细继承结构,虽然看起来很多,别慌,我们常用的还是有限的,分享给大家(如果看不清的话,建议右键--查看图像,因为截图可能显得比较小)

InputStream和OutputStream

  1. 类层次结构
    Java进阶知识学习:I/O流详细总结_第4张图片
    Java进阶知识学习:I/O流详细总结_第5张图片
  2. 定义的方法
     1) InputStream
    Java进阶知识学习:I/O流详细总结_第6张图片
    Java进阶知识学习:I/O流详细总结_第7张图片
     2) OutputStream
    Java进阶知识学习:I/O流详细总结_第8张图片

Reader和Writer

  1. 类层次结构
    Java进阶知识学习:I/O流详细总结_第9张图片
  2. 定义的方法
     1) Reader
    Java进阶知识学习:I/O流详细总结_第10张图片
     2) Writer
    Java进阶知识学习:I/O流详细总结_第11张图片

文件字节流

  1. FileInputStream和FileOutputStream可以提供对于磁盘文件的输入流和输出流,我们只需要向投早期提供文件名(相对路径)或文件的完整路径名(绝对路径)
     1) 关于I/O中的相对路径:所在在java.io中的类都将相对路径解释为从用户工作目录开始,可以通过调用System.getproperty("user.dir")获得具体位置
     2) 关于路径的书写:由于反斜杠字符在Java字符串中是转义字符,因此要确保在Windows风格的路径名中使用\\(比如:c:\test\test.txt),因为大部分Windows文件处理的系统调用都会将斜杠解释成文件分隔符.但是最好获取一下自己使用的平台上的文件分割符,可以使用下面这种方式打印输出查看一下
      Java进阶知识学习:I/O流详细总结_第12张图片
  2. 构造方式
     1) FileInputStream
     可以使用字符串类型的文件名来创建或者使用一个文件对象来创建
    Java进阶知识学习:I/O流详细总结_第13张图片
     2) FileOutputStream
     可以使用字符串类型的文件名来创建或者使用一个文件对象来创建
    Java进阶知识学习:I/O流详细总结_第14张图片
  3. 同InputStream和OutputStream一样,只支持字节级别的读写.只能读入字节和字节数组.
  4. 常见方法
     1) FileInputStream
    Java进阶知识学习:I/O流详细总结_第15张图片
     2) FileOutputStream
    Java进阶知识学习:I/O流详细总结_第16张图片
  5. 示例—文件复制
    Java进阶知识学习:I/O流详细总结_第17张图片

文件字符流

  1. 介绍
     FileReader类从InputStreamReader类继承而来。该类按字符读取流中数据。
     FileWriter 类从 OutputStreamWriter 类继承而来。该类按字符向流中写入数据。
  2. 构造方法
     1) FileReader
     常用的:从给定文件的路径下创建和从一个File对象来创建
    Java进阶知识学习:I/O流详细总结_第18张图片
     2) FileWriter
    Java进阶知识学习:I/O流详细总结_第19张图片
  3. 常用方法
     1) FileReader
    Java进阶知识学习:I/O流详细总结_第20张图片
     2) FileWriter
    Java进阶知识学习:I/O流详细总结_第21张图片
  4. 示例
    Java进阶知识学习:I/O流详细总结_第22张图片 如果你发现你的输出文件出现乱码,可以使用EditPlus之类的文本编辑器打开一下你的原文件,另存为一下选择UTF-8的格式,再尝试一下就可以了

缓冲字节流

  1. BufferedInputStream和BufferedOutputStream
     前面提到的FileInputStream和FileOutputStream都属于节点流
     BufferedInputStream和BufferedOutputStream都是处理流,是对FileInputStream和FileOutputStream的增强
  2. 特点
     1) 读文件和写文件都使用了缓冲区,减少了读写次数,从而提高了效率
     2)当创建这两个缓冲流的对象时,会创建了内部缓冲数组,使用8k(8192/1024)大小的缓冲区
      在这里插入图片描述  在这里插入图片描述
      
     3)当读取数据时,数据按块读入缓冲区,其后的操作则直接访问缓冲区。
     4)当写入数据时,首先写入缓冲区,当缓冲区满时,其中的数据写入所连接的输出流。使用方法flush()可以强制将缓冲区的内容全部写入输出流。
     5)关闭流的顺序和打开流的顺序相反,只要关闭高层流即可,关闭高层流同时也会关闭的底层节点流
  3. 对比InputStream和OutputStream的效率
    Java进阶知识学习:I/O流详细总结_第23张图片
    Java进阶知识学习:I/O流详细总结_第24张图片
    Java进阶知识学习:I/O流详细总结_第25张图片
     我测试的文件还比较小,文件大一些会更明显

缓冲字符流

  1. BufferedReader和BufferedWriter
     对FileReader以及FileWriter的读写操作进行增强
     同样也是使用了缓冲机制,加快了读写速度
    Java进阶知识学习:I/O流详细总结_第26张图片
    Java进阶知识学习:I/O流详细总结_第27张图片 和前面一样也是同样大小的缓冲区,读取文本文件时,会先尽量从文件中读入字符数据并放满缓冲区,而之后若使用read()方法,会先从缓冲区中进行读取。如果缓冲区数据不足,才会再从文件中读取,使用BufferedWriter时,写入的数据并不会先输出到目的地,而是先存储至缓冲区中。如果缓冲区中的数据满了,才会一次对目的地进行写出
  2. 对比FileReader和FileWriter的效率
    Java进阶知识学习:I/O流详细总结_第28张图片
    Java进阶知识学习:I/O流详细总结_第29张图片
    Java进阶知识学习:I/O流详细总结_第30张图片
     对比前面缓冲字符流的结果,我前后使用的都是一个文件,但是发现在测试docx格式文件的时候,字符流比字节流慢很多,但是当测试纯文本文件是,字符流比字节流块,这里就不再贴案例了
     所以说,如果是纯文本,推荐使用字符流,提高读取效率和写入效率可以使用,其他情况都可以使用字节流来完成

转换流

  1. InputStreamReader和OutputStreamWriter
  2. 它们都是一种处理流,用于将字节流转化成字符流,字符流与字节流之间的桥梁
  3. InputStreamReader的作用是把InputStream转换成Reader;OutputStreamWriter的作用是把OutputStream转换成Writer
  4. 源码分析
// 将InputStream转为Reader
public class InputStreamReader extends Reader {

    private final StreamDecoder sd;

	// 使用默认的字符编码集创建一个InputStreamReader
    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            // The default encoding should always be available
            throw new Error(e);
        }
    }

    // 使用指定的字符编码集(名称)和InputStream创建一个InputStreamReader
    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }

    // 使用指定的字符编码类和InputStream创建一个InputStreamReader
    public InputStreamReader(InputStream in, Charset cs) {
        super(in);
        if (cs == null)
            throw new NullPointerException("charset");
        sd = StreamDecoder.forInputStreamReader(in, this, cs);
    }

    // 使用指定的解码器dec和InputStream创建
    public InputStreamReader(InputStream in, CharsetDecoder dec) {
        super(in);
        if (dec == null)
            throw new NullPointerException("charset decoder");
        sd = StreamDecoder.forInputStreamReader(in, this, dec);
    }

    // 返回使用的编码类型
    public String getEncoding() {
        return sd.getEncoding();
    }

    //读取一个字符
    public int read() throws IOException {
        return sd.read();
    }

    //将数据写入一个字符数组,cbuf中写入开始位置为offset,写入长度lenght
    public int read(char cbuf[], int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }

    // 返回该流是否可以被读取
    public boolean ready() throws IOException {
        return sd.ready();
    }

	//关闭流
    public void close() throws IOException {
        sd.close();
    }
}
// 将OutputStream转为Writer
public class OutputStreamWriter extends Writer {

    private final StreamEncoder se;

    // 使用指定名称的编码集和OutputStream创建
    public OutputStreamWriter(OutputStream out, String charsetName)
        throws UnsupportedEncodingException
    {
        super(out);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
    }

    // 使用默认编码集和OutputStream创建
    public OutputStreamWriter(OutputStream out) {
        super(out);
        try {
            se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
        } catch (UnsupportedEncodingException e) {
            throw new Error(e);
        }
    }

    // 使用指定的Charset类型和OutputStream创建
    public OutputStreamWriter(OutputStream out, Charset cs) {
        super(out);
        if (cs == null)
            throw new NullPointerException("charset");
        se = StreamEncoder.forOutputStreamWriter(out, this, cs);
    }

    // 使用CharsetEncoder类型的编码器和OutputStream创建
    public OutputStreamWriter(OutputStream out, CharsetEncoder enc) {
        super(out);
        if (enc == null)
            throw new NullPointerException("charset encoder");
        se = StreamEncoder.forOutputStreamWriter(out, this, enc);
    }

    // 获取当前的编码类型
    public String getEncoding() {
        return se.getEncoding();
    }

    // 刷新缓冲区,将缓冲区中数据输出
    void flushBuffer() throws IOException {
        se.flushBuffer();
    }

    // 输出一个字符
    public void write(int c) throws IOException {
        se.write(c);
    }

    // 将cbuf字符数组从off位置开始,写出len长度的字符
    public void write(char cbuf[], int off, int len) throws IOException {
        se.write(cbuf, off, len);
    }

    //刷新输出流
    public void flush() throws IOException {
        se.flush();
    }

	//关闭流
    public void close() throws IOException {
        se.close();
    }
}
  1. 示例
    Java进阶知识学习:I/O流详细总结_第31张图片

打印流

  1. 介绍
     字节打印流:PrintStream 字符打印流 PrintWriter
  2. PrintStream特点
     1)PrintStream提供了一系列的print()和println(),可以实现将基本数据类型格式化成字符串输出。对象类型将先调用toString(),然后输出该方法返回的字符串
     2)System.out就是PrintStream的一个实例,代表显示器
     3)System.err也是PrintStream的一个实例,代表显示器
     4)PrintStream的输出功能非常强大,通常需要输出文本内容,都可以将输出流包装秤PrintStream后进行输出
     5)PrintStream的方法都不抛出IOException
     6)其实我们平时用的System.out.println,源码上就是调用的PrintStream的println方法
  3. PrintWriter特点
     1)PrintStream的对应字符流,功能相同,方法对应。
     2)PrintWriter的方法也不抛出IOException
     3)复制文件时可以使用PrintWriter代替BufferedWriter完成,更简单
  4. PrintStream示例
public static void main(String[] args) {
        try{
            //1. 创建一个字节输出流
            FileOutputStream fos = new FileOutputStream("e:\\test.txt");
            //2. 构建缓冲流提高效率
            BufferedOutputStream bs = new BufferedOutputStream(fos);
            //3.构建字节打印流
            PrintStream ps = new PrintStream(bs);
            //4. 进行输出
            ps.println("学习打印流");
            ps.println("学习Java");
            ps.println(2020);
            ps.close();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
  1. PrintWriter示例
public static void main(String[] args) {
        try {
            //1. 构建一个字符输出流  
            Writer os = new FileWriter("e:\\test.txt");
            //2. 构建缓冲流,提高效率  
            BufferedWriter bos = new BufferedWriter(os);
            //3. 构建字符打印流  
            PrintWriter ps = new PrintWriter(bos);
            // 数据输出
            ps.println(false);//写入boolean型  
            ps.println("好好学习,天天向上");//写入字符串  
            ps.println(3);//写入int类型  
            //关闭流  
            ps.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

数据流

  1. 介绍
     DataInputStream和DataOutputStream
     提供了可以存取所有java基础类型数据(如int ,double等)和String的方法
     处理流,只针对字节流,二进制文件
  2. 常见的流叠加流程
    Java进阶知识学习:I/O流详细总结_第32张图片
  3. 写入示例
    Java进阶知识学习:I/O流详细总结_第33张图片 此时,我们打开输出的文件,发现内容我们看不懂.原因在于使用数据流输出后的数据是以二进制形式保存的,但是使用数据流重新读该文件是可以重新拿到数据的
      Java进阶知识学习:I/O流详细总结_第34张图片
  4. 读取示例
    Java进阶知识学习:I/O流详细总结_第35张图片
    Java进阶知识学习:I/O流详细总结_第36张图片

对象流及序列化和反序列化

  1. 介绍
     1) ObjectOutputStream—>写对象—>序列化(对象的内存状态以字节的形式存储到磁盘的文件上)
     2) ObjectInputStream—>读对象—>反序列化(磁盘上的字节形式的数据还原成对象的内存状态)
     3) 序列化----将java对象转换成字节序列(IO字节流)
     4)对象反序列化–从字节序列中恢复java对象
  2. 为啥要进行序列化
     序列化以后的对象可以保存到磁盘上,也可以在网络上传输,使得不同的计算机可以共享对象。(序列化的字节序列是平台无关的)
  3. 序列化的条件
     只有实现了Serializable接口的类的对象才可以被序列化。Serializable接口中没有任何的方法,实现该接口的类不需要实现额外的方法。如果对象的属性是对象,属性对应类也必须实现Serializable接口
  4. 序列化和反序列化的步骤
     1) 要序列化一个对象,首先要创建某些OutputStream对象,然后将其封装在一个ObjectOutputStream对象内.这时,只需要调用writeObject()就可以将其序列化,并将其发送给OutputStream
     2) 要反序列化,需要将一个InputStream封装在ObjectInputStream内,然后调用readObject().最后获得一个引用,它指向一个向上转型的Object,所以必须向下转型才能进一步使用
  5. 使用序列化和反序列化写入和读取对象
public class TestSerializable {
    public static void main(String[] args) throws Exception {
        //写入对象
        // 1. 创建一个文件输出流
        FileOutputStream fos = new FileOutputStream("e:\\test.txt");
        // 2. 这里就直接将fos放入对象流里面了,省略了包装为缓冲流之类的做法
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        // 3.写对象
        oos.writeObject(new Person(1,"root"));
        //4.关闭流
        if (oos!=null)
            oos.close();


        // 读取对象
        //1. 创建一个文件输入流
        FileInputStream fis = new FileInputStream("e:\\test.txt");
        //2.创建对象输入流
        ObjectInputStream ois = new ObjectInputStream(fis);
        //3.读对象  默认得到的Object类型,需要做强制类型转换
        Person person = (Person)ois.readObject();
        //4. 关闭流
        if (ois!=null)
            ois.close();
        System.out.println(person.toString());
    }
}

class Person implements Serializable{
    private int id;
    private String name;

    public Person() {
    }

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public void setId(int id) {
        this.id = id;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}
  1. 序列化能保存的元素
     1)只能保存对象的非静态成员变量
     2)不能保存任何成员方法和静态的成员变量
     3)不保存transient成员变量
     4)如果一个对象的成员变量是一个对象,这个对象的成员变量也会保存
     5)串行化保存的只是变量的值,对于变量的任何修饰符,都不能保存
  2. 注意点
     1) 使用对象流把一个对象写到文件时不仅保证该对象是序列化的,而且如果该对象中保存的其他对象的引用的话,那么对应的类也必须是可序列化的也必须是可序列化的。
     2) 如果一个可序列化的对象包含对某个不可序列化的对象的引用,那么整个序列化操作将会失败,并且会抛出一个NotSerializableException,我们可以将这个引用标记为transient,那么对象仍然可以序列化。
  3. 一个序列化失败的案例
     对Person类进行序列化,但是Person类中持有一个Address类对象,但是Address类并没有实现Serializable接口
     修改的代码地方不多,就根据上面给的代码改改就可以,控制台报错信息就是Address类没有实现该接口
    Java进阶知识学习:I/O流详细总结_第37张图片Java进阶知识学习:I/O流详细总结_第38张图片
    Java进阶知识学习:I/O流详细总结_第39张图片
  4. 对象进行序列化时出现的情况
     1) 同一个对象多次序列化
      所有保存到磁盘中的对象都有一个序列化编号
      序列化一个对象中,首先检查该对象是否已经序列化过,如果没有,进行序列化;如果已经序列化,将不再重新序列化,而是输出编号即可
     2) 如果不希望类的某些属性被序列化,该怎么做?
      为属性添加transient关键字(完成排除在序列化之外)
      自定义序列化(不仅可以决定哪些属性不参与序列化,还可以定义属性具体如何序列)
  5. 最后一个就是说一下,可能出现的序列化版本不兼容的问题,一句话描述一下:假设有一个Person类,有id和name属性,首先对其进行了序列化,在反序列化之前,给Person类添加了一个address属性,然后进行反序列化,这样程序就会报错
     解决方案:为进行序列化的类指定序列化版本号,生成方式就不占用这里的篇幅了,具体见(IDEA中为类生成序列化号),需要注意一点:依旧是类内部有其他对象引用,比如Person类中有一个Student类对象,那么该类对象也得去生成序列化号

你可能感兴趣的:(Java)