一、File对象
File用于操作文件和目录:
File f = new File("C:\\Windows\\notepad.exe");
//即可传入绝对路径,也可以传入相对路径
//可以用.表示当前目录,..表示上级目录。
//Windows平台使用\作为路径分隔符,Java字符串中需要用\\表示一个\。Linux平台使用/作为路径分隔符:
System.out.println(f);
用File获取路径:
-
getPath()
,返回构造方法传入的路径 -
getAbsolutePath()
,返回绝对路径 -
getCanonicalPath
,它和绝对路径类似,但是返回的是规范路径。规范路径即将.和… 转换后的绝对路径 如\System32\…\notepad.exe --> \notepad.exe
文件和目录:
- File对象既可以表示文件,也可以表示目录;
- 构造一个File对象,不会导致磁盘操作,只有当调用File对象的某些方法的时候,文件或目录不存在,会出错;
isFile()
,判断该File
对象是否是一个已存在的文件,调用isDirectory()
,判断该File
对象是否是一个已存在的目录:- 用
File
对象获取到一个文件时,还可以进一步判断文件的权限和大小:boolean canRead()
:是否可读;boolean canWrite()
:是否可写;boolean canExecute()
:是否可执行;long length()
:文件字节大小。
创建和删除文件:
- 当File对象表示一个文件时,可以通过
createNewFile()
创建一个新文件,用delete()
删除该文件: - File对象提供了
createTempFile()
来创建一个临时文件,以及deleteOnExit()
在JVM退出时自动删除该文件。
遍历文件和目录:
-
当File对象表示一个目录时,可以使用
list()
和listFiles()
列出目录下的文件和子目录名。 -
listFiles()
提供了一系列重载方法,可以过滤不想要的文件和目录:File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件 public boolean accept(File dir, String name) { return name.endsWith(".exe"); // 返回true表示接受该文件 } });
-
和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:
boolean mkdir()
:创建当前File对象表示的目录;boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功。
Path:
Path
对象和File
对象类似,如果需要对目录进行复杂的拼接、遍历等操作,使用Path
对象更方便。
二、InputStream
InputStream介绍:
-
InputStream
就是Java标准库提供的最基本的输入流。位于java.io
这个包; -
InputStream
并不是一个接口,而是一个抽象类,它是所有输入流的超类。 -
这个抽象类定义的一个最重要的方法就是
int read()
签名如下:public abstract int read() throws IOException; //该方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
FileInputStream:
-
从文件流中读取数据:
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(); }//必须要关闭流,释放底层资源 } } ------------------//java7引入的新的try(resource)的语法 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() try中的resource对象需实现java.lang.AutoCloseable接口,InputStream和OutputStream都实现了这个接口 }
缓冲:
-
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."); } } }
阻塞:
int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
InputStream实现类:
-
FileInputStream:可以从文件获取输入流 (上面提到)
-
ByteArrayInputStream:在内存中模拟 一个InputStream
byte[] data = { 72, 101, 108, 108, 111, 33 }; //实际上是把一个byte[]数组在内存中变成一个InputStream,多用于测试时构造InputStream try (InputStream input = new ByteArrayInputStream(data)) { int n; while ((n = input.read()) != -1) { System.out.println((char)n); } }
三、OutputStream
OutputStream介绍:
-
OutputStream
是Java标准库提供的最基本的输出流。位于java.io
这个包; -
OutputStream
也是抽象类,它是所有输出流的超类。 -
这个抽象类定义的一个最重要的方法就是
void write(int b)
,签名如下:public abstract void write(int b) throws IOException; //这个方法会写入一个字节到输出流,传入的是int参数,只写入int最低8位(为一个字节)
-
OutputStream
不仅提供了close()
方法关闭输出流,还提供了一个flush()
方法,将缓冲区的内容真正输出到目的地。(操作系统会等待缓冲区满再输出,flush()
方法能强制输出)
FIleOutputStream:
-
将若干个字节写入文件流:
public void writeFile() throws IOException { //没有考虑发生异常 OutputStream output = new FileOutputStream("out/readme.txt"); output.write(72); // H output.write(101); // e output.write(108); // l output.write(108); // l output.write(111); // o output.close(); }
-
缓冲–
OutputStream
提供的重载方法void write(byte[])
来实现:public void writeFile() throws IOException { try (OutputStream output = new FileOutputStream("out/readme.txt")) { output.write("Hello".getBytes("UTF-8")); // Hello 同样会阻塞 } // 编译器在此自动为我们写入finally并调用close() }
OutputStream实现类:
-
FileOutputStream
: 可以从文件获取输出流 -
ByteArrayOutputStream
:可以在内存中模拟一个OutputStream
byte[] data; //实际上是把一个byte[]数组在内存中变成一个OutputStream,多用于测试时构造OutputStream 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模式
存在的问题:
- Java的IO标准库提供的
InputStream
根据来源可以包括:FileInputStream
:从文件读取数据,是最终数据源;ServletInputStream
:从HTTP请求读取数据,是最终数据源;Socket.getInputStream()
:从TCP连接读取数据,是最终数据源;
- 我们要给
FileInputStream
添加缓冲功能,需要从FileInputStream
派生一个类 - 如果要给
FileInputStream
添加计算签名的功能,需要从FileInputStream
派生一个类 - 如果要给
FileInputStream
添加加密/解密功能,还是需要从FileInputStream
派生一个类 - 对其他数据来源的InputStream添加功能,就会出现非常多的子类
解决方式:
-
将InputStream分为两大类:
- 一类是直接提供数据的基础
InputStream
,例如:- FileInputStream
- ByteArrayInputStream
- ServletInputStream
- 一类是提供额外附加功能的
InputStream
,例如:- BufferedInputStream
- DigestInputStream
- CipherInputStream
- 一类是直接提供数据的基础
-
当我们需要添加功能时,先确定提供数据来源的InputStream,例如数据来自文件
InputStream file = new FileInputStream("test.gz");
-
开始添加功能,我们希望添加缓冲的功能,因此我们用
BufferedInputStream
包装这个InputStream
,得到的包装类型是BufferedInputStream
,但仍用InputStream
指向它InputStream buffered = new BufferedInputStream(file);
-
最后,如果该文件被gzip压缩了,我们希望直接读取压缩的内容,就可以进一步包装一个
GZIPInputStream
:InputStream gzip = new GZIPInputStream(buffered);
但无论包装几次,我们都用InputStream来指向它,这就是FIlter模式,也可以称为装饰器模式
实践:
编写一个CountInputStream
,它的作用是对输入的字节进行计数:
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))) {//只要持有最外层的InputStream,当最外层的InputStream关闭时,内层的InputStream的close()方法也会被自动调用
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:
ZipInputStream
是一种FilterInputStream
,它可以直接读取zip包的内容。JarInputStream
是从ZipInputStream
派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF
文件。
实践:
-
读取zip包
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {//传入一个FileInputStream作为数据源 ZipEntry entry = null;//一个ZipEntry表示一个压缩文件或目录 while ((entry = zip.getNextEntry()) != null) {//返回null,表示zip流结束 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();//closeEntry()结束这个文件的打包。 } } //要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。
六、读取classpath资源
存在问题:
- Java存放
.class
的目录或jar包也可以包含任意其他类型的文件,例如:- 配置文件,例如
.properties
; - 图片文件,例如
.jpg
; - 文本文件,例如
.txt
,.csv
;
- 配置文件,例如
- 不同环境下文件路径不一致,不能从磁盘的固定目录读取配置文件;
解决方法:
-
把
default.properties
配置文件文件放到classpath中,不用关心它的实际存放路径。 -
在classpath中的资源文件,路径总是以
/
开头,我们先获取当前的Class
对象,然后调用getResourceAsStream()
就可以直接从classpath读取任意的资源文件:try (InputStream input = getClass().getResourceAsStream("/default.properties")) { if (input != null) {//资源不存在,返回null,需要判断 // TODO: } }
-
灵活配置:把默认的配置放到jar包中,再从外部文件读取一个可选的配置文件,就可以做到既有默认的配置,又可以让用户自己修改配置
Properties props = new Properties(); props.load(inputStreamFromClassPath("/default.properties")); props.load(inputStreamFromFile("./conf.properties"));
七、序列化
介绍:
- 说明:序列化是指把一个Java对象变成二进制内容,本质上就是一个
byte[]
数组。 - 目的:序列化后可以把
byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
序列化:
-
一个Java对象要能序列化,必须实现一个特殊的
java.io.Serializable
接口,它的定义如下:public interface Serializable { } //没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”.
-
把一个Java对象变为
byte[]
数组,需要使用ObjectOutputStream
。它负责把一个Java对象写入一个字节流:public class Main { public static void main(String[] args) throws IOException { 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)); } System.out.println(Arrays.toString(buffer.toByteArray())); } } //ObjectOutputStream既可以写入基本类型,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
反序列化:
-
ObjectInputStream
负责从一个字节流读取Java对象:try (ObjectInputStream input = new ObjectInputStream(...)) { int n = input.readInt(); String s = input.readUTF(); Double d = (Double) input.readObject(); } //ObjectInputStream除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
-
readObject()
可能抛出的异常有:ClassNotFoundException
:没有找到对应的Class;-- 传输后另一台电脑上没有定义该类,无法反序列化。InvalidClassException
:Class不匹配。-- 定义的字段在反序列化时被修改了类型,导致class不兼容。
-
为避免不兼容,Java的序列化允许class定义一个特殊的
serialVersionUID
静态变量,用于标识序列化的“版本",通常由IDE自动生成,在增加或修改字段时,可以改变serialVersionUID
的值,能阻止不匹配的class版本public class Person implements Serializable { private static final long serialVersionUID = 2709425275741743919L; }
安全性:
- Java的序列化机制可以导致一个实例能直接从
byte[]
数组创建,不经过构造方法,存在一定安全隐患。 - Java的序列化还存在兼容性问题,通过JSON这样的通用数据结构来实现序列化是更好的方法。
八、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)
-
java.io.Reader
是所有字符输入流的超类,它最主要的方法是:public int read() throws IOException; //读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。
FileReader:
-
FileReader
是Reader
的一个子类,它可以打开文件并获取Reader
: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(); // 关闭流 }
-
Reader
也是一种资源,需要保证出错的时候能正确关闭,我们可以用try (resource)
来保证Reader
能够正确地关闭:try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) { // TODO }
-
Reader
还提供了一次性读取若干字符并填充到char[]
数组的方法: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) {//回实际读入的字符个数,最大不超过char[]数组的长度。返回-1表示流结束。 System.out.println("read " + n + " chars."); } } }
Reader实现类:
-
FileReader
: 可以打开文件并获取Reader
-
CharArrayReader
: 可以在内存中模拟一个Reader
try (Reader reader = new CharArrayReader("Hello".toCharArray())) { } //它的作用实际上是把一个char[]数组变成一个Reader
-
StringReader
: 可以直接把String
作为数据源try (Reader reader = new StringReader("Hello")) { }
InputStreamReader:
-
Reader
是基于InputStream
构造的:可以通过InputStreamReader
在指定编码的同时将任何InputStream
转换为Reader
。// 持有InputStream: InputStream input = new FileInputStream("src/readme.txt"); // 变换为Reader: Reader reader = new InputStreamReader(input, "UTF-8");
-
可以指定编码,并通过
try (resource)
更简洁地改写如下:try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) { // TODO: } //使用try (resource)结构时,只需要关闭最外层的Reader对象即可。
九、Writer
和OutputStream区别:
-
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)
。
- 写入一个字符(0~65535):
Writer实现类:
-
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
:可以在内存中创建一个Writer
:try (CharArrayWriter writer = new CharArrayWriter()) { writer.write(65); writer.write(66); writer.write(67); char[] data = writer.toCharArray(); // { 'A', 'B', 'C' } } //它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组
-
StringWriter
也是一个基于内存的Writer
,它和CharArrayWriter
类似。实际上,StringWriter
在内部维护了一个StringBuffer
,并对外提供了Writer
接口。
OutputStreamWriter:
-
Writer
是基于OutputStream
构造的,可以通过OutputStreamWriter
将OutputStream
转换为Writer
,转换时需要指定编码。try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) { // TODO: }
十、PrintStream和PrintWriter
PrintStream:
-
PrintStream
是一种FilterOutputStream
,它在OutputStream
的接口上,额外提供了一些写入各种数据类型的方法:- 写入
int
:print(int)
- 写入
boolean
:print(boolean)
- 写入
String
:print(String)
- 写入
Object
:print(Object)
,实际上相当于print(object.toString())
- 写入
-
我们经常使用的
System.out.println()
实际上就是使用PrintStream
打印各种数据。其中,System.out
是系统默认提供的PrintStream
,表示标准输出:System.out.print(12345); // 输出12345 System.out.print(new Object()); // 输出类似java.lang.Object@3c7a835a System.out.println("Hello"); // 输出Hello并换行
PrintWriter:
-
PrintStream
最终输出的总是byte数据,而PrintWriter
则是扩展了Writer
接口,它的print()
/println()
方法最终输出的是char
数据:public class Main { public static void main(String[] args) { StringWriter buffer = new StringWriter(); try (PrintWriter pw = new PrintWriter(buffer)) { pw.println("Hello"); pw.println(12345); pw.println(true); } System.out.println(buffer.toString()); } }
十一、使用Files
FIles:
-
Files封装了很多读写文件的简单方法,例如把一个文件的全部内容读取为一个
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
提供的读写方法,受内存限制,只能读写小文件,例如配置文件等。
根据廖雪峰的java教程总结:https://www.liaoxuefeng.com/wiki/1252599548343744/1255945227202752