IO简介
IO是指Input/Output,即输入和输出。以内存为中心:
- Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
- Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等
同步和异步
- 同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。如read()方法必须等返回结果后才会再次执行
- 方法同步也称之为阻塞
- 而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io
提供了同步IO,而java.nio
则是异步IO。上面我们讨论的InputStream
、OutputStream
、Reader
和Writer
都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream
、FileOutputStream
、FileReader
和FileWriter
。
IOException
IO异常可能是流找不到文件(即可能会出现文件存在但是因为某些原因读取不到的情况)等等
需要注意的一点是:输入流可以正常读取的情况下,如果还没有读取完就关闭(read还没返回-1),是会产生IO异常的
字节与字符
字节是内存占用空间,字符是可以看到的占用空间
IO流中字节默认都是Ascii编码。string默认编码是utf-8
- ASCII 码中,一个英文字母(不分大小写)为一个字节,一个中文汉字为两个字节。
- Unicode 编码中,一个英文为一个字节,一个中文为两个字节。
- UTF-8 编码中,一个英文字为一个字节,一个中文为三个字节。
IO结构图
Hutool中的io工具类
```xml
cn.hutool hutool-all 5.4.5 test ```java
FileReader fileReader = new FileReader("public/index.html"); //会从classpath下查找文件,开头不要加/ //默认UTF-8编码,可以在构造中传入第二个参数做为编码
String result = fileReader.readString(); //获取整个文件内容
List<String> res=fileReader.readLines(); //按行读取
File
要注意的是,构造一个
**File**
对象,存储的仅仅只是些基本信息,并没有创建流。所以即使传入的文件或目录不存在,代码也不会出错。即File没有保存文件内容数据(一般用于构造流,file作为流的数据来源信息,当然也可以直接传入字符串路径)- 用于创建文件/文件夹对象,存储文件路径与文件名信息
isFile()
判断该File
对象是否是一个已存在的文件isDirectory()
判断该File
对象是否是一个已存在的目录getAbsolutePath()
获取文件路径文件信息判断:
创建对象时路径可以传
绝对路径
或者相对路径
- 绝对路径
File f = new File("C:\\Windows\\notepad.exe");
- 绝对路径
windows平台使用\
作为路径分隔符,在Java字符串中需要用\\
或者/
。Linux平台使用/
java使用2个\理解为转义字符就好记了
- 相对路径 可以用
.
表示当前目录,..
表示上级目录。相对路径前面加上当前目录就是绝对路径
路径方法:getPath()
,返回构造方法传入的路径getAbsolutePath()
,返回绝对路径getCanonicalPath
,它和绝对路径类似,但是返回的是规范路径。
文件的创建与删除
- 创建文件:
createNewFile()
- 创建临时文件:
createTempFile()
- JVM退出时自动删除临时文件
deleteOnExit()
- JVM退出时自动删除临时文件
删除文件:
delete()
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀 f.deleteOnExit(); // JVM退出时自动删除
目录的创建和删除
boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。遍历文件和目录
**list()**
返回一个目录下所有文件的文件名信息 返回值String**listFiles()**
返回一个目录下所有文件和目录的完整路径信息 返回值为File类型数组- 该方法还有很多重载方法可以提供过滤功能。使用过滤功能必须传入FilenameFilter()接口的实现类对象并实现接口方法
**accept()**
- FilenameFilter:文件名过滤器
accept(File file,String name)
将file对象中的文件名name逐个提取出来进行比对过滤endswith(String str)
:判断字符串是否以给定字符串结尾
- FilenameFilter()是单方法接口。所以可以借助匿名内部类或者Lambda简化
- 该方法还有很多重载方法可以提供过滤功能。使用过滤功能必须传入FilenameFilter()接口的实现类对象并实现接口方法
注意list只返回目录下的文件!
public static void main(String[] args) throws IOException { File f = new File("C:\\Windows"); File[] fs1 = f.listFiles(); // 列出所有文件和子目录 printFiles(fs1); File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件 匿名内部类 public boolean accept(File dir, String name) { return name.endsWith(".exe"); // 返回true表示接受该文件 } }); printFiles(fs2); } static void printFiles(File[] files) { System.out.println("=========="); if (files != null) { //避免null文件对象进行操作引起错误 for (File f : files) { //将File数组内的File对象逐个提取出来删除 System.out.println(f); } } System.out.println("=========="); }
Path
Path既是一个类,也是一个接口。它跟File功能类似,但是操作更加简单
- 如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
- Path因为是一个接口,所以构建对象不能new得借助
Path.get()
toPath()
等方法 ```java public class Main { public static void main(String[] args) throws IOException {
} }Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象 System.out.println(p1); Path p2 = p1.toAbsolutePath(); // 转换为绝对路径 System.out.println(p2); Path p3 = p2.normalize(); // 转换为规范路径 System.out.println(p3); File f = p3.toFile(); // 转换为File对象 System.out.println(f); for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path System.out.println(" " + p); }
<a name="JacJT"></a>
# Files
见廖雪峰
<a name="JM2Le"></a>
# IO流通用方法
- 输入输出流通用方法:`close()` 关闭流
- 可借助try(resource)来自动关闭close()
- 输入流通用方法:`read()` 读取字节或者字符
- `read(byte/char[])` 缓冲输入
- 输出流通用方法:
- `write()` 写入字节或者字符
- `flush()` 强制输出缓冲区
- **IO缓冲区本质上都是一个数组**
- **一般不需要使用,缓冲区满了输入流会自动调用。另外**`**close()**`**执行时也会自动调用**`**flush()**`
- **在一些聊天应用中会主动调用flush(),因为一般输入完一句话还没有达到缓冲区上限,也就不会将消息发送出去,这时就需要主动清空缓冲区让消息发送**
<a name="3fh7c"></a>
# InputStream字节输入流
- `InputStream`就是Java标准库提供的最基本的输入流。
- `**InputStream**`**并不是一个接口,而是一个抽象类,它是所有输入流的超类。**
---
- 字节输入流定义的一个最重要的方法就是`read`方法:`public abstract ``**int**`` read() throws IOException;`
- `int available()` 获取输入流来源的总长度
- **一般用于读取磁盘文件,对于网络数据可能不适合,因为网络数据一般有缓冲区,读的可能只是抵达缓冲区的数据就结束了**
- `read(byte[])` 读取字节数组长度的数据,并额外保存到数组中,即流和数组都有数据了
- **类很多记不住?其实文件输入流,字节数组输入流等等就是数据来源的区别,使用方法还是差不多的。输出流同**
<a name="nMzHP"></a>
## FileInputStream文件输入流
- `FileInputStream`是`InputStream`的一个实现类。顾名思义,`FileInputStream`就是从文件流中读取数据。
```java
public static void main(String[] args) {
int n;
try {
File file =new File("C:\\Users\\Lw\\Desktop\\lhy.cpp");
// 构造一个输入流,数据来源为file,此时还为null流
InputStream input = new FileInputStream(file);
while ((n = input.read()) != -1){
System.out.println(n); // 打印byte的值
//要输出字符可以(char)n
input.close(); // 关闭流
}
}catch (IOException e) {
System.out.println("文件不存在");
}
}
也可以把close写在finally里面
finally {
if (input != null) input.close();
}
ByteArrayInputStream
- 输入源是数组的字节输入流,可以在内存中模拟一个InputStream
- 实际上一般不会用输入流输出数组,更多的是用于将文件转为数组或者字符串
public class Main { public static void main(String[] args) throws IOException { String s; try (InputStream input = new FileInputStream("C:\\test\\README.txt")) { int n; StringBuilder sb = new StringBuilder(); while ((n = input.read()) != -1) { sb.append((char) n); } s = sb.toString(); } System.out.println(s); } }
缓冲输入
- 只有输入流才有缓冲
在读取流的时候,一次读取一个字节并不是最高效的方法。很多流支持一次性读取多个字节到缓冲区,对于文件和网络流来说,利用缓冲区一次性读取多个字节效率往往要高很多。InputStream
提供了两个重载方法来支持读取多个字节。Reader也一样的用法,只不过字节换成了字符
int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]
数组作为缓冲区,read()
方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()
方法的返回值不再是字节的int
值,而是返回实际读取了多少个字节。如果返回-1
,表示没有更多的数据
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
// 定义1000个字节大小的缓冲区:
byte[] buffer = new byte[1000];
int n;
while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
System.out.println("read " + n + " bytes.");
}
}
}
public class Main {
public static void main(String[] args) throws IOException {
String s;
try (InputStream input = new FileInputStream("C:\\test\\README.txt")) {
s = readAsString(input);
}
System.out.println(s);
}
public static String readAsString(InputStream input) throws IOException {
int n;
StringBuilder sb = new StringBuilder();
while ((n = input.read()) != -1) {
sb.append((char) n); /*将字节逐个读取并转为char类型,然后追加到可变字符串sb后,然后就可以
组合为一个字符串 */
}
return sb.toString();
}
}
OutputStream字节输出流
- 跟InputStream差不多的结构与定义,也是一个抽象类。
最重要的一个方法是
是OutputStream的一个实现类。即写入对象是文件的输出流
OutputStream output = new FileOutputStream("C:\\Users\\Lw\\Desktop\\12.txt"); output.write(72); // H ascii编码 output.close(); }
将字符串转为数组然后写入
OutputStream output = new FileOutputStream("out/readme.txt"); output.write("Hello".getBytes("UTF-8")); // Hello output.close();
ByteArrayOutputStream
即写入对象是字节数组的输出流。可以通过
toByteArray()
将字节数组输出流对象转变为字节数组public class Main { public static void main(String[] args) throws IOException { byte[] data; try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { output.write("Hello ".getBytes("UTF-8")); output.write("world!".getBytes("UTF-8")); data = output.toByteArray(); } System.out.println(new String(data, "UTF-8")); } }
transferTo
该方法可以将输入流复制给一个输出流,从而实现同时读和写
用法和
IntPutStream
差不多Reader
是Java的IO库提供的另一个输入流接口。和InputStream
的区别是,InputStream
是一个字节流,即以byte
为单位读取,而Reader
是一个字符流,即以char
为单位读取 它是所有字符类的超类,- 最主要的方法
public ``**int**`` read() throws IOException;
读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。public int read(char[] c) throws IOException
读取若干字符并存入数组
StringReader
可以直接把String
作为数据源进行读取Reader reader = new StringReader("Hello");
FileReader
FileReader
是Reader
的一个子类,它的数据源为文件如果我们读取一个纯ASCII编码的文本文件,上述代码工作是没有问题的。但如果文件中包含中文,就会出现乱码,因为
FileReader
默认的编码与系统相关。要避免乱码问题,我们需要在创建FileReader
时指定编码:Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
public void readFile() throws IOException { // 创建一个FileReader对象: Reader reader = new FileReader("src/readme.txt"); // 字符编码是??? for (;;) { int n = reader.read(); // 反复调用read()方法,直到返回-1 if (n == -1) { break; } System.out.println((char)n); // 打印char } reader.close(); // 关闭流 }
CharArrayReader
同
ByteArrayInputStream
就是数据源是字符数组的字节输入流try (Reader reader = new CharArrayReader("Hello".toCharArray())) { }
InputStreamReader字节流转字符流
除了特殊的
CharArrayReader
和StringReader
,普通的Reader
实际上是基于InputStream
构造的,因为Reader
需要从InputStream
中读入字节流(byte
),然后,根据编码设置,再转换为char
就可以实现字符流。如果我们查看FileReader
的源码,它在内部实际上持有一个FileInputStream
。既然
Reader
本质上是一个基于InputStream
的byte
到char
的转换器,那么,如果我们已经有一个InputStream
,想把它转换为Reader
,是完全可行的。InputStreamReader
就是这样一个转换器,它可以把任何InputStream
转换为Reader
。示例代码如下:InputStream input = new FileInputStream("src/readme.txt"); // 变换为Reader: Reader reader = new InputStreamReader(input, "UTF-8");
构造
InputStreamReader
时,我们需要传入InputStream
,还需要指定编码,就可以得到一个Reader
对象。上述代码可以通过try (resource)
更简洁地改写如下:try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) { // TODO: }
上述代码实际上就是
FileReader
的一种实现方式。- 使用
try (resource)
结构时,当我们关闭Reader
时,它会在内部自动调用InputStream
的close()
方法,所以,只需要关闭最外层的Reader
对象即可。 使用InputStreamReader,可以把一个InputStream转换成一个Reader。
Write
Writer
是所有字符输出流的超类,它提供的方法主要有:- 写入一个字符(0~65535):
void write(int c)
; - 写入字符数组的所有字符:
void write(char[] c)
; - 写入String表示的所有字符:
void write(String s)
- 写入一个字符(0~65535):
-
FileWrite
FileWriter
就是向文件中写入字符流的Writer
。它的使用方法和FileReader
类似:try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) { writer.write('H'); // 写入单个字符 writer.write("Hello".toCharArray()); // 写入char[] writer.write("Hello"); // 写入String }
StringWriter
-
OutputStreamWriter
除了
CharArrayWriter
和StringWriter
外,普通的Writer实际上是基于OutputStream
构造的,它接收char
,然后在内部自动转换成一个或多个byte
,并写入OutputStream
。因此,OutputStreamWriter
就是一个将任意的OutputStream
转换为Writer
的转换器:try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) { //... }
PrintStream和PrintWriter
https://www.liaoxuefeng.com/wiki/1252599548343744/1302299230076961
序列化
序列化即把java对象信息转变为二进制内容(byte[]) . 这样以达到持久化对象的实现 显而易见需要使用
ByteArrayOutputStream
- 一个对象能否进行序列化需要看它的类是否实现了
**java.io.Serializable**
该接口未定义任何方法,仅仅是给实现类贴上一个“可以序列化”的标签
- 一个对象能否进行序列化需要看它的类是否实现了
- 反序列化即相反
除二进制以外还有很多其他类型的序列化,如xml,json。虽然 JSON 和 XML 可读性比较好,但是性能较差
除了字节数组输出流,还需要用到对象输出流
ObjectOutputStream
字节数组输出流作为对象输出流的参数,对象输出流会将序列化后的信息写入到字节数组输出流中ObjectOutputStream
可以写入基本类型和String还有实现了Serializable接口的Object- 序列化和反序列化的String都应该使用UTF-8
可以使用
**transient**
对变量修饰,这样该变量不会被序列化(static教程上说有无transient都不会被序列化,实践是有无都会被序列化)反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
read?()
从对象输出流中读取一个?类型的对象。String类型对象的读取方法为readUTF()
- 还可以直接读取一个Object对象,强制转换后进行使用。可能抛出以下异常
- ClassNotFoundException:没有找到对应的Class;
- InvalidClassException:Class不匹配。
- 对于ClassNotFoundException,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。
- 对于InvalidClassException,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
- 还可以直接读取一个Object对象,强制转换后进行使用。可能抛出以下异常
为了避免这种class定义变动导致的不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常可以由IDE自动生成。如果增加或修改了字段,可以改变serialVersionUID的值,这样就能自动阻止不匹配的class版本。即相同的类才具有相同的版本号
- 序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号
public class Person implements Serializable { private static final long serialVersionUID = 2709425275741743919L; }
实操
//-----------------------序列化---------- ByteArrayOutputStream buffer = new ByteArrayOutputStream(); //ObjectOutputStream用于把对象转为字节,下面是转为数组并写入到字节数组输出流,字节数组输出流再获取字节数组 ObjectOutputStream output = new ObjectOutputStream(buffer); int n = input.readInt(); String s = input.readUTF(); Double d = (Double) input.readObject(); byte[] res=buffer.toByteArray(); //-----------------------反序列化------------ //ObjectInputStream用于字节转对象 ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(res)); //注意反序列取得顺序要和存得顺序一致,不能跳过某个元素取 int n = input.readInt(); String s = input.readUTF(); Double d = (Double) input.readObject();
第三方二进制序列化
- 序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列,当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID,如果不手动指定,那么编译器会动态生成默认的序列化号
上述的二进制序列化是jdk自身功能的实现,但是存在2个问题:
- 不支持跨语言调用
- 性能差
常用的第三方的二进制反/序列化有如下框架:
- Kryo
- Protobuf
- ProtoStuff:基于Protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表 ProtoStuff 性能更差
- hessian :dubbo RPC 默认启用的序列化方式是 hessian2 ,但是,Dubbo 对 hessian2 。hessian 2类似hessian 。不过Dubbo是推荐使用Kryo 替代hessian2