IO
IO即Input/Output,内存<——>外部(磁盘,网络等)交互
InputStream / OutputStream是最基本的两种IO流
IO流顺序读写,单向流动,以byte(字节)为最小单位。
Reader/Writer
读写的是字符,按照char读写,字符流传输的最小数据单位是char
Reader和Writer本质上是一个能自动编解码的InputStream和OutputStream。
使用Reader,数据源虽然是字节,但我们读入的数据都是char类型的字符,原因是Reader内部把读入的byte做了解码,转换成了char。使用InputStream,我们读入的数据和原始二进制数据一模一样,是byte[]数组,但是我们可以自己把二进制byte[]数组按照某种编码转换为字符串如果数据源不是文本,就只能使用InputStream,如果数据源是文本,使用Reader更方便一些。Writer和OutputStream是类似的。
同步和异步
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStream、OutputStream、Reader和Writer都是同步IO的抽象类,对应的具体实现类,以文件为例,有FileInputStream、FileOutputStream、FileReader和FileWriter。
IO流按照操作对象分类结构图如下
File对象
File对象可以表示文件,也可以表示目录
构造File对象,需要传入文件路径(可以是绝对路径也可以是相对路径)
FIle f = new File("C:\\Windows\\notepad.exe");
Windows以 \ 作为路径分隔符,所以字符串中需要用 \\ 表示一个 \
Linux平台使用 / 作为路径分隔符。
File对象3种形式表示的路径:
- getPath(),返回构造方法传入的路径
- getAbsolutePath(),返回绝对路径
- getCanonicalPath,返回规范路径
文件和目录
调用isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录。
用File对象获取到一个文件时,还可以进一步判断文件的权限和大小:
- boolean canRead():是否可读;
- boolean canWrite():是否可写;
- boolean canExecute():是否可执行;
- long length():文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录。
创建和删除文件
当File对象表示一个文件时,
createNewFile():创建一个新文件
delete():删除该文件
File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。
遍历文件和目录
当File对象表示一个目录时,
- list()和listFiles():列出目录下的文件和子目录名
- boolean mkdir():创建当前File对象表示的目录;
- boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
- boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path
Path对象,它位于java.nio.file包,和File对象类似,但操作更简单,如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。
import java.io.*;
import java.nio.file.*;
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);
}
}
}
InputStream
InputStream是一个抽象类,是所有输入流的超类。这个抽象类定义的一个最重要方法就是int read(),签名如下:
public abstract int read() throws IOException;
read():读取输入流的下一个字节,并返回字节表示的int值(0~255)。读到末尾返回-1,无法继续读取。
InputStream和OutputStream都是通过close()方法来关闭流。关闭流就会释放对应的底层资源。
文件不存在导致无法读取,没有写权限导致写入失败,等等,这些底层错误由Java虚拟机自动封装成IOException异常并抛出。所有与IO操作相关的代码都必须正确处理IOException。
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}
缓冲
很多流支持利用缓冲区一次读取多个字节。InputStream提供了两个重载方法支持读取多个字节:(返回值是实际读取的字节数)
- int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
- int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
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.");
}
}
}
阻塞
InputStream的read()方法是阻塞的,必须等方法返回后才能继续执行下一行代码。
InputStream实现类
用FileInputStream可以从文件获取输入流,这是InputStream常用的一个实现类。此外,ByteArrayInputStream可以在内存中模拟一个InputStream,ByteArrayInputStream实际上是把一个byte[]数组在内存中变成一个InputStream:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
}
小结
Java标准库的java.io.InputStream定义了所有输入流的超类:
FileInputStream实现了文件流输入;
ByteArrayInputStream在内存中模拟一个字节流输入。
总是使用try(resource)来保证InputStream正确关闭。
OutputStream
OutputStream是最基本的输出流,抽象类,所有输出类的超类。定义的最重要方法是void write(int b),签名如下:
public abstract void write(int b) throws IOException;
write方法只会写入一个字节到输出流。write()重载的一个方法void write(byte [ ])可以一次性写入若干字节。
OutputStream提供了close()方法关闭输出流,释放系统资源。也提供了**flush()**方法将缓冲区的内容真正地输出到目的地(磁盘、网络等,联系操作系统的知识)。
操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。flush()方法,能强制把缓冲区内容输出。
一般不需要手动调用flush()方法,某些情况下必须手动调用,例如在线即时聊天。通过OutputStream的write()方法将用户输入的一句话写入内存缓冲区,缓冲区满了(可能得用户输入N句话)才会再写入网络流,接收方等得花都谢了。解决方法是每输入一句话就调用flush()。
FileOutputStream
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}
阻塞
write()方法也是阻塞的,参考read()方法
OutputStream实现类
用FileOutputStream可以从文件获取输出流。ByteArrayOutputStream可以在内存中模拟一个OutputStream,实际上是把一个byte[ ]数组在内存中变成一个OutputStream。
import java.io.*;
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"));
}
}
同时操作多个AutoCloseable资源时,在try(resource) { … }语句中可以同时写出多个资源,用;隔开。例如,同时读写两个文件:
// 读取input.txt,写入output.txt:
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // transferTo的作用是?
}
Filter模式(装饰器模式)
InputStream根据来源可以包括:
- FileInputStream:从文件读取数据,是最终数据源;
- ServletInputStream:从HTTP请求获取数据,是最终数据源;
- Socket.getInputStream():从TCP连接读取数据,是最终数据源
- …
如果要给InputStram添加缓冲、签名、加解密功能的话就要派生出很多子类,爆炸了啊(一个功能一个子类,功能叠加又很多子类,累死程序员?)。所以不使用继承机制来添加功能,而是使用Filter模式(或者装饰器模式:Decorator)。
JDK将InputStream分为两大类:
- 直接提供数据的基础InputStream,例如:FileInputStream;ByteArrayInputStream;ServletInputStream等。
- 提供额外附加功能的InputStream,例如BufferedInputStream;DigestInputStream;CipherInputStream等。
//基础InputStream,数据来源是文件
InputStream file = new FileInputStream("test.gz");
//用BufferedInputStream包装file这个InputStream,提供缓冲功能
InputStream buffered = new BufferedInputStream(file);
//假设该文件已经用gzip压缩了,我们希望直接读取解压缩的内容,就可以再包装一个GZIPInputStream:
InputStream gzip = new GZIPInputStream(buffered);
无论我们包装多少次,得到的对象始终是InputStream,我们直接用InputStream来引用它,就可以正常读取:
Filter模式通过少量的类实现就能实现各种功能的组合:
编写FilterInputStream
编写FilterInputStream,以便可以把自己的FilterInputStream“叠加”到任何一个InputStream中。
编写一个CountInputStream,对输入的字节进行计数:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] byte = "hello world!".getBytes("UTF-8");
try(CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))){
int n;
while (n = input.read() != -1) {
System.out.println((char)n);
}
System.out.println("Total read" + input.getBytesRead() + "bytes");
}
}
}
class CountInputStream extends FilterInputStream {
private int count = 0;
CountInputStream (InputStream in) {
super(in);
}
public int getByteRead() {
return this.count;
}
public int read() throw IOException {
int n = in.read();
if(n != -1){
this.count ++;
}
return n;
}
public int read(byte[] b, int off, int len) {
int n = in.read(b, off, len);
this.count += n;
return n;
}
}
小结
Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能:
- 可以把一个InputStream和任意个FilterInputStream组合;
- 可以把一个OutputStream和任意个FilterOutputStream组合。
Filter模式可以在运行期动态增加功能(又称Decorator模式)
操作Zip
ZipInputStream是一种FilterInputStream流,可以直接读取zip包的内容
读取zip包
创建ZipInputStream,通常传入一个FileInputStream作为数据源,循环调用getNextEntry(),直到返回null,表示zip流结束。
一个ZipEntry表示一个压缩文件或目录,如果是压缩文件,就用read()方法不断读取,直到返回-1。
try(ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ( (entry = zip.getNextEntry()) != null ) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
写入zip包
ZipOutputStream是一种FilterOutputStream,可以直接写入内容到zip包。先创建一个ZipOutputStream,通常是包装一个FileOutputStream,然后,每写入一个文件前,先调用putNextEntry(),然后用write()写入byte[]数据,写入完毕后调用closeEntry()结束这个文件。
小结
ZipInputStream可以读取zip格式的流,ZipOutputStream可以把多份数据写入zip包;
配合FileInputStream和FileOutputStream就可以读写zip文件。
读取classpath资源
很多java程序启动的时候都需要读取配置文件。
把资源存储在classpath中可以避免文件路径依赖;
Class对象的getResourceAsStream()可以从classpath中读取指定资源;
根据classpath读取资源时,需要检查返回的InputStream是否为null。
序列化
序列化:java对象——>二进制内容(byte[ ]数组)
反序列化:二进制内容(byte[ ]数组)——>java对象
好处:byte[ ]保存到文件或者通过网络传输到远程
Java对象序列化
java对象序列化前提:实现java.io.Serializable接口:没有定义任何方法,只是空接口。
使用ObjectOutputStream,将Java对象变为byte[ ]数组,写入字节流:
public class Main {
public static void main(String[] args) {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try(ObjectOutputStream output = new ObjectOutputStream(buffer)) {
//写入int
output.writerInt(12345);
//写入String,以UTF-8编码
output.writerUTF("Hello");
//写入Object
output.writerObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
反序列化
ObjectInputStream负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
readObject()返回一个Object对象,可以通过强制类型转换转变成特定类型。
readObject()可能抛出的异常有:
- ClassNotFoundException:没有找到对应的Class;
常见于一台电脑上的Java程序把一个Java对象,例如,Person对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。 - InvalidClassException:Class不匹配。
常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
解决办法:java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”,通常由IDE自动生成。
反序列化特点:由JVM直接构造出对象,不调用构造方法
安全性
Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法。(一看就是流氓行为)。一个精心构造的byte[]数组被反序列化后可以执行特定的Java代码,从而导致严重的安全漏洞。Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。
更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
Reader
java.io.Reader是所有字符输入流的超类,最主要的方法是:
public int read() throws IOException; //读取字符流的下一个字符,返回字符表示的int,范围是0~65535,读到末尾返回-1
重载方法:int read(char[ ] c):一次性读取若干字符并填充到char[ ]数组
public int read(char[] c) throws IOException;
FileReader
FileReader是Reader的一个子类,它可以打开文件并获取Reader。
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
//设置缓冲区buffer
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
CharArrayReader
CharArrayReader可以在内存中模拟一个Reader,实际上是把一个char[]数组变成一个Reader,这和ByteArrayInputStream非常类似:
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
StringReader
StringReader可以直接把String作为数据源,它和CharArrayReader几乎一样:
try (Reader reader = new StringReader("Hello")) {
}
InputStreamReader
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。
既然Reader本质上是一个基于InputStream的byte到char的转换器,那么,如果我们已经有一个InputStream,想把它转换为Reader,是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader。构造InputStreamReader时,我们需要传入InputStream,还需要指定编码,就可以得到一个Reader对象。
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}
Writer
Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。
FileWriter
FileWriter就是向文件中写入字符流的Writer。
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
CharArrayWriter
CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
StringWriter
StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。
OutputStreamWriter
除了CharArrayWriter和StringWriter外,普通的Writer实际上是基于OutputStream构造的,它接收char,然后在内部自动转换成一个或多个byte,并写入OutputStream。因此,OutputStreamWriter就是一个将任意的OutputStream转换为Writer的转换器:
PrintStream和PrintWriter
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
写入int:print(int)
写入boolean:print(boolean)
写入String:print(String)
写入Object:print(Object),实际上相当于print(object.toString())
…
以及对应的一组println()方法,它会自动加上换行符。
我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据。其中,System.out是系统默认提供的PrintStream,表示标准输出:
PrintStream和OutputStream相比,除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException。
PrintWriter
rintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据。
使用Files
Java7开始,提供了Files和Paths这两个工具类。
import java.nio.*;
//把一个文件的全部内容读取为一个byte[]
byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
//文本文件可以把文件全部内容读取为String
//默认使用UTF-8编码读取
String content1 = Files.readString(Paths.get("/path/to/file.txt"));
//可指定编码
String content2 = Files.readString(Paths.get("/path/to/file.txt"),StandardCharsets.ISO_8859_1);
//按行读取并返回每行内容
List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt");
//写入文件也很方便
// 写入二进制文件:
byte[] data = ...
Files.write(Paths.get("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Paths.get("/path/to/file.txt"), lines);
Files工具类还有copy()、delete()、exists()、move()等快捷方法操作文件和目录。
Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。读写大型文件仍然要使用文件流,每次只读写一部分文件内容。