【Java之IO操作全集】

1. 首先看IO操作的思路大纲

Java IO操作大纲

2. IO的分类

2.1 传输方式

  • 字节流
    在这里插入图片描述
  • 字符流
    在这里插入图片描述
  • 字节流和字符流的区别
    • 字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。)
    • 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。

字节给计算机看,字符给人看

  • 字节转字符的妙用
    • 字符转换成字节的过程,称为编码(encode);字节转换为字符的过程为解码(decode);
    • 如果编码和解码使用的编码方式不同,就会出现乱码。
      • GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
      • UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
      • UTF-16be 编码中,中文字符和英文字符都占 2 个字节。

2.2 数据操作

  • 文件file
    • FileInputStream
    • FIleOutputStream
    • FileReader
    • FileWriter
  • 数组
    • ByteArrayInputStream
    • ByteArrayOutputStream
    • CharArrayReader
    • CharArrayWriter
  • 管道
    • PipedInputStream
    • PipedOutputStream
    • PipedReader
    • PipedWriter
  • 基本数据类型
    • DataInputStream
    • DataOutputStream
  • 缓存操作
    • BufferedInputStream
    • BufferedOutputStream
    • BufferedReader
    • BufferedWriter
  • 打印
    • PrintStream
    • PrintWriter
  • 对象序列化/反序列化
    • ObjectInputStream
    • ObjectOutputStream
  • 转换
    • InputStreamReader
    • OutputStreamWriter

3. IO设计模式之装饰器模式

让我们看看大佬是如何设计底层IO组件的架构的?

以InputStream为例:

  • Inputstream是抽象组件
  • FileInputStream是具体组件,是InputStream子类,提供字节流的输入操作
  • FilterInputStream是抽象组件,装饰者用于装饰组件,为组件提供额外的功能。例如:BufferedInputStream为FileInputStream提供缓存的功能。
    JavaIO架构图
    实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 intdouble 等基本类型。

4. Java IO — 常用类案例

4.1 递归列出目录下所有文件:

public static void listAllFiles(File dir){
	if(dir == null || !dir.exists()){
		return;
	}
	if(dir.isFile()){
		System.out.println(dir.getName);
	}
	for(File file:dir.listFiles()){
		listAllFiles(file);
	}
}

4.2 使用字节流拷贝文件

	public static void copyFile(String src,String dist){
		FileInputStream in = new FileInputStream(src);
		FileOutputStream out = new FileOutputStream(dist);
		// 创建一个20KB的字节数组
		byte[] buffer = new byte[20 * 1024];
		// read()最多读取buffer.length个字节
		// 返回-1 的时候表示读到eof,即文件末尾
		while(in.read(buffer,0,buffer.length)!= -1){
			out.write(buffer);
		}
		in.close();
		out.close();
	}

4.3 实现逐行输出文件内容

public static readFileContent(String filePath) throws IOException{
	FileReader fileReader = new FileReader(filePath);
	BufferedReader bufferedReader = new BufferedReader(fileReader);
	String line;
	while((line = bufferedReader.readLine()) != null){
		System.out.println(line);
	}
	bufferedReader.close();
	//装饰着模式使得BufferedReader组合了一个Reader对象
	//在调用BufferedReader的close()方法时会去调用Reader的close()方法
	// 因此只要一个close()调用即可
}

4.4 序列化和反序列化

序列化就是将一个对象转换为字节序列,方便存储和传输

  • 序列化:ObjectOutputStream.writeObject();
  • 反序列化:ObjectInputStream.readObject();

注意:不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
序列化的类需要实现Serializable接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。

public static void main(String[] args) throws IOException, ClassNotFoundException {
    A a1 = new A(123, "abc");
    String objectFile = "file/a1";
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();

    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
}

private static class A implements Serializable {
    private int x;
    private String y;

    A(int x, String y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "x = " + x + "  " + "y = " + y;
    }
}

关键字:transient可以将一些属性不会被序列化。ArrayList中对elementData进行了修饰。

5. Java中的网络支持

  • InetAddress: 用于表示网络上的硬件资源,即 IP 地址;
  • URL: 统一资源定位符;
  • Sockets: 使用 TCP 协议实现网络通信;
    • ServerSocket: 服务器端类
    • Socket: 客户端类
    • 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
  • Datagram: 使用 UDP 协议实现网络通信
    • DatagramSocket: 通信类
    • DatagramPacket: 数据包类
