【JavaLearn】# (12)IO流、文件字节(符)流、缓冲字节(符)流、数据流、对象流、序列化

1. IO流概述

在Java中,数据的输入/输出操作,以”流“(stream)方式进行,一般位于 java.io 包中

数据源(data source)提供原始数据的原始媒介:数据库、文件、其他程序、内存、网络连接、IO设备

流是一个抽象、动态的概念,是一连串连续动态的数据集合

从源文件到目的文件,中间必须经过中转(把源数据读到程序中,再从程序输出到目的

1.1 IO流分类

  • 流的方向输入,输入是相对于程序(中转站)来说的
    • 输入流:从数据源到程序(InputStream、Reader 结尾)
    • 输出流:从程序到目的地(OutputStream、Writer 结尾)

  • 处理的数据单元
    • 字节流:以字节为单位获取数据,一般是 Stream 结尾的流(顶级类:InputStream、OutputStream)
    • 字符流:以字符为单位获取数据,一般是 Reader/Writer 结尾的流 (顶级类:Reader、Writer)
  • 处理对象不同
    • 节点流:直接数据源目的地读写数据,如 FileInputStream、FileReader
    • 处理流(包装流):不直接连接数据源或目的地,处理流的流,通过处理其他流提高程序的性能,如 BufferedInputStream、BufferedReader

节点流位于 IO操作的第一线,所有的操作都通过它们来进行。处理流是用来提高性能和程序灵活性的

1.2 IO流体系结构

InputStream 和 OutputStream

  • 最基本的字节输入输出流,其他的字节流类都继承自它们
  • 都是抽象类,不能创建实例,只能使用它们的子类

Reader 和 Writer

  • 最基本的字符输入输出流,其他的字符流类都继承自它们
  • 都是抽象类,不能创建实例,只能使用它们的子类

2. 文件【字节】流

FileInputStream

文件字节输入流:FileInputStream 文件字节输出流:FileOutputStream

/**
  *  1.创建流
  *     File file = new File("E:/readme.txt");
  *     InputStream in = new FileInputStream(file);
  */
// 简化写法
InputStream in = new FileInputStream("E:/readme.txt");
OutputStream out = new FileOutputStream("E:/readme2.txt");

/**
  *  2.使用流
  *     准备一个中转站,借助循环和中转站,使用输入流和输出流完成复制
  */
// 先读源文件的一个字节赋给 中转站n
int n = in.read();
while (n != -1) { // 读到了数据,还没有到末尾
    // 写一个字节到文件
    out.write(n);
    // 再读一个字节,赋给n
    n = in.read();
}

// 3.关闭流
in.close();
out.close();

注意:存在问题,只适用于复制小文件,因为中转站太小了,进行修改,使用字节数组,byte

// 准备一个中转站,一次读 1024 个字节
byte[] buf = new byte[1024];

// 使用输入流读取文件,将读到的内容放入到 buf字节数组中,返回的是【真正读取到】的字节
int len = in.read(buf);

while (len != -1) {
    // 将字节数组的内容写入到文件,从第 0个字节开始读,读取 len个字节
    out.write(buf, 0, len);
    len = in.read(buf);
}

3. 文件【字符】流

FileReader

文件字符输入流:FileReader 文件字符输出流:FileWriter 节点流,数据源和目的地是文件

// 创建流
Reader in = new FileReader("E:/readme.txt"); // 还可以带一个参数,默认为 false,表示不追加,直接覆盖
Writer out = new FileWriter("E:/readme2.txt", true); // 设置为 true时,在后面追加

// 使用流
int n = in.read();
while ((n = in.read()) != -1) {
    out.write(n);
}

// 关闭流
in.close();
out.close();

注意扩大中转站,字符流,需要使用 char

char[] cbuf = new char[1024];
int len;
while ((len = in.read(cbuf)) != -1) {
    // 一定要写真正读取到的文件长度
    out.write(cbuf, 0, len);
}

优化:进行异常处理

Reader in = null;
Writer out = null;
try {
    // 创建流
    in = new FileReader("E:/readme.txt");
    out = new FileWriter("E:/readme2.txt");

    // 使用流
    char[] cbuf = new char[1024];
    int len;
    while ((len = in.read(cbuf)) != -1) {
        out.write(cbuf, 0, len);
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 关闭流
    try {
        // 如果没有创建好,就无需关闭
        if (in != null) {
            in.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        if (out != null) {
            out.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 其实只有字节流,字符流的底层还是字节流
  • 字节流可以完成所有类型文件的复制,字符流只可以完成文本文件的复制
  • 异常处理的分析:创建流、使用流要使用一次 try-catch 语句,关闭流要分开进行异常处理

4. 缓冲【字节】流

BufferedInputStream

// 创建【文件字节流】
InputStream in = new FileInputStream("E:/readme.txt");
OutputStream out = new FileOutputStream("E:/readme2.txt");

// 使用【缓冲字节流】包装字节流,提高性能
BufferedInputStream bis = new BufferedInputStream(in); // 默认输入缓冲区大小: 8192
BufferedOutputStream bos = new BufferedOutputStream(out); // 默认输出缓冲区大小: 8192

// 使用流,直接使用扩大中转站的代码
byte[] buf = new byte[1024];
int len;
while ((len = bis.read(buf)) != -1) {
    bos.write(buf, 0, len);
}

// 关闭流(只关闭顶层的即可)
bos.close();
bis.close();

有了缓冲流之后,中转站先从缓存区中读取数据,缓存区若为空,则读取一次硬盘,读8192个字节的内容,放入缓冲区

输出也是先输出到缓冲区,到满8192个字节后,一次性写到硬盘中

注意:关闭时,只关闭高层流即可,底层流自动关闭

何时将输出缓冲区的内容写入到硬盘?

  • 输出缓冲区满,自动写入硬盘(刷新 flush)
  • close() 会先刷新
  • 手动的 flush()

5. 缓冲【字符】流

BufferedReader

优化:一次读取一行,一次写一行,提高效率(普通版本,最终版本用 PrintWriter)

// 创建流
BufferedReader br = new BufferedReader(new FileReader("E:/readme.txt")); // 默认大小:8192
BufferedWriter bw = new BufferedWriter(new FileWriter("E:readme2.txt"));

// 使用流
String str;
// 一次读取一行
while ((str = br.readLine()) != null) {
    // 一次写一行
    bw.write(str);
    bw.newLine(); // 换行
}

// 关闭流
bw.close();
br.close();

readLine() 底层原理:还是一个一个字符的读取,然后 append() 放入到 StringBuilder(或者 char[] )中,直到遇到换行符,将 StringBuilder(char[])转换成 String 并返回

6. 数据流和对象流

能很方便的实现对各种基本类型引用类型数据的读写,并保留其本身的类型

  • 只有字节流,没有字符流
  • 都是处理流,不是结点流
  • 数据流:只能操作基本数据类型和字符串,对象流:还可以操作对象
  • 写入的是二进制数据
  • 写入的数据,需要使用对应的输入流来读取

DataInputStream

// 写操作
public static void write() throws IOException {
    // 创建流
    OutputStream out = new FileOutputStream("E:/readme2.txt");
    // 加上缓冲流,提高读写速度(先写到缓冲区中)
    BufferedOutputStream bos = new BufferedOutputStream(out);
    // 然后用数据流包装缓冲流
    DataOutputStream dos = new DataOutputStream(bos);    // 【DataOutputStream】

    // 使用流
    dos.writeInt(123);
    dos.writeDouble(3.14);
    dos.writeUTF("lwclick");
    dos.writeChar('学');

    // 关闭流
    dos.close();
}

// 读操作
public static void read() throws IOException {
    InputStream in = new FileInputStream("E:/readme2.txt");
    BufferedInputStream bis = new BufferedInputStream(in);
    DataInputStream dis = new DataInputStream(bis);

    // 按照写入的顺序读
    dis.readInt();
    dis.readDouble();
    dis.readUTF();
    dis.readChar();

    dis.close();
}

ObjectInputStream

public static void write() throws IOException {
    // 创建流
    OutputStream out = new FileOutputStream("E:/readme2.txt");
    // 加上缓冲流,提高读写速度(先写到缓冲区中)
    BufferedOutputStream bos = new BufferedOutputStream(out);
    ObjectOutputStream oos = new ObjectOutputStream(bos);   // 【ObjectOutputStream】

    // 使用流
    oos.writeInt(123);
    oos.writeDouble(3.14);
    oos.writeUTF("lwclick");
    oos.writeChar('学');

    // 还可以写对象
    oos.writeObject(new Date());

    // 写自定义的类时,需要实现 Serializable 序列化接口
    // public class Student implements Comparable<Student>, Serializable {}
    oos.writeObject(new Student(1, "zhangsan", 89.0));

    // 关闭流
    oos.close();
}

public static void read() throws IOException, ClassNotFoundException {
    InputStream in = new FileInputStream("E:/readme2.txt");
    BufferedInputStream bis = new BufferedInputStream(in);
    ObjectInputStream ois = new ObjectInputStream(bis);

    // 按照写入的顺序读
    ois.readInt();
    ois.readDouble();
    ois.readUTF();
    ois.readChar();
    ois.readObject();  // Date
    ois.readObject();  // Student

    ois.close();
}

7. 序列化和反序列化

什么是序列化?

  • 序列化(Serialization):将对象(内存) ------> 字节数组 字节序列(外存、网络)
  • 反序列化(DeSerialization):字节数组 字节序列(外存、网络) -------> 对象(内存)

什么时候需要序列化和反序列化?

  • 存储或传输的时候, 比如存储到外存(硬盘)中 传输到网络

如何实现序列化和反序列化?

  • 相应的类要实现 Serializable 接口

    ObjectInputStream ois = new ObjectInputStream(...);
    ois.readObject();  // 反序列化
    
    ObjectOutputStream oos = new ObjectOutputStream(...);
    oos.writeObject(new Student(1, "张三", 89));
    

需要注意的细节:

  • 实现了 Serializable 接口,是为了源码中做判断,instanceof

  • static 属性不参与序列化(如果只是希望某个属性不参与序列化,使用 transient 修饰)

  • 实现了序列化的接口,都给一个固定的序列化版本号(用IDEA自动生成)

    image-20210821110154961

  • 如果一个对象中,包含另一个对象的引用,想要实现序列化,那么该对象的引用,也必须实现序列化

    public class Student implements Comparable<Student>, Serializable {
        private static final long serialVersionUID = 3023320127814394755L; // 固定的版本号
        private int sno;
        private transient String name; // 不参与序列化
        private Double score;
        private Clazz clazz; // 对象的引用,也必须实现 Serializable 接口
    }
    
    // Clazz类
    public class Clazz implements Serializable {
    }
    

8. 其他流

8.1 打印流

PrintStream 只有输出流,是一个包装流,字节流

PrintStream ps = System.out;
System.out.println("Hello World!");
// 效果是一样的,都是打印到控制台
ps.println("Hello World!!!");

// 打印到文件中(但是不管什么类型,打印到文件中,都变为 String 类型)
PrintStream toFile = new PrintStream("E:/readme2.txt");
toFile.println(3.14);
toFile.println("lwclick");
toFile.println(new Date());

PrintWriter 只有输出流,是一个包装流,字符流 按行读写

// 创建输入流
BufferedReader br = new BufferedReader(new FileReader("E:/readme.txt"));
// 创建输出流(PrintWriter)
PrintWriter pw = new PrintWriter("E:/readme2.txt"); // 底层代码中,带缓冲(BufferedWriter)

String str;
while ((str = br.readLine()) != null) {
    pw.println(str);
}

br.close();
pw.close();

8.2 转换流

InputStreamReader ,读取来自键盘的一行行数据,写入到硬盘的文件中

用到了设计模式:适配器模式

Reader ----> InputStreamReader -----> InputStream

// 获取来自键盘的输入
InputStream is = System.in;
Reader reader = new InputStreamReader(is); // 将字节流 ----> 字符流     属于一种字符流
BufferedReader br = new BufferedReader(reader);

// 输出到硬盘
PrintWriter pw = new PrintWriter("E:/readme2.txt");

String str = br.readLine();
while (!"exit".equals(str)) {
    pw.println(str);
}

pw.close();
br.close();

8.3 其他流

ByteArrayInputStream bais;  // 字节数组

8.4 Java IO 中使用了哪些设计模式?

  • 适配器模式: InputStreamReader

  • 装饰模式:现组装(继承的一种替代方案)

    InputStream fis = new FileInputStream("E:/readme.txt");
    BufferedInputStream bis = new BufferedInputStream(fis);
    DataInputStream dis = new DataInputStream(bis);
    

针对字节流:

  • 顶级类:InputStream
  • 处理流的顶级类:FilterInputStream extends InputStream

9. 案例:复制文件夹

File.separator 可以根据操作系统选择对应的分隔符

注意点:

  • 使用字节流(可能有图片、视频、音频等二进制文件)
  • 提高复制速度:使用缓冲 BufferedInputStream …
  • 涉及到知识点:IO流(文件的复制)、递归操作、File类(文件夹的创建)

思路:

  • 复制一个文件

    // 复制一个文件
    private static void copyFile(String sourceName, String destName) {
        BufferedInputStream  bis = null;
        BufferedOutputStream bos = null;
        try {
            InputStream fis = new FileInputStream(sourceName);
            bis = new BufferedInputStream(fis);
    
            OutputStream fos = new FileOutputStream(destName);
            bos = new BufferedOutputStream(fos);
    
            byte[] buf = new byte[1024];
            int len;
            while ((len = bis.read(buf)) != -1) {
                bos.write(buf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null) {
                    bis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  • 复制一个文件夹下的所有文件(不包括子文件夹)

    private static void copyDir(String sourceDirName, String destDirName) {
        // 源文件夹必须存在
        File sourceDir = new File(sourceDirName);
        if (!sourceDir.exists()) {
            System.err.println("源文件夹必须存在");
            return;
        }
    
        // 目的文件夹必须手动创建
        File destDir = new File(destDirName);
        if (!destDir.exists()) {
            destDir.mkdirs();
        }
    
        // 复制源文件夹下的所有文件,到目的文件夹
        File[] files = sourceDir.listFiles(); // 目录下所有的文件
        for (File file : files) {
            // 如果是文件,就复制
            if (file.isFile()) {
                copyFile(sourceDirName + "/" + file.getName(), destDirName + "/" + file.getName());
            }
    
            // 如果是文件夹,也复制【递归调用自己!!!】
            if (file.isDirectory()){
                copyDir(sourceDirName + "/" + file.getName(), destDirName + "/" + file.getName());
            }
        }
    }
    
  • 复制一个文件夹下的所有文件和子文件夹

    递归调用自己!!!

10. BIO、NIO、AIO总结

10.1 同步与异步

  • 同步 :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行
    • 被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回
  • 异步: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行
    • 一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情

10.2 阻塞和非阻塞

  • 阻塞: 发起一个请求,调用者一直等待请求结果返回,当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态

10.3 BIO(Blocking I/O)

同步阻塞I/O模式,数据的读取 写入必须阻塞在一个线程内等待其完成,典型的 一请求一应答通信模型

如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是socket.accept()socket.read()socket.write() 涉及的三个主要函数都是同步阻塞的)

在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,
可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。
因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

10.4 NIO(New I/O)

同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

  • 它支持面向缓冲的,基于通道的I/O操作方法。
  • 对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性
  • 对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

10.4.1 NIO特性(重点)

Channel
Buffer
Selector

  • IO流是阻塞的,NIO流是不阻塞(Non-blocking)的。
单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。
面向流的I/O中,可以将数据直接写入或者将数据直接读到 Stream 对象中。
虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是【从流读到缓冲区】

NIO 却是直接读到 Buffer 中进行操作,所有数据都是用缓冲区处理的

最常用的缓冲区是 ByteBuffer(除了boolean其他都有)
  • Channel (通道)
NIO 通过Channel(通道) 进行读写。

通道是双向的,可读也可写,而流的读写是单向的。

无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
  • Selector (选择器)
NIO有选择器,而IO没有。

选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。

10.4.2 NIO 读数据和写数据方式

通常来说NIO中的所有IO都是从 Channel(通道) 开始的。

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

10.4.3 Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。

10.5 AIO (Asynchronous I/O)

在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LRcoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值