灵魂一击!详解Java中的IO输入输出流

什么是流?流表示任何有能力产生数据的数据源对象或者是有能力接收数据的接收端对象,它屏蔽了实际的I/O设备中处理数据的细节。html

IO流是实现输入输出的基础,它能够很方便地实现数据的输入输出操做,即读写操做。java

本片要点

  • 介绍流的定义和基本分类。
  • 介绍文件字符流、字节流、转换流、合并流、打印流等使用。
  • 介绍序列化的意义。
  • 介绍两种自定义序列化方式。

基本分类

  • 根据方向输入流:数据从外部流向程序,例如从文件中读取数据输出流:数据从程序流向外部,例如向文件中写数据
  • 根据形式字符流:字符类文件,【如 txt、 java、 html】,操做16位的字符。字节流:【图片、视频、音频】 ,操做8位的字节。
  • 根据功能节点流:直接从/向数据源【如磁盘、网络】进行数据读写处理流:封装其余的流,来提供加强流的功能。
灵魂一击!详解Java中的IO输入输出流

 

  • 上面四大基本流都是抽象类,都不能直接建立实例对象。
  • 数据的来源/目的地:磁盘、网络、内存、外部设备。

发展史

  • java1.0版本中,I/O库中与输入有关的全部类都将继承InputStream,与输出有关的全部类继承OutputStream,用以操做二进制数据。
  • java1.1版本对I/O库进行了修改:在原先的库中新增了新类,如ObjectInputStreamObjectOutputStream。增长了Reader和Writer,提供了兼容Unicode与面向字符的I/O功能。在Reader和Writer类层次结构中,提供了使字符与字节相互转化的类,OutputStreamWriterInputStreamReader
  • 两个不一样的继承层次结构拥有类似的行为,它们都提供了读(read)和写(write)的方法,针对不一样的状况,提供的方法也是相似的。
  • java1.4版本的java.nio.*包中引入新的I/O类库,这部分之后再作学习。

文件字符流

  • 文件字符输出流 FileWriter自带缓冲区,数据先写到到缓冲区上,而后从缓冲区写入文件。
  • 文件字符输入流 FileReader:没有缓冲区,能够单个字符的读取,也能够自定义数组缓冲区。

输出的基本结构

在实际应用中,异常处理的方式都须要按照下面的结构进行,本篇为了节约篇幅,以后都将采用向上抛出的方式处理异常。linux

//将流对象放在try以外声明,并附为null,保证编译,能够调用close
    FileWriter writer = null;
    try {
        //将流对象放在里面初始化
        writer = new FileWriter("D:\\b.txt");
        writer.write("abc");
        
        //防止关流失败,没有自动冲刷,致使数据丢失
        writer.flush();
        
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //判断writer对象是否成功初始化
        if(writer!=null) {
            //关流,不管成功与否
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                //不管关流成功与否,都是有意义的:标为垃圾对象,强制回收
                writer = null;
            }
        }
    }
  • 并不会直接将数据写入文件中,而是先写入缓冲区,待缓冲区满了以后才将缓冲区的数据写入文件。
  • 假设数据写入缓冲区时且缓冲区还没满,数据还没可以写入文件时,程序就已经结束,会致使数据惨死缓冲区,这时须要手动冲刷缓冲区,将缓冲区内的数据冲刷进文件中。writer.flush();
  • 数据写入完毕,释放文件以容许别的流来操做该文件。关闭流能够调用close()方法,值得注意的是,在close执行以前,流会自动进行一次flush的操做以免数据还残存在缓冲区中,但这并不意味着flush操做是多余的。

流中的异常处理

  • 不管流操做成功与否,关流操做都须要进行,因此须要将关流操做放到finally代码块中
  • 为了让流对象在finally中依然可以使用,因此须要将流对象放在try以外声明而且赋值为null,而后在try以内进行实际的初始化过程。
  • 在关流以前要判断流对象是否初始化成功,实际就是判断流对象是否为nullwriter!=null时才执行关流操做。
  • 关流可能会失败,此时流依然会占用文件,因此须要将流对象置为null,标记为垃圾对象进行强制回收以释放文件。
  • 若是流有缓冲区,为了防止关流失败致使没有进行自动冲刷,因此须要手动冲刷一次,以防止有数据死在缓冲区而产生数据的丢失。

异常处理新方式

JDK1.7提出了对流进行异常处理的新方式,任何AutoClosable类型的对象均可以用于try-with-resourses语法,实现自动关闭。面试