/* InetAddress类是Java中用于表示IP地址的类,它可以用来获取主机的IP地址、主机名、域名等相关信息。
* 下面是一个使用InetAddress类的Java代码演示案例:
* */
public class InetAddressExample {
    public static void main(String[] args) {
        try {
            // 获取本地主机的IP地址
            InetAddress localHost = InetAddress.getLocalHost();
            System.out.println("本地主机的IP地址: " + localHost.getHostAddress());

            // 获取百度的IP地址
            InetAddress baidu = InetAddress.getByName("www.baidu.com");
            System.out.println("百度的IP地址: " + baidu.getHostAddress());

            // 获取谷歌的IP地址
            InetAddress google = InetAddress.getByName("www.google.com");
            System.out.println("谷歌的IP地址: " + google.getHostAddress());
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}
/* 运行结果为: 
	本地主机的IP地址: 192.168.0.100
	百度的IP地址: 220.181.38.148
	谷歌的IP地址: 172.217.25.196
*/
/* InetAdress类还提供了其他的方法,如getHostName()用于获取主机名,isReachable()用于检测主机是否可
达等等,可以根据具体需求选择合适的方法使用。*/

// URl:可以直接从 URL 中读取字节流数据。
public static void main(String[] args){
	Url url = new Url("http://www.baidu.comm");
	/*字节流*/
	InputStream is = url.openStream();
	/*转化为字符流*/
	InputStreamReader isr = new InputStreamReader(is,"utf-8");
	/*增强为缓存流*/
	BufferedReader br = new BufferedReader(isr);
	String line;
	while((line = br.readLine())!= null){
		System.out.println(line);
	}
	br.close();
}

6. IO模型—Unix IO模型

一个输入操作通常包括两个阶段:

  • 等待数据准备好
  • 从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当等待分组到达时,它被复制到内核中的某个缓存去。第二步就是把数据从内核缓存区复制到应用程序缓存区。

6.1 Unix 下有五种I/O模型:

  • 阻塞式IO
    在这里插入图片描述
    翻译后这样理解:
    在这里插入图片描述
  • 非阻塞IO

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。

在这里插入图片描述
翻译过来就是这样:
在这里插入图片描述

  • IO复用(select 和poll)
  1. 使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
  2. 它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
  3. 如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。

在这里插入图片描述
翻译过来就是这样:
在这里插入图片描述

  • 信号驱动式IO(SIGIO)
  1. 应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
  2. 相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。

在这里插入图片描述
翻译过来就是这样:
在这里插入图片描述

  • 异步IO(AIO)
  1. 进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
  2. 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。

在这里插入图片描述
翻译后是这样的:
在这里插入图片描述

6.2 I/O模型比较

6.2.1 同步I/O和异步I/O

  1. 同步 I/O: 应用进程在调用 recvfrom 操作时会阻塞。
  2. 异步 I/O: 不会阻塞。
    阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。

6.2.2 五大 I/O 模型比较

前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的: 将数据从内核复制到应用进程过程中,应用进程会被阻塞。

在这里插入图片描述

6.2.3 IO多路复用

IO多路复用最为重要,后面的Java NIO - IO多路复用详解将对IO多路复用,Ractor模型以及Java NIO对其的支持作详解。

  1. select 应用场景select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求更高的场景,比如核反应堆的控制。select 可移植性更好,几乎被所有主流平台所支持。
  2. poll 应用场景poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且epoll 的描述符存储在内核,不容易调试。
  3. epoll 应用场景只需要运行在 Linux 平台上,并且有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。

epoll 的描述符事件有两种触发模式: LT(level trigger)和 ET(edge trigger)。

  1. LT 模式当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
  2. ET 模式和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

7.Java I - BIO详解

BIO就是: blocking IO。最容易理解、最容易实现的IO工作方式,应用程序向操作系统请求网络IO操作,这时应用程序会一直等待;另一方面,操作系统收到请求后,也会等待,直到网络上有数据传到监听端口;操作系统在收集数据后,会把数据发送给应用程序;最后应用程序受到数据,并解除等待状态。

7.1 几个重要概念:

  • 同步IO和非同步IO
    这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
  • 阻塞IO和非阻塞IO
    这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)

7.2 传统的BIO通信方式简介

以前大多数网络通信方式都是阻塞模式的,即:

  • 客户端向服务器端发出请求后,客户端会一直等待(不会再做其他事情),直到服务器端返回结果或者网络出现问题。
  • 服务器端同样的,当在处理某个客户端A发来的请求时,另一个客户端B发来的请求会等待,直到服务器端的这个处理线程完成上一个处理。
    在这里插入图片描述

7.2.1 传统的BIO的问题

