CS-Notes/notes/Java IO.md
2019-12-06 10:11:23 +08:00

628 lines
22 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- GFM-TOC -->
* [概览](#一概览)
* [磁盘操作](#二磁盘操作)
* [字节操作](#三字节操作)
* [实现文件复制](#实现文件复制)
* [装饰者模式](#装饰者模式)
* [字符操作](#四字符操作)
* [编码与解码](#编码与解码)
* [String 的编码方式](#string-的编码方式)
* [Reader Writer](#reader--writer)
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
* [对象操作](#五对象操作)
* [序列化](#序列化)
* [Serializable](#serializable)
* [transient](#transient)
* [网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
* [Sockets](#sockets)
* [Datagram](#datagram)
* [NIO](#七nio)
* [流与块](#流与块)
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
* [选择器](#选择器)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
* [参考资料](#八参考资料)
<!-- GFM-TOC -->
# 概览
Java I/O 大概可以分成以下几类
- 磁盘操作File
- 字节操作InputStream OutputStream
- 字符操作Reader Writer
- 对象操作Serializable
- 网络操作Socket
- 新的输入/输出NIO
# 磁盘操作
File 类可以用于表示文件和目录的信息但是它不表示文件的内容
递归地列出一个目录下所有文件
```java
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
```
Java7 开始可以使用 Paths Files 代替 File
# 字节操作
## 实现文件复制
```java
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
byte[] buffer = new byte[20 * 1024];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
in.close();
out.close();
}
```
## 装饰者模式
Java I/O 使用了装饰者模式来实现 InputStream 为例
- InputStream 是抽象组件
- FileInputStream InputStream 的子类属于具体组件提供了字节流的输入操作
- FilterInputStream 属于抽象装饰者装饰者用于装饰组件为组件提供额外的功能例如 BufferedInputStream FileInputStream 提供缓存的功能
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/9709694b-db05-4cce-8d2f-1c8b09f4d921.png" width="650px"> </div><br>
实例化一个具有缓存功能的字节流对象时只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可
```java
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作比如 intdouble 等基本类型
# 字符操作
## 编码与解码
编码就是把字符转换为字节而解码是把字节重新组合成字符
如果编码和解码过程使用不同的编码方式那么就出现了乱码
- GBK 编码中中文字符占 2 个字节英文字符占 1 个字节
- UTF-8 编码中中文字符占 3 个字节英文字符占 1 个字节
- UTF-16be 编码中中文字符和英文字符都占 2 个字节
UTF-16be 中的 be 指的是 Big Endian也就是大端相应地也有 UTF-16lele 指的是 Little Endian也就是小端
Java 的内存编码使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式而是说 char 这种类型使用 UTF-16be 进行编码char 类型占 16 也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储
## String 的编码方式
String 可以看成一个字符序列可以指定一个编码方式将它编码为字节序列也可以指定一个编码方式将一个字节序列解码为 String
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在调用无参数 getBytes() 方法时默认的编码方式不是 UTF-16be双字节编码的好处是可以使用一个 char 存储中文和英文而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码getBytes() 的默认编码方式与平台有关一般为 UTF-8
```java
byte[] bytes = str1.getBytes();
```
## Reader Writer
不管是磁盘还是网络传输最小的存储单元都是字节而不是字符但是在程序中操作的通常是字符形式的数据因此需要提供对字符进行操作的方法
- InputStreamReader 实现从字节流解码成字符流
- OutputStreamWriter 实现字符流编码成为字节流
## 实现逐行输出文本文件的内容
```java
public static void 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 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
# 对象操作
## 序列化
序列化就是将一个对象转换成字节序列方便存储和传输
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
不会对静态变量进行序列化因为序列化只是保存对象的状态静态变量属于类的状态
## Serializable
序列化的类需要实现 Serializable 接口它只是一个标准没有任何方法需要实现但是如果不去实现它的话而进行序列化会抛出异常
```java
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
transient 关键字可以使一些属性不会被序列化
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的因为这个数组是动态扩展的并不是所有的空间都被使用因此就不需要所有的内容都被序列化通过重写序列化和反序列化方法使得可以只序列化数组中有内容的那部分数据
```java
private transient Object[] elementData;
```
# 网络操作
Java 中的网络支持
- InetAddress用于表示网络上的硬件资源 IP 地址
- URL统一资源定位符
- Sockets使用 TCP 协议实现网络通信
- Datagram使用 UDP 协议实现网络通信
## InetAddress
没有公有的构造函数只能通过静态方法来创建实例
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
```
## URL
可以直接从 URL 中读取字节流数据
```java
public static void main(String[] args) throws IOException {
URL url = new URL("http://www.baidu.com");
/* 字节流 */
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();
}
```
## Sockets
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream OutputStream 进行输入输出
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1e6affc4-18e5-4596-96ef-fb84c63bf88a.png" width="550px"> </div><br>
## Datagram
- DatagramSocket通信类
- DatagramPacket数据包类
# NIO
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的弥补了原来的 I/O 的不足提供了高速的面向块的 I/O
## 流与块
I/O NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据 NIO 以块的方式处理数据
面向流的 I/O 一次处理一个字节数据一个输入流产生一个字节数据一个输出流消费一个字节数据为流式数据创建过滤器非常容易链接几个过滤器以便每个过滤器只负责复杂处理机制的一部分不利的一面是面向流的 I/O 通常相当慢
面向块的 I/O 一次处理一个数据块按块处理数据比按流处理数据要快得多但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了所以现在它可以利用 NIO 的一些特性例如java.io.\* 包中的一些类包含以块的形式读写数据的方法这使得即使在面向流的系统中处理速度也会更快
## 通道与缓冲区
### 1. 通道
通道 Channel 是对原 I/O 包中的流的模拟可以通过它读取和写入数据
通道与流的不同之处在于流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类)而通道是双向的可以用于读写或者同时用于读写
通道包括以下类型
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据
- SocketChannel通过 TCP 读写网络中数据
- ServerSocketChannel可以监听新进来的 TCP 连接对每一个新进来的连接都会创建一个 SocketChannel
### 2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中同样地从通道中读取的任何数据都要先读到缓冲区中也就是说不会直接对通道进行读写数据而是要先经过缓冲区
缓冲区实质上是一个数组但它不仅仅是一个数组缓冲区提供了对数据的结构化访问而且还可以跟踪系统的读/写进程
缓冲区包括以下类型
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
## 缓冲区状态变量
- capacity最大容量
- position当前已经读写的字节数
- limit还可以读写的字节数
状态变量的改变过程举例
新建一个大小为 8 个字节的缓冲区此时 position 0 limit = capacity = 8capacity 变量不会改变下面的讨论会忽略它
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
从输入通道中读取 5 个字节数据写入缓冲区中此时 position 5limit 保持不变
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
在将缓冲区的数据写到输出通道之前需要先调用 flip() 方法这个方法将 limit 设置为当前 position并将 position 设置为 0
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
从缓冲区中取 4 个字节到输出缓冲中此时 position 设为 4
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
最后需要调用 clear() 方法来清空缓冲区此时 position limit 都被设置为最初位置
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 文件 NIO 实例
以下展示了使用 NIO 快速复制文件的实例
```java
public static void fastCopy(String src, String dist) throws IOException {
/* 获得源文件的输入字节流 */
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);
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
}
}
```
## 选择器
NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用
NIO 实现了 IO 多路复用中的 Reactor 模型一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件从而让一个线程就可以处理多个事件
通过配置监听的通道 Channel 为非阻塞那么当 Channel 上的 IO 事件还未到达时就不会进入阻塞状态一直等待而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行
因为创建和切换线程的开销很大因此使用一个线程来处理多个事件而不是一个线程处理一个事件对于 IO 密集型的应用具有很好地性能
应该注意的是只有套接字 Channel 才能配置为非阻塞 FileChannel 不能 FileChannel 配置非阻塞也没有意义
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/093f9e57-429c-413a-83ee-c689ba596cef.png" width="350px"> </div><br>
### 1. 创建选择器
```java
Selector selector = Selector.open();
```
### 2. 将通道注册到选择器上
```java
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式否则使用选择器就没有任何意义了因为如果通道在某个事件上被阻塞那么服务器就不能响应其它事件必须等待这个事件处理完毕才能去处理其它事件显然这和选择器的作用背道而驰
在将通道注册到选择器上时还需要指定要注册的具体事件主要有以下几类
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它们在 SelectionKey 的定义如下
```java
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
```
可以看出每个事件可以被当成一个位域从而组成事件集整数例如
```java
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
### 3. 监听事件
```java
int num = selector.select();
```
使用 select() 来监听到达的事件它会一直阻塞直到有至少一个事件到达
### 4. 获取到达的事件
```java
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
```
### 5. 事件循环
因为一次 select() 调用不能处理完所有的事件并且服务器端有可能需要一直监听事件因此服务器端处理事件的代码一般会放在一个死循环内
```java
while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}
```
## 套接字 NIO 实例
```java
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
```
```java
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
```
## 内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法它可以比常规的基于流或者基于通道的 I/O 快得多
向内存映射文件写入可能是危险的只是改变数组的单个元素这样的简单操作就可能会直接修改磁盘上的文件修改数据与将数据保存到磁盘是没有分开的
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类因此可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区操作系统会在需要时负责执行映射
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 对比
NIO 与普通 I/O 的区别主要有以下两点
- NIO 是非阻塞的
- NIO 面向块I/O 面向流
# 参考资料
- Eckel B, 埃克尔, 昊鹏, . Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
<div align="center"><img width="320px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/githubio/公众号二维码-2.png"></img></div>