要求处理的对象的声明过程必须在try后跟的()中,在try代码块以外。设计模式

try(FileWriter writer = new FileWriter("D:\\c.txt")){
    writer.write("abc");
}catch (IOException e){
    e.printStackTrace();
}

读取的基本结构

public static void main(String[] args) throws IOException {
        FileReader reader = new FileReader("D:\\b.txt");
        //定义数组做为缓冲区
        char[] cs = new char[5];
        //定义一个变量记录每次读取的字符
        int hasRead;
        //读取到末尾为-1
        while ((hasRead = reader.read(cs)) != -1) {
            System.out.println(new String(cs, 0, hasRead));
        }
        reader.close();
    }
  • read方法能够传入字符数组,每次读取一个字符数组的长度。
  • 定义变量m记录读取的字符,以达到末尾为终止条件。m!=-1时,终止循环。
  • 读取结束,执行关流操做。

运用输入与输出完成复制效果

运用文件字符输入与输出的小小案例:数组

public static void copyFile(FileReader reader, FileWriter writer) throws IOException {
    //利用字符数组做为缓冲区
    char[] cs = new char[5];
    //定义变量记录读取到的字符个数
    int hasRead;
    while((hasRead = reader.read(cs)) != -1){
        //将读取到的内容写入新的文件中
        writer.write(cs, 0, hasRead));

    }
    reader.close();
    writer.close();
}

文件字节流

  • 文件字节输出流 FileOutputStream 在输出的时候没有缓冲区,因此不须要进行flush操做。
public static void main(String[] args) throws Exception {
        FileOutputStream out = new FileOutputStream("D:\\b.txt");
        //写入数据
        //字节输出流没有缓冲区
        out.write("天乔巴夏".getBytes());
        //关流是为了释放文件
        out.close();
    }
  • 文件字节输入流 FileInputStream,能够定义字节数组做为缓冲区。
public static void main(String[] args) throws Exception{
        FileInputStream in = new FileInputStream("E:\\1myblog\\Node.png");
       //1.读取字节
       int i;
       while((i = in.read()) ! =-1)
           System.out.println(i);
       //2.定义字节数组做为缓冲区
       byte[] bs = new byte[10];
       //定义变量记录每次实际读取的字节个数
       int len;
       while((len = in.read(bs)) != -1){
           System.out.println(new String(bs, 0, len));
       }
       in.close();

    }

缓冲流

字符缓冲流

  • BufferedReader:在构建的时候须要传入一个Reader对象,真正读取数据依靠的是传入的这个Reader对象BufferedReadReader对象中获取数据提供缓冲区
public static void main(String[] args) throws IOException {
        //真正读取文件的流是FileReader,它自己并无缓冲区
        FileReader reader = new FileReader("D:\\b.txt");
        BufferedReader br = new BufferedReader(reader);
        //读取一行
        //String str = br.readLine();
        //System.out.println(str);

        //定义一个变量来记录读取的每一行的数据(回车)
        String str;
        //读取到末尾返回null
        while((str = br.readLine())!=null){
            System.out.println(str);
        }
        //关外层流便可
        br.close();
    }
  • BufferedWriter:提供了一个更大的缓冲区,提供了一个newLine的方法用于换行,以屏蔽不一样操做系统的差别性
public static void main(String[] args) throws Exception {
        //真正向文件中写数据的流是FileWriter,自己具备缓冲区
        //BufferedWriter 提供了更大的缓冲区
        BufferedWriter writer = new BufferedWriter(new FileWriter("E:\\b.txt"));
        writer.write("天乔");
        //换行: Windows中换行是 \r\n linux中只有\n
        //提供newLine() 统一换行
        writer.newLine();
        writer.write("巴夏");
        writer.close();
    }

装饰设计模式

缓冲流基于装饰设计模式,即利用同类对象构建本类对象,在本类中进行功能的改变或者加强。网络

例如,BufferedReader自己就是Reader对象,它接收了一个Reader对象构建自身,自身提供缓冲区其余新增方法,经过减小磁盘读写次数来提升输入和输出的速度。ide

灵魂一击!详解Java中的IO输入输出流

 

除此以外,字节流一样也存在缓冲流,分别是BufferedInputStreamBufferedOutputStream学习

转换流(适配器)

利用转换流能够实现字符流和字节流之间的转换this

  • OutputStreamWriter
