IO
File对象
在计算机系统中,文件是非常重要的存储方式。Java的标准库java.io提供了File对象来操作文件和目录。
要构造一个File对象,需要传入文件路径:
File f = new File("C:\\Windows\\notepad.exe");
构造File对象时,既可以传入绝对路径,也可以传入相对路径。
Windows平台使用\
作为路径分隔(Java字符串中需要用\\
表示一个),Linux平台使用/
作为路径分隔符.
- 相对路径
用.
表示当前目录,..
表示上级目录。
// 绝对路径是C:\Docs\sub\javac
File f1 = new File("sub\\javac");
// 绝对路径是C:\Docs\sub\javac
File f3 = new File(".\\sub\\javac");
// 绝对路径是C:\sub\javac
File f3 = new File("..\\sub\\javac");
- 绝对路径
File f = new File("C:\\Windows\\notepad.exe");
getPath()
返回构造方法传入的路径
…
getAbsolutePath()
返回绝对路径
C:\Users\ThinkPad\IdeaProjects\Algorithm…
getCanonicalPath()
它和绝对路径类似,但是返回的是规范路径。
C:\Users\ThinkPad\IdeaProjects
File.separator根据当前平台打印" \ “或” / "
文件和目录
File对象既可以表示文件,也可以表示目录。特别要注意的是,构造一个File对象,即使传入的文件或目录不存在,代码也不会出错,因为构造一个File对象,并不会导致任何磁盘操作。只有当我们调用File对象的某些方法的时候,才真正进行磁盘操作。
isFile()
判断该File对象是否是一个已存在的文件
isDirectory()
判断该File对象是否是一个已存在的文件
canRead()
是否可读
canWrite()
是否可写
canExecute()
是否可执行
length()
文件字节大小
创建和删除文件
File file = new File("/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}
createTempFile()
来创建一个临时文件
deleteOnExit()
在JVM退出时自动删除该文件
遍历文件和目录
list()
和listFiles()
列出目录下的文件和子目录名.
listFiles()
提供了一系列重载方法,可以过滤不想要的文件和目录:
File f = new File("C:\\Windows");
File[] f1 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
boolean mkdir()
:创建当前File对象表示的目录;
boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
path对象
public static void main(String[] args) throws IOException {
// 构造一个Path对象
Path p1 = Paths.get(".", "project", "study");
// 转换为绝对路径
Path p2 = p1.toAbsolutePath();
// 转换为规范路径
Path p3 = p2.normalize();
// 转换为File对象
File f = p3.toFile();
// 可以直接遍历Path
for (Path p : Paths.get("..").toAbsolutePath()) {
System.out.println(" " + p);
}
}
InputStream
InputStream就是Java标准库提供的最基本的输入流.InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。
public abstract int read() throws IOException;
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回 -1表示不能继续读取 了。
FileInputStream fis = new FileInputStream("src/readme.txt");
while (true){
int n = fis.read();
if (n == -1){
break;
}
System.out.println(n);
}
fis.close();
我们需要用try … finally来保证InputStream在无论是否发生IO错误的时候都能够正确地关闭:
public void readFile() throws IOException {
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();
}
}
}
用try … finally来编写上述代码会感觉比较复杂,更好的写法是利用Java 7引入的新的try(resource) 的语法,只需要编写try语句,让编译器自动为我们关闭资源。
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[]数组的偏移量和最大填充数
利用上述方法一次读取多个字节时,需要先定义一个byte[]数组作为缓冲区,read()方法会尽可能多地读取字节到缓冲区, 但不会超过缓冲区的大小。read()方法的返回值不再是字节的int值,而是返回实际读取了多少个字节。如果返回 -1,表示没有更多的数据了。
阻塞
在调用InputStream的read() 方法读取数据时,我们说read()方法是阻塞(Blocking)的。
int n;
// 必须等待read()方法返回才能执行下一行代码
n = input.read();
int m = n;
执行到第二行代码时,必须等read()方法返回后才能继续。因为读取IO流相比执行普通代码,速度会慢很多,因此,无法确定read()方法调用到底要花费多长时间。
读取classpath资源
从classpath读取文件就可以避免不同环境下文件路径不一致的问题:如果我们把default.properties文件放到classpath中,就不用关心它的实际存放路径。
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// TODO:
}
}
如果我们把默认的配置放到jar包中,再从外部文件系统读取一个可选的配置文件,就可以做到既有默认的配置文件,又可以让用户自己修改配置:
Properties props = new Properties();
props.load(inputStreamFromClassPath("/default.properties"));
props.load(inputStreamFromFile("./conf.properties"));
ByteArrayInputStream
ByteArrayInputStream实际上是把一个byte[] 数组在内存中变成一个InputStream,虽然实际应用不多,但测试的时候,可以用它来构造一个InputStream。
byte[] data = { 72, 101, 108, 108, 111, 33 };
InputStream input = new ByteArrayInputStream(data);
ZipInputStream
要创建一个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) {
...
}
}
}
}
OutputStream
和InputStream相反,OutputStream是Java标准库提供的最基本的输出流。
OutputStream也是抽象类,它是所有输出流的超类。这个抽象类定义的一个最重要的方法就是void write(int b).和InputStream一样,OutputStream的write() 方法也是阻塞的。
public abstract void write(int b) throws IOException;
要注意的是,虽然传入的是int参数,但只会写入一个字节,即只写入int最低8位表示字节的部分(相当于b & 0xff
)。
close()
关闭输出流,以便释放系统资源
flush()
向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。
OutputStream os = new FileOutputStream("src/a.txt");
os.write("hello".getBytes("UTF-8"));
os.write(72);
os.close();
Filter模式(装饰器模式)
为了解决依赖继承会导致子类数量失控的问题,JDK首先将InputStream分为两大类:
一类是直接提供数据的基础InputStream,例如:
- FileInputStream
- ByteArrayInputStream
- ServletInputStream
…
一类是提供额外附加功能的InputStream,例如:
- BufferedInputStream
- DigestInputStream
- CipherInputStream
…
//数据来源自文件
InputStream file = new FileInputStream("test.gz");
//提供缓冲的功能来提高读取的效率
InputStream buffered = new BufferedInputStream(file);
//直接读取解压缩的内容
InputStream gzip = new GZIPInputStream(buffered);
无论我们包装多少次,得到的对象始终是InputStream
序列化
序列化后可以把byte[] 保存到文件中,或者把byte[] 通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:
public interface Serializable {
//标记接口
}
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(buffer);
output.writeInt(12345);
output.writeUTF("hello");
output.writeObject(Double.valueOf(123.456));
System.out.println(Arrays.toString(buffer.toByteArray()));
}
反序列化
和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
readObject() 可能抛出的异常有:
-
ClassNotFoundException
:没有找到对应的Class;
对于ClassNotFoundException
,这种情况常见于一台电脑上的Java程序把一个Java对象,例如,Person
对象序列化以后,通过网络传给另一台电脑上的另一个Java程序,但是这台电脑的Java程序并没有定义Person类,所以无法反序列化。 -
InvalidClassException
:Class不匹配。
对于InvalidClassException
,这种情况常见于序列化的Person对象定义了一个int类型的age字段,但是反序列化时,Person类定义的age字段被改成了long类型,所以导致class不兼容。
Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识Java类的序列化“版本”
public class Person implements Serializable {
private static final long serialVersionUID = 2709425275741743919L;
}
反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。 实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String) 的内容,而不存储任何与代码相关的信息。
Reader
Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:
InputStream | Reader |
---|---|
字节流,以byte为单位 | 字符流,以char为单位 |
读取字节(-1,0~255) | 读取字符(-1,0~65535) |
读到字节数组:int read(byte[] b) | 读到字符数组:int read(char[] c) |
java.io.Reader是所有字符输入流的超类,它最主要的方法是:
public int read() throws IOException;
//一次性读取若干字符
public int read(char[] c) throws IOException
FileReader
public void readFile() throws IOException {
// 创建一个FileReader对象:
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.");
}
reader.close(); // 关闭流
}
如果文件中包含中文,就会出现乱码,因为FileReader默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK,打开一个UTF-8编码的文本文件就会出现乱码。
CharArrayReader
try (Reader reader = new CharArrayReader("Hello".toCharArray()))
{
}
StringReader
try (Reader reader = new StringReader("Hello")) {
}
InputStreamReader
除了特殊的CharArrayReader和StringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。
// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");
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是所有字符输出流的超类,它提供的方法主要有:
- 写入一个字符(0~65535):
void write(int c)
; - 写入字符数组的所有字符:
void write(char[] c)
; - 写入String表示的所有字符:
void write(String s)
。
PrintStream
PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:
- 写入int:print(int)
- 写入boolean:print(boolean)
- 写入String:print(String)
- 写入Object:print(Object),实际上相当于print(object.toString())
以及对应的一组println()方法,它会自动加上换行符。
System.out是系统默认提供的PrintStream
System.err是系统默认提供的标准错误输出。