  • 同一时间,服务器只能接受来自于客户端A的请求信息;虽然客户端A和客户端B的请求是同时进行的,但客户端B发送的请求信息只能等到服务器接受完A的请求数据后,才能被接受。
  • 由于服务器一次只能处理一个客户端请求,当处理完成并返回后(或者异常时),才能进行第二次请求的处理。很显然,这样的处理方式在高并发的情况下,是不能采用的。

7.2.2 多线程方式 - 伪异步方式

上面说的情况是服务器只有一个线程的情况,那么读者会直接提出我们可以使用多线程技术来解决这个问题:

  • 当服务器收到客户端X的请求后,(读取到所有请求数据后)将这个请求送入一个独立线程进行处理,然后主线程继续接受客户端Y的请求。
  • 客户端一侧,也可以使用一个子线程和服务器端进行通信。这样客户端主线程的其他工作就不受影响了,当服务器端有响应信息的时候再由这个子线程通过 监听模式/观察模式(等其他设计模式)通知主线程。

如下图所示:
在这里插入图片描述
但是使用线程来解决这个问题实际上是有局限性的:

  • 虽然在服务器端,请求的处理交给了一个独立线程进行,但是操作系统通知accept()的方式还是单个的。也就是,实际上是服务器接收到数据报文后的“业务处理过程”可以多线程,但是数据报文的接受还是需要一个一个的来(下文的示例代码和debug过程我们可以明确看到这一点)
  • 在linux系统中,可以创建的线程是有限的。我们可以通过cat /proc/sys/kernel/threads-max 命令查看可以创建的最大线程数。当然这个值是可以更改的,但是线程越多,CPU切换所需的时间也就越长,用来处理真正业务的需求也就越少。创建一个线程是有较大的资源消耗的。JVM创建一个线程的时候,即使这个线程不做任何的工作,JVM都会分配一个堆栈空间。这个空间的大小默认为128K,您可以通过-Xss参数进行调整。当然您还可以使用ThreadPoolExecutor线程池来缓解线程的创建问题,但是又会造成BlockingQueue积压任务的持续增加,同样消耗了大量资源。
  • 另外,如果您的应用程序大量使用长连接的话,线程是不会关闭的。这样系统资源的消耗更容易失控。 那么,如果你真想单纯使用线程解决阻塞的问题,那么您自己都可以算出来您一个服务器节点可以一次接受多大的并发了。看来,单纯使用线程解决这个问题不是最好的办法。

8. Java NIO — 详解

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。

standard IO 是对字节流的读写,在进行IO之前,首先创建一个流对象,流对象进行读写都是按字节,一个字节一个字节的来进行读或写。而NIO把io抽象成块,类似磁盘读写,每次IO操作的单位都是一个块,块被读入内存之后就是一个byte[],NIO一次可以读或写多个字节。

8.1 流与块

  1. I/O与NIO最重要的区别是数据打包和传输方式,I/O以流的方式处理数据,而NIO以块的方式处理数据
  2. 面向流的 I/O 一次处理一个字节数据: 一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
  3. 面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

8.2 通道与缓存区

8.2.1 通道

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel: 从文件中读写数据;
  • DatagramChannel: 通过 UDP 读写网络中数据;
  • SocketChannel: 通过 TCP 读写网络中数据;
  • ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

8.2.2 缓存区

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

8.3 缓冲区状态变量

  • capacity: 最大容量;
  • position: 当前已经读写的字节数;
  • limit: 还可以读写的字节数。

8.4 文件NIO实例

以下展示NIO快速复制文件的案例:

public static void fastCopy(String src,String dist) throws Exception{
	/*获得源文件的输入字节流*/
	FileInputStream fin = new FileInputStream(src);
	/*获取输入字节流的文件通道*/
	FileChannel fcin = fin.getChannel();
	//获得目标文件的输出字节流
	FileOutputStream fout = new FileOutputStream(dist);
	/*获取输出字节流的通道*/
	FileChannel fcout = fout.getChannel();
	/*为缓冲区分配 1024 个字节*/
	ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
	while(true){
		//
		int r = fcin.read(buffer);
		if(r == -1){
			break;
		}
		/*切换读写*/
		buffer.flip();
		/* 把缓冲区的内容写入输出文件中 */
		fcout.write(buffer);
		/* 清空缓冲区*/
		buffer.clear();
	}
}

8.5 选择器

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

8.6 套接字NIO实例

8.7 内存映射文件

内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。

内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘的文件。修改数据与将数据保存到磁盘是没有分开的。

下面代码行将文件的前1024个字节映射到文件中,map()方法返回一个MappedByteBuffer,它是ByteBuffer的子类。因此,可以像使用其他任何ByteBuffer一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。

MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,0,1024);

8.8 对比

NIO与普通IO的区别主要有以下特点:

  • NIO是非阻塞的
  • NIO面向块,IO面向流

9.Java NIO — IO多路复用详解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值