public static void main(String[] args) throws Exception {
        //在构建转换流时须要传入一个OutputStream 字节流
        OutputStreamWriter ow = 
                new OutputStreamWriter(
                        new FileOutputStream("D:\\b.txt"),"utf-8");
        //给定字符--> OutputStreamWriter转化为字节-->以字节流形式传入文件FileOutputStream
        //若是没有指定编码,默认使用当前工程的编码
        ow.write("天乔巴夏");
        ow.close();
    }

最终与文件接触的是字节流,意味着将传入的字符转换为字节


  • InputStreamReader
public static void main(String[] args) throws IOException {
        //以字节形式FileInputStream读取,通过转换InputStreamReader -->字符
        //若是没有指定编码。使用的是默认的工程的编码
        InputStreamReader ir = 
                new InputStreamReader(
                        new FileInputStream("D:\\b.txt"));
        char[] cs = new char[5];
        int len;
        while((len=ir.read(cs))!=-1){
            System.out.println(new String(cs,0,len));
        }
        ir.close();
    }

最初与文件接触的是字节流,意味着将读取的字节转化为字符

适配器设计模式

缓冲流基于适配器设计模式,将某个类的接口转换另外一个用户所但愿的类的接口,让本来因为接口不兼容而不能在一块儿工做的类能够在一块儿进行工做。

OutputStreamWriter为例,构建该转换流时须要传入一个字节流,而写入的数据最开始是由字符形式给定的,也就是说该转换流实现了从字符向字节的转换,让两个不一样的类在一块儿共同办事。

灵魂一击!详解Java中的IO输入输出流

 

标准流/系统流

程序的全部输入均可以来自于标准输入,全部输出均可以发送到标准输出,全部错误信息均可以发送到标准错误

标准流分类

灵魂一击!详解Java中的IO输入输出流

 

能够直接使用System.outSystem.err,可是在读取System.in以前必须对其进行封装,例如咱们以前常常会使用的读取输入:Scanner sc = new Scanner(System.in);实际上就封装了System.in对象。

  • 标准流都是字节流
  • 标准流对应的不是类而是对象。
  • 标准流在使用的时候不用关闭。
/** * 从控制台获取一行数据 * @throws IOException readLine 可能会抛出异常 */
    public static void getLine() throws IOException {
        //获取一行字符数据 -- BufferedReader
        //从控制台获取数据 -- System.in
        //System是字节流,BufferedReader在构建的时候须要传入字符流
        //将字节流转换为字符流
        BufferedReader br =
                new BufferedReader(
                        new InputStreamReader(System.in));
        //接收标准输入并转换为大写
        String str = br.readLine().toUpperCase();
        //发送到标准输出
        System.out.println(str);
    }

经过转换流,将System.in读取的标准输入字节流转化为字符流,发送到标准输出,打印显示。

打印流

打印流只有输出流没有输入流

  • PrintStream: 打印字节流
public static void main(String[] args) throws IOException {
        //建立PrintStream对象
        PrintStream p = new PrintStream("D:\\b.txt");
        p.write("abc".getBytes());
        p.write("def".getBytes());
        p.println("abc");
        p.println("def");
        //若是打印对象,默认调用对象身上的toString方法
        p.println(new Object());
        p.close();
    }
  • PrintWriter:打印字符流
//将System.out转换为PrintStream
    public static void main(String[] args) {
        //第二个参数autoFlash设置为true,不然看不到结果
        PrintWriter p = new PrintWriter(System.out,true);
        p.println("hello,world!");
    }

合并流

  • SequenceInputStream用于将多个字节流合并为一个字节流的流。
  • 有两种构建方式:将多个合并的字节流放入一个Enumeration中来进行。传入两个InputStream对象。
  • 合并流只有输入流没有输出流。

以第一种构建方式为例,咱们以前说过,Enumeration能够经过Vector容器的elements方法建立。

public static void main(String[] args) throws IOException {
        FileInputStream in1 = new FileInputStream("D:\\1.txt");
        FileInputStream in2 = new FileInputStream("D:\\a.txt");
        FileInputStream in3 = new FileInputStream("D:\\b.txt");
        FileInputStream in4 = new FileInputStream("D:\\m.txt");

        FileOutputStream out = new FileOutputStream("D:\\union.txt");
        //准备一个Vector存储输入流
        Vector<InputStream> v = new Vector<>();
        v.add(in1);
        v.add(in2);
        v.add(in3);
        v.add(in4);

        //利用Vector产生Enumeration对象
        Enumeration<InputStream> e = v.elements();
        //利用迭代器构建合并流
        SequenceInputStream s = new SequenceInputStream(e);

        //读取
        byte[] bs = new byte[10];
        int len;
        while((len = s.read(bs))!=-1){
            out.write(bs,0,len);
        }
        out.close();
        s.close();
    }

序列化/反序列化流

  • 序列化:将对象转化为字节数组的过程。
  • 反序列化:将字节数组还原回对象的过程。

序列化的意义

对象序列化的目标是将对象保存在磁盘中,或容许在网络中直接传输对象。对象序列化机制容许把内存中的Java对象转换成平台无关的二进制流,从而容许把这种二进制流持久地保存在磁盘上,经过网络将这种二进制流传输到另外一个网络节点。其余程序一旦得到了这种流,均可以将这种二进制流恢复为原来的Java对象。

让某个对象支持序列化的方法很简单,让它实现Serializable接口便可:

public interface Serializable {
}

这个接口没有任何的方法声明,只是一个标记接口,代表实现该接口的类是可序列化的。

咱们一般在Web开发的时候,JavaBean可能会做为参数或返回在远程方法调用中,若是对象不可序列化会出错,所以,JavaBean须要实现Serializable接口。

序列化对象

建立一个Person类。

//必须实现Serializable接口
class Person implements Serializable {
    //序列化ID serialVersionUID
    private static final long serialVersionUID = 6402392549803169300L;
    private String name;
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

建立序列化流,将对象转化为字节,并写入"D:\1.data"。

public class ObjectOutputStreamDemo {
    public static void main(String[] args) throws IOException {
        Person p = new Person();
        p.setAge(18);
        p.setName("Niu");
        //建立序列化流
        //真正将数据写出的流是FileOutputStream
        //ObjectOutputStream将对象转化为字节
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\1.data"));
        out.writeObject(p);
        out.close();
    }
}

建立反序列化流,将从"D:\1.data"中读取的字节转化为对象。

public static void main(String[] args) throws IOException, ClassNotFoundException {
        //建立反序列化流
        //真正读取文件的是FileInputStream
        //ObjectInputStream将读取的字节转化为对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\1.data"));
        //读取数据必须进行数据类型的强制转换
        Person p = (Person)in.readObject();
        in.close();
        System.out.println(p.getName());//Niu
        System.out.println(p.getAge());//18

    }

须要注意的是:

  • 若是一个对象要想被序列化,那么对应的类必须实现接口serializable,该接口没有任何方法,仅仅做为标记使用。
  • statictransient修饰的属性不会进行序列化。若是属性的类型没有实现serializable接口可是也没有用这二者修饰,会抛出NotSerializableException
  • 在对象序列化的时候,版本号会随着对象一块儿序列化出去,在反序列化的时候,对象中的版本号和类中的版本号进行比较,若是版本号一致,则容许反序列化。若是不一致,则抛出InvalidClassException
  • 集合容许被总体序列化 ,集合及其中元素会一块儿序列化出去。
  • 若是对象的成员变量是引用类型,这个引用类型也须要是可序列化的。
  • 当一个可序列化类存在父类时,这些父类要么有无参构造器,要么是须要可序列化的,不然将抛出InvalidClassException的异常。

关于版本号

  • 一个类若是容许被序列化,那么这个类中会产生一个版本号 serialVersonUID。若是没有手动指定版本号,那么在编译的时候自动根据当前类中的属性和方法计算一个版本号,也就意味着一旦类中的属性发生改变,就会从新计算新的,致使先后不一致。可是,手动指定版本号的好处就是,不须要再计算版本号。
  • 版本号的意义在于防止类产生改动致使已经序列化出去的对象没法反序列化回来。版本号必须用static final修饰,自己必须是long类型。

自定义序列化的两种方法

Serializable自定义

// 实现writeObject和readObject两个方法
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;
    private int age;

    // 将name的值反转后写入二进制流
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }

    // 将读取的字符串反转后赋给name
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

还有一种更加完全的自定义机制,直接将序列化对象替换成其余的对象,须要定义writeReplace

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Serializable {

    private String name;
    private int age;

    private Object writeReplace(){
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

Externalizable自定义

Externalizable实现了Seriablizable接口,并规定了两个方法:

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

实现该接口,并给出两个方法的实现,也能够实现自定义序列化。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Externalizable {

    String name;
    int age;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer) in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

最后

若是本文有叙述错误之处,还望评论区批评指正,共同进步。

原文连接:https://www.cnblogs.com/summerday152/p/14152925.html

若是以为本文对你有帮助,能够关注一下我公众号,回复关键字【面试】便可获得一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,你们一块儿学习进步!

相关文章
相关标签/搜索