一、简述
对Java中I/O相关的基础知识和I/O模型进行系统化梳理总结,以便更有效的学习应用 。读者可以按三个层次来学习理解:首先是对基础io包中的类按模块进行学习理解 ,如有磁盘操作、字节操作、字符操作、对象操作、网络操作等,然后更进一步的了解不同工具类的方法和使用;其次对nio包的类安模块进行学习理解,如有通道、缓冲、选择器、选择键(包含关联关系)等;之后再通过了解操作系统层面的一些特性,进一步理解阻塞IO和非阻塞IO的内涵,学习理解在工程服务中是怎样用不同的模式构建通信处理模块及其相应优劣势。
希望我的知识总结梳理能给您的阅读带来收获 。
PS:由于内容较多,部分内容待更新补充。。。。
二、思维导图
![](https://img-blog.csdnimg.cn/img_convert/6d3e2b35d1ada55e3212f4a67a83a3ed.png)
三、知识要点
1、理解FilterInputStream
![](https://img-blog.csdnimg.cn/img_convert/45b6e471e888783504eabefbc5579a71.png)
FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。实例化一个具有缓存功能的字节流对象时,只要在FileInputStream对象上再套一层BufferedInputStream即可使用 。
public static void copyFile(String src, String dist) throws IOException {
// 直接从磁盘读
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
// 从内存读 缓存大小 8192
BufferedInputStream buffered= new BufferedInputStream(in);
byte[] buffer = new byte[ 1024];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof,即文件尾
while ((cnt = buffered.read(buffer)) != -1) {
out.write(buffer, 0, cnt);
}
// 装饰着会去调用inputStream的close()方法
buffered.close();
out.close();
}
2、编码和解码
由于存在各个国家地区对字符编码标准的不统一,因此在编码和解码所用的编码方式不是一致的情况下,会出现乱码
/**
GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
UTF-16be 编码中,中文字符和英文字符都占 2 个字节, be 指的是 Big Endian, 大端
UTF-16le,le 指的是 Little Endian ,小端
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
**/
/**String 的编码方式 一般为 UTF-8 **/
String str1 = "文本内容";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
3、字符流与字节流的转换
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
public static void readFileContent(String src) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(src));
String line ;
while ((line = reader.readLine()) != null) {
System.out.println( line);
}
reader.close();
}
4、transient关键字
在序列化与反序列化场景中,transient 关键字可以使一些属性不会被序列化。
一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。
public static void main(String[] args) {
User user = new User() ;
user.setVersion("1.1");
user.setUsername("zhangsan_ sxx");
user.setCode("fsjjkjsfs");
user.setGender("male");
serializeToFile(FileConfig.FILE_SERIALIZE_OBJECT_FILE,user);
User user1 = deserializeFromFile(FileConfig.FILE_SERIALIZE_OBJECT_FILE);
System.out.println("version = " + user1.getVersion());
System.out.println("userName = " + user1.getUsername());
System.out.println("code = " + user1.getCode());
System.out.println("gender = " + user1.getGender());
user.setVersion("2.3");
User user2 = deserializeFromFile(FileConfig.FILE_SERIALIZE_OBJECT_FILE);
System.out.println("version = " + user2.getVersion());
System.out.println("userName = " + user2.getUsername());
System.out.println("code = " + user2.getCode());
System.out.println("gender = " + user2.getGender());
}
public static void serializeToFile(String fileName,User user) {
try{
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(fileName));
os.writeObject(user);
os.flush();
os.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static <T> T deserializeFromFile(String fileName ) {
try{
ObjectInputStream in = new ObjectInputStream(new FileInputStream(fileName));
T t = (T)in.readObject();
in.close();
return t ;
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
5、流与块的理解
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.* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
6、缓冲区的读写操作
class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0; // 当前已经读写的字节数
private int limit; // 还可以读写的字节数
private int capacity; // 容量
public Buffer reset();
public Buffer clear();
public Buffer flip() ;// 翻转 读写模式翻转
public abstract boolean isDirect();
public abstract boolean isReadOnly();
public final boolean hasRemaining() {
return position < limit;
}
}
public static void main(String[] args) {
// 演示复制资源文件
try {
fileCopy(FileConfig.FILE_RESOURCE_SRC_FILE,FileConfig.FILE_RESOURCE_DEST_FILE);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void fileCopy(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();
}
}
7、内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
8、网络编程
![](https://img-blog.csdnimg.cn/img_convert/2ec5f38fb0fcc38c9a1e9d8eeef1c0df.png)
四、应用场景
文件读写
FileInputStream \ FileOutputStream ; FileReader \ FileWriter
2、获取网络地址对应资源
public static void main(String[] args) {
try {
getUrl("https://leetcode.cn/u/wang-chao-xk/");
}catch (IOException e ){
e.printStackTrace();
}
}
private static void getUrl(String urls) throws IOException {
URL url = new URL(urls);
/* 字节流 */
InputStream is = url.openStream();
/* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
String line;
int l =0 ;
while ((line = br.readLine()) != null) {
System.out.printf("line : %s , content: %s ,\n",l++,line);
}
br.close();
}
3、BIO 方式 ServerSocket
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(10000);
Socket socket = server.accept();
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = is.readLine();
System.out.println("received from client : " + line);
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("received data : " + line);
pw.flush();
pw.close();
is.close();
socket.close();
server.close();
}catch (Exception e){
log.error("ServerBIO error" ,e);
}
}
4、NIO 方式 ServerSocket
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(10000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
Handler handler = new Handler(1024);
while (true) {
if (selector.select(3000) == 0) {
System.out.println("等待超时");
continue;
}
System.out.println("处理请求。。");
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
if (key.isAcceptable()) {
handler.handleAccept(key);
}
if (key.isReadable()) {
handler.handleRead(key);
}
} catch (IOException e) {
keyIterator.remove();
continue;
}
keyIterator.remove();
}
}
} catch (Exception e) {
log.error("ServerBIO error", e);
}
}
五、 参考资料
1、Java I/O