IO
IO是指Input/Output,即输入和输出。以内存为中心
Input指从外部读入数据到内存 例如,把文件从磁盘读取到内存,从网络读取数据到内存等等
Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。
I是把外部输入内存,O是从内存输出
IO的传输方式: 字节 和 字符
字节流接口:InputStream/OutputStream; 二进制数据以byte为最小单位在InputStream/OutputStream中单向流动
字符流接口:Reader/Writer。 字符数据以char为最小单位在Reader/Writer中单向流动
同步IO和异步IO
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低
异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂
File
File f = new File("C:\\Windows\\notepad.exe");
构造File对象时,既可以传入绝对路径,也可以传入相对路径
获取路径的三种方式:
getPath() 获取的是相对路径
getAbsolutePath() 获取的是绝对路径
getCanonicalPath() 获取的是规范的绝对路径 就是会把.. .计算之后转换一下
File.separator 这个是根据系统来判断是 \ 还是 /
文件和目录
File对象既可以表示文件,也可以表示目录
构造一个File对象 即使传入的文件或目录不存在,代码也不会出错
因为构造一个File对象,并不会导致任何磁盘操作 只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作
isDirectory
isFile
boolean canRead():是否可读;
boolean canWrite():是否可写;
boolean canExecute():是否可执行;
long length():文件字节大小。
对目录而言,是否可执行表示能否列出它包含的文件和子目录
创建和删除文件
创建文件
File f = new File("/usr/local/aa");
f.createNewFile()
删除文件
f.delete()
创建临时文件
File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
遍历文件和目录
当File对象表示一个目录时,
可以使用list()和listFiles()列出目录下的文件和子目录名。
listFiles()提供了一系列重载方法,
可以过滤不想要的文件和目录
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir():创建当前File对象表示的目录;
boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path
Java标准库还提供了一个Path对象,它位于java.nio.file包。Path对象和File对象类似,但操作更加简单
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就是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能
InputStream不是一个接口,而是一个抽象类,这是所有输入流的超类,这个类里最重要的一个方法就是 int read()
当read返回的是-1 时,说明读取完毕
InputStream input = null;
try {
input = new FileInputStream("src/readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
但时上边这种写法不够简练
需要使用java7的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源
推荐这种
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
缓冲
read的时候一个一个读取效率很低
很多流支持一次性读取多个字节到缓冲区
InputStream提供了两个重载方法来支持读取多个字节:
int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,
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实现类
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);
}
}
阻塞
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
执行到第二行代码时,必须等read()方法返回后才能继续。
注意:重点和易错点
很重要,之前一次美观网络请求接口,导致docker死了
在计算机中,类似文件、网络端口这些资源,都是由操作系统统一管理的
应用程序在运行的过程中,如果打开了一个文件进行读写,完成后要及时关闭,让操作系统的资源释放掉
否则,应用程序占用的资源会越来越多,不但白白占用内存,还会影响其他应用程序的运行
OutputStream
跟InputStream基本上一样 也是抽象类
里边的重要方法就是 write(int b) 注意参数是int
但是这个方法会写入一个字节到输出流。要注意的是,虽然传入的是int参数,
但只会写入一个字节,即只写入int最低8位表示字节的部分
缓冲区
因为磁盘和网络写入数据的时候,出于效率的考虑,操作系统不会输出一个字节,就马上写入文件或者发送
只有当超过了缓冲区,才会发送消息。
如果要想写入的文件,没超过缓冲区的时候也写入文件,需要手动调用flush()
阻塞
和InputStream一样,OutputStream的write()方法也是阻塞的
OutputStream实现类
ByteArrayOutputStream的实现
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"));
Filter模式
FileInputStream:从文件读取数据,是最终数据源;
ServletInputStream:从HTTP请求读取数据,是最终数据源;
Socket.getInputStream():从TCP连接读取数据,是最终数据源;
各种各样的读流模式,如果要想扩展怎么办。肯定不是对具体的某个实现方法进行扩展,那样面向对象就没意义了
JAVA提供了
┌─────────────┐
│ InputStream │
└─────────────┘
▲ ▲
┌────────────────────┐ │ │ ┌─────────────────┐
│ FileInputStream │─┤ └─│FilterInputStream│
└────────────────────┘ │ └─────────────────┘
只需要定义自己的FilterInputStream 就可以了,还可以写多个进行组合使用。
例子:
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "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 getBytesRead() {
return this.count;
}
public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
this.count += n;
return n;
}
}
操作Zip
ZipInputStream是一种FilterInputStream
JarInputStream->ZipInputStream->InflaterInputStream->FilterInputStream->InputStream
JarInputStream它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件
读取zip包
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包
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName())); // 考虑文件目录输入相对路径
zip.write(getFileDataAsBytes(file));
zip.closeEntry();
}
}
读取classpath资源
在classpath中的资源文件 路径总是以/开头,我们先获取当前的Class对象,
然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
// TODO:
}
Class对象的getResourceAsStream()可以从classpath中读取指定资源
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));
序列化
什么是序列化?
序列化就是把一个java对象变成二进制,本质上就是一个byte[]数组
为什么要序列化?
因为序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了
什么是反序列化?
即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化
保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下
public interface Serializable {
}
Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface)
实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法
怎么进行序列化?
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456)); // 因为Double已经实现了Serializable接口
}
System.out.println(Arrays.toString(buffer.toByteArray()));
ObjectOutputStream既可以写入基本类型,如int,boolean,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
怎么进行反序列化?
和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
readObject是很有可能抛出异常的
1. ClassNotFoundException 找不到类
2. InvalidClassException 类不匹配
通常为了防止异常发生,通常用IDE生成一个serialVersionUID静态变量
如果增加或修改了字段,
可以改变serialVersionUID的值,
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
安全性
因为Java的序列化机制可以导致一个实例能直接从byte[]数组创建,而不经过构造方法,
可序列化的Java对象必须实现java.io.Serializable接口
类似Serializable这样的空接口被称为“标记接口”(Marker Interface);
Reader
Reader和InputStream的区别是什么?
InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取
InputStream Reader
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)
FileReader
FileReader是Reader的一个子类,它可以打开文件并获取Reader。下面的代码演示了如何完整地读取一个FileReader的所有字符:
public void readFile() throws IOException {
// 创建一个FileReader对象:
// Reader reader = new FileReader("src/readme.txt"); // 字符编码是??? 但如果文件中包含中文,就会出现乱码
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
for (;;) {
int n = reader.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 打印char
}
reader.close(); // 关闭流
}
还提供了一次性读取到char[]数组的方法
public int read(char[] c) throws IOException
利用这个方法,我们可以先设置一个缓冲区,然后,每次尽可能地填充缓冲区:
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
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")) {
}
Reader和InputStream有什么关系?
除了特殊的CharArrayReader和StringReader,
普通的Reader实际上是基于InputStream构造的,
因为Reader需要从InputStream中读入字节流(byte),
然后,根据编码设置,再转换为char就可以实现字符流。
如果我们查看FileReader的源码,它在内部实际上持有一个FileInputStream。
try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}
上述代码实际上就是FileReader的一种实现方式。
使用InputStreamReader,可以把一个InputStream转换成一个Reader。
Writer
Reader是带编码转换器的InputStream,它把byte转换为char,
而Writer就是带编码转换器的OutputStream,它把char转换为byte并输出
OutputStream Writer
字节流,以byte为单位 字符流,以char为单位
写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
无对应方法 写入String:void write(String s)
Writer是所有字符输出流的超类,它提供的方法主要有
FileWriter
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的转换器:
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}
上述代码实际上就是FileWriter的一种实现方式。这和上一节的InputStreamReader是一样的
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打印各种数据
PrintStream和OutputStream相比
除了添加了一组print()/println()方法,可以打印各种数据类型,比较方便外,
它还有一个额外的优点,就是不会抛出IOException,这样我们在编写代码的时候,就不必捕获IOException
PrintWriter
PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,
它的print()/println()方法最终输出的是char数据。两者的使用方法几乎是一模一样的: