文章目录
- 一. Java BIO
- 二. File
- 三. InputStream
- 四. OutputStream
- 五. Reader
- 六. Writer
- 七. Print___
- 八. ZipStream
- 九. BufferedStream
- 十. 番外篇
- 1️⃣、Properties
- 2️⃣、其他 BIO类简介
- 3️⃣、Java NIO
- 4️⃣、Java AIO
- 附录
前置概念:
-
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中用的比较少,相反在其他软件中用的比较多。
附录