文章目录
前置概念:
BIO
( Blocking I/O ) :同步并阻塞NIO
( New I/O ) :同步非阻塞AIO
( Asynchronous I/O ):异步非阻塞
同步与异步
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
阻塞和非阻塞
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
一. Java BIO
Java中存在两种最基本的 IO流(字节流):
- InputStream
- OutputStream
在此基础上延伸出来了另外两种最基本的字符流:
(Reader与 Writer本质上是能按照当前编码格式自动编解码的 InputStream和 OutputStream)
- Reader
- Writer
关系表
- 字节流 字符流 输入流 InputStream Reader 输出流 OutputStream Writer 字节流可以处理一切文件,而字符流只能处理纯文本文件。
IO流以内存为中心,输入与输出是相对于应用程序而言的。
- input:从外部把数据读到内存中。
- output:把数据从内存中输出到外部。
IO流是一种顺序读写数据模式,它的特点是单向流动,一连串的数据(字符或字节),以先进先出的方式发送信息的通道。数据在其中就像自来水一样在水管中流动,所以把它称为 IO流。
IO流以 byte[ ]为最小单位,byte支持的数据范围为:-128~127,在计算机中通常 8 byte等于1字节。
Java中文字符默认采用 Unicode编码,其编解码方式不同于 UTF-8。
ASCII
:一个英文字母为一个字节,一个中文汉字为两个字节。Unicode
:一个英文为一个字节,一个中文为两个字节。UTF-8
:一个英文字为一个字节,一个中文为三个字节。UTF-16
:一个英文字母或一个汉字都需要 2 个字节(一些汉字需要 4 个字节)。UTF-32
:世界上任何字符的存储都需要 4 个字节。符号
:英文标点为一个字节,中文标点为两个字节。
关于文件读取路径,分为三种情况:
Linux类操作系统:/
Windows操作系统:\
但在代码中
\
表示转义字符串,所以要用\\
表示\
。 同时绝对路径要以磁盘符号开头,如
C:\\Windows\\note.md
classpath类路径下:/ ,此种方式为可以采用、不采用
同步和异步:
- 同步 IO指:读写 IO时,代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是 CPU执行效率低。
- 异步 IO指:读写 IO时仅发出请求,然后立刻执行后续代码,它的优点是 CPU执行效率高,缺点是代码编写复杂。
- Java标准库的包 java.io提供了同步IO,而 java.nio则是异步IO。
- 以上讨论的InputStream、OutputStream、Reader、Writer全部都是同步 IO模型。
缓冲区的意义:
我们知道,程序与磁盘的交互相对于内存运算很慢,容易成为程序的性能瓶颈。减少程序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
try( ) 语句诠释
实际上编译器并不会特别地为 InputStream加上自动关闭。编译器只看 try(resource = …)中的对象是否实现了 java.lang.AutoCloseable接口,如果实现了,就自动加上 finally语句并调用 close( )方法。InputStream和 OutputStream都实现了这个接口,因此可以用在 try(resource)中。
Java中对文件的读取采用了 Filter模式,也就是 Decorate 装饰者模式。
字符流底层默认使用到了缓冲区,而字节流没有。
二. File
简介:
- 既可以表示文件,又可以表示目录。
- 用来操作文件,不能操作文件中的数据。
- File在构建对象时并不会产生真正的磁盘 IO操作,只有当被调用时才会发生磁盘 IO操作。所以当构建一个并不存在的文件或者目录时并不会发生错误。
获取当前系统的分隔符:
final String SEPARATOR = File.separator;
构造方法:
常用方法:
常用方法扩充与解释性说明:
long length()
:文件字节大小。mkdir()
:创建新目录(父目录不存在则报错)mkdirs()
:创建新目录(父目录不存在则自动创建)delete()
:只有当目录为空时才能删除,否则报错。createTempFile()
:创建临时文件,JVM退出时自动删除。File f = File.createTempFile("tmp-", ".txt");
判断 是否存在以及类型
exists()
:是否存在isDirectory()
:目录isFile()
:文件
底层表示的码并不一样,分别为:存在、文件、目录、隐藏
遍历文件和目录
使用
list()
或者listFiles()
可以遍历文件和目录,两者的区别在于:- list:返回字符型数组,只能显示最后的文件名。
- listFiles:返回文件对象数组,不仅能显示全部的路径,而且可以设置返回的限制条件。
String[] list = file.list(); File[] files = file.listFiles(); File[] files2 = f.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".exe"); // 仅列出 .exe文件 }});
Path对象(了解):
Java标准库提供了一个 Path对象,位于 java.nio.file包,Path对象与 File对象类似。
Path p1 = Paths.get(".", "project", "study"); Path p2 = p1.toAbsolutePath(); // 转换为绝对路径 File fe = p3.toFile(); // 转换为File对象
Files类
从 JDK 7开始,Java提供了
Files
工具类,极大的方便了我们读写文件。简单范例:
- 将某文件的全部内容读取为
byte[]
byte[] data = Files.readAllBytes(Path.of("./file.txt"));
- 如果是文本文件,则可以将其全部内容读取为
String
// 默认使用UTF-8编码读取: String content1 = Files.readString(Path.of("./file.txt")); // 按行读取并返回每行内容: List<String> lines = Files.readAllLines(Path.of("./file.txt"));
注意事项:
- 此外,Files工具类还有
copy()
、delete()
、exists()
、move()
等快捷方法操作文件和目录。 - 最后需要特别注意的是,
Files
提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。
- 将某文件的全部内容读取为
三. InputStream
简介:
- 是抽象类
- 是所有输入流的父类
read()
方法诠释它是 InputStream类中最重要的方法
它会读取输入流中的下一个字节,并返回字节表示的 int值(0~255)。如果已经读到末尾,则返回 -1表示结束。
尽量使用有参的形式建立缓冲区,默认一次读取一个字节的方式效率很低。
public abstract int read() throws IOException;
read()
返回值为什么是 Integer类型:- 返回值范围被定义为 0~255。
- 字节在计算机中用补码表示、存在正负数,byte的范围是 -128~127,并不符合条件。
- 采用 Integer + 高位补零的方式,只取后 8 byte的数据,我们就可以得到 0~255。
read()
有参、无参区分:两者虽然都是返回 int类型的数据,但是所表示的含义却大不相同!
int read()
:每次读取 1个字节,即 8 bit的数据。返回该 8 bit数据所表示的十进制数。
int read(byte[2048])
本例中每次最多读取 2048个 byte的数据。返回实际读取到的 byte数量。
三种子类:
FileInputStream
:从文件读取数据,是最终数据源;ServletInputStream
:从HTTP请求读取数据,是最终数据源;Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
简单范例:(读取并打印)
public class Test01 {
public static void main(String[] args) throws IOException {
FileInputStream file = new FileInputStream("../1.txt");
int n;
StringBuilder stringBuilder = new StringBuilder();
while (true){
n=file.read();
if (n == -1) break;
stringBuilder.append((char) n);
}
System.out.println(stringBuilder);
}
四. OutputStream
简介:
- 抽象类。
- 大多数定义与 InputStream类似。
write()
方法诠释:- 它是 OutputStream中最主要的方法。
- 虽然传入类型是 Integer,但是默认每次只写入一个字节,即取 Integer类型的后 8位。
public abstract void write(int b) throws IOException;
public void writeFile() throws IOException { OutputStream output = new FileOutputStream("./readme.txt"); output.write(72); // H output.write(101); // e output.close(); }
字符串转 byte[ ],然后写入 OutputStream:
(
getBytes()
方法使我感到新颖)public void writeFile() throws IOException { OutputStream output = new FileOutputStream("out/readme.txt"); output.write("Hello".getBytes("UTF-8")); output.close(); }
flush()
阐述作用:刷新,将缓冲区内容真正地输出到目的地。
为什么存在?
在我们向磁盘、网络写入数据时,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上 byte[ ]数组),等到缓冲区写满了,再一次性写入文件或者网络。
对于很多 IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以 OutputStream有个 flush( )方法,能强制把缓冲区内容输出。
何时使用?
通常情况下我们并不需要调用该方法。当缓冲区写满数据时,OutputStream会自动调用,并且在调用 close( )方法关闭 OutputStream之前,也会自动调用方法。
但是,在某些情况下我们必须手动去调用该方法,比如消息的及时更新机制。
close()
阐述 在执行 close方法之前会默认先执行 flush方法刷新缓冲区。
write方法 有参与无参 的区别:
- 无参时:虽然每次读取的是 int类型的值,但是 write只会取的它的后 8位 byte数,然后当做一个字节读入内存当中。
- 有参时:根据 byte[__] 的大小进行读取。比如 byte[ 2048 ] 则表示一次最多可读取 2048比特的数据,是无参时的 256倍。
五. Reader
简介:
- 抽象类
- 本质上是带编码转换器的 InputStream,能把 byte按当前编码格式转换为 char。
主要方法:
读取字符流的下一个字符,并返回字符表示的 int值,范围是 0~65535(2的16次方)。如果已读到末尾,返回-1。
public int read() throws IOException;
FileReader
子类,存在默认编码格式(与系统相同)。
public class Test01 { public static void main(String[] args) throws IOException { Reader reader = new FileReader("./1.txt"); while(true) { int n = reader.read(); // 反复调用read()方法,直到返回-1 if (n == -1) break; System.out.print((char)n); } reader.close(); }}
指定编码格式
- JDK8
InputStream stream = new FileInputStream("./1.txt"); FileReader fileReader = new InputStreamReader(stream,"UTF-8");
- JDK13
Reader reader = new FileReader("./1.txt","UTF-8");
建立缓冲区
char[] buffer = new char[1024];
InputStreamReader
- 可以把任意的 InputStream转换为 Reader
InputStream input = new FileInputStream("src/readme.txt"); // 变换为Reader: Reader reader = new InputStreamReader(input, "UTF-8");
六. Writer
简介:
与 Reader相反,Writer是带编码转换器的 OutputStream,它把 char转换为 byte并输出。
Writer是所有字符输出流的父类。
七. Print___
代指 PrintStream与 PrintWriter
PrintStream与PrintWriter的区别
- PrintStream本质是 FilterOutputStream。
- PrintWriter本质是 Writer。
- PrintStream最终输出的是 byte数据,而 PrintWriter最终输出的是 char数据。
- 除此之外两者的使用方法几乎是一模一样的。
构造方法参数:
常用方法:
print(int)
:不支持换行print(boolean)
print(String)
print(Object)
println( ... )
:支持换行
注意点:
我们经常使用的
System.out.println()
,其里面包含的System.out
就是系统默认提供的 PrintStream,表示标准输出。 而标准错误输出采用
System.err
表示。
八. ZipStream
简介:
- 分为 ZipInputStream 与 ZipOutputStream。
ZipInputStream
是一种 FilterInputStream,它可以直接读取zip包的内容。ZipOutputStream
是一种 FilterOutputStream,它可以直接将内容写入到 zip包。
继承结构图
ZipInputStream
使用步骤:创建对象
循环调用 getNextEntry ( ) 获取 ZipEntry对象,直到返回 null。
(一个 ZipEntry表示一个压缩文件或者目录)
判断类型
- 文件:使用 read ( ) 方法读取,直到返回 -1。
- 目录:跳过本次循环
**注意点:**判断时用的是 ZipEntry对象,但是读取时用的是 ZipInputStream对象。
简单实现
public class Test01 { public static void main(String[] args) throws IOException { InputStream file = new FileInputStream("3.zip"); ZipInputStream zip = new ZipInputStream(file); ZipEntry entry; StringBuilder builder = new StringBuilder(); int read = 0; while ((entry = zip.getNextEntry()) != null) { if (!entry.isDirectory()) { while (true) { read = zip.read(); if (read == -1) break; builder.append((char) read); }}System.out.println(builder);}}}
ZipOutputStream
使用步骤:- 创建对象
- 在每写入一个文件前,调用 putNextEntry( )方法
- 然后用 write( )写入 byte[ ]数据
- 最后再调用 closeEntry( )结束这个文件的打包。
九. BufferedStream
简介:
- 字节缓冲流:BufferedInputStream 与 BufferedOutputStream
- 字符缓冲流:BufferedReader 与 BufferedWriter
- 为高效率而设计,但是真正的读写操作还是靠 FileOutputStream和 FileInputStream。
默认缓冲区大小
DEFAULT_BUFFER_SIZE = 8192;
BufferedReader 存在
readLine()
方法,其他不存在。
十. 番外篇
1️⃣、Properties
简介:
Properties(Java.util.Properties),该类主要用于读取Java的配置文件,不同的编程语言有自己所支持的配置文件,配置文件中很多变量是经常改变的,为了方便用户的配置,能让用户够脱离程序本身去修改相关的变量设置。就像在Java中,其配置文件常为 .properties文件,是以键值对的形式进行参数配置的。
Properties继承自Hashtable,而 Hashtable又实现了 Map接口,所以 Properties里可以使用 get与 put 方法的,但由于方法的参数对象是 Object 而不是 String,所以一般不用。常用方法是 setProperties或者 getProperties。
使用步骤:
- 创建 Properties对象
- load加载指定文件
- 用键获取值
简单实现
public class Test01 {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
properties.load(new FileInputStream("./1.properties"));
System.out.println(properties.getProperty("a"));;
}
}
2️⃣、其他 BIO类简介
DataInputStream
数据输入流,它是用来装饰其它输入流,作用是“允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型”。
PipedInputStream
管道字节输入流,能实现多线程间的管道通信。
ByteArrayInputStream
字节数组输入流,从字节数组 ( byte[ ] ) 中进行以字节为单位的读取,也就是将资源文件都以字节的形式存入到该类中的字节数组中去。
FilterInputStream
装饰者类,具体的装饰者继承该类,这些类都是处理类,作用是对节点类进行封装,实现一些特殊功能。
ObjectInputStream
对象输入流,用来提供对基本数据或对象的持久存储。通俗点说,也就是能直接传输对象,通常应用在反序列化中。它也是一种处理流,构造器的入参是一个InputStream的实例对象。
PipedReader
管道字符输入流。实现多线程间的管道通信。
CharArrayReader
从 Char数组中读取数据的介质流。
StringReader
从 String中读取数据的介质流。
3️⃣、Java NIO
简介:
NIO
:New IO即 新IO,Non-blocking非堵塞BIO面向流,NIO面向块(缓冲区)
Java 1.4中引入了 NIO框架,NIO 核心组件包括:
Channel
(通道)Buffer
(缓冲区)Selector
(选择器)
整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。
任何时候访问 NIO中的数据,都是通过缓冲区进行操作。
NIO 通过Channel(通道) 进行读写:通道是双向的,可读也可写,而流的读写是单向的。
无论读写,通道只能和Buffer交互。也是因为 Buffer,所以通道可以异步地读写。
Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。
Selectors 用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。
为什么大家都不愿意用 JDK 原生 NIO 进行开发呢?
- 编程复杂、不好用。
- JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%
- 项目庞大之后,自行实现的 NIO 很容易出现各类 bug,维护成本较高。
Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。
CSDN NIO详解文章:链接
简单实现( copy程序 ):
public static void copyFileUseNIO(String src,String dst) throws IOException{
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
int eof =inChannel.read(buffer);
if(eof==-1){ break; } //判断是否读完文件
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
inChannel.close(); outChannel.close();
fi.close();fo.close();
}
- Selector管理示意图
4️⃣、Java AIO
- 简介:
- AIO:Asynchronous I/O,即 NIO 2。
- 在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。
- 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
- 感想:其实这几种思想感觉在 Java中用的比较少,相反在其他软件中用的比较多。