20220424 Java核心技术 卷2 高级特性 2
输入与输出
输入/输出流
在 Java API 中,可以从其中读入一个字节序列的对象称做 输入流 ,而可以向其中写入一个字节序列的对象称做 输出流 。 这些字节序列的来源地和目的地可以是文件,而且通常都是文件,但是也可以是网络连接,甚至是内存块。抽象类 InputStream
和 OutputStream
构成了输入/输出( I/O )类层次结构的基础
这些输入/输出流与在 Java SE 8 的流没有任何关系
因为面向字节的流不便于处理以 Unicode 形式存储的信息(回忆一下, Unicode 每个字符都使用了多个字节来表示),所以从抽象类 Reader
和 Writer
中继承出来了一个专门用于处理 Unicode 字符的单独的类层次结构。这些类拥有的读入和写出操作都是基于两字节的 Char
值的(即, Unicode 码元),而不是基于 byte
值的
读写字节
InputStream
类有一个抽象方法 read
:
public abstract int read() throws IOException;
这个方法将读入一个字节,并返回读入的字节,或者在遇到输入源结尾时返回 -1 。在设计具体的输入流类时 ,必须覆盖这个方法以提供适用的功能,例如,在 FileInputStream
类中,这个方法将从某个文件中读入一个字节,而 System.in
(它是 InputStream
的一个子类的预定义对象)却是 “标准输入” 中读入信息, 控制台或重定向的文件
InputStream
类还有若干个非 象的方法,它们可以读入一个字节数组,或者跳过大量的字节。这些方法都要调用抽象 read
方法,因此,各个子类都只需覆盖这一个方法
与此类似, OutputStream
类定义了下面的抽象方法 write
,它可以向某个输出位置写出一个字节
public abstract void write(int b) throws IOException;
read
和 write
方法在执行时都将阻塞,直至字节确实被读入或写出。这就意味着如果流不能被立即访问(通常是因为网络连接忙),那么当前的线程将被阻塞
available
方法可以去检查当前可读入的字节数 ,这意味着像下面这样的代码片段就不可能被阻塞:
InputStream in = null;
int bytesAvailable = in.available();
if (bytesAvailable > 0) {
byte[] data = new byte[bytesAvailable];
in.read(data);
}
当你完成对输入/输出流的读写时,应该通过调用 close
方法来关闭它,这个调用会释放掉十分有限的操作系统资源。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的 的形式传递的字节在关闭输出流时都将被送出。特别是,如果不关闭文件,那么写出字节的最后一个包可能将永远也得不到传递。当然,我们还可以使用 flush
方法来人为地冲刷这些输出
即使某个输入/输出流类提供了使用原生的 read
和 write
功能的某些具体方法,应用系统的程序员还是很少使用它 ,因为大家感兴趣的数据可能包含数字、字符串和对象,而不是原生字节。我们可以使用众多从基本 InputStream
和 OutputStream
类导出的某个输入/输出类,而不只是直接使用字节
java.io.InputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
read |
public abstract int read() throws IOException; |
从数据中读入一个字节,并返回该字节。这个 read 方法在碰到输入流的结尾时返回 -1 |
read |
public int read(byte b[]) throws IOException |
读入一个字节数组,并返回实际读入的字节数,或者在碰到输入流的结尾时返回 -1 。这个 read 方法最多读入 b.length 个字节 |
read |
public int read(byte b[], int off, int len) |
读入一个字节数组,并返回实际读入的字节数,或者在碰到输入流的结尾时返回 -1 。b :数据读入的数组off :第一个读入字节应该被放置的位置在 b 中的偏移量len :读入字节的最大数量 |
skip |
public long skip(long n) throws IOException |
在输入流中跳过 n 个字节,返回实际跳过的字节数(如果碰到输入流的结尾,则可能小于 n |
available |
public int available() throws IOException |
返回在不阻塞的情况下可获取的字节数(阻塞意味着当前线程将失去它对资源的占用) |
close |
public void close() throws IOException |
关闭这个输入流 |
mark |
public synchronized void mark(int readlimit) |
在输入流的当前位置打一个标记(并非所有的流都支持这个特性)。 如果从输入流中已经读入的字节多于 readlimit 个, 这个流允许忽略这个标记 |
reset |
public synchronized void reset() throws IOException |
返回到最后一个标记,随后对 read 的调用将重新读入这些字节。如果当前没有任何标记, 这个流不被重置 |
markSupported |
public boolean markSupported() |
如果这个流支持打标记, 返回 true |
java.io.OutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
write |
public abstract void write(int b) throws IOException; |
写出一个字节的数据 |
write |
public void write(byte b[]) throws IOException public void write(byte b[], int off, int len) throws IOException |
写出所有字节或者某个范围的字节到数组 b 中b :数据写出的数组off :第一个写出字节在 b 中的偏移量len :写出字节的最大数量 |
close |
public void close() throws IOException |
冲刷并关闭输出流 |
flush |
public void flush() throws IOException |
冲刷输出流, 就是将所有缓冲的数据发送到目的地 |
完整的流家族
Java 拥有一个流家族,包含各种输入 / 输出流类型
InputStream
和 OutputStream
可以读写单个字节或字节数组:
DataInputStream
和 DataOutputStream
可以以二进制格式读写所有的基本 Java 类型。最后,还包含了多个很有用的输入/输出流,例如,ZipInputStream
和 ZipOutputStream
可以以常见的 ZIP 压缩格式读写文件
对于 Unicode 文本 可以使用抽象类 Reader
和 Writer
的子类。 Reader
和 Writer
类的基本方法与 InputStream
和 OutputStream
中的方法类似
public int read() throws IOException
public void write(int c) throws IOException
read
方法将返回一个 Unicode 码元( 1 - 65535 之间的整数),或者在碰到文件结尾时返回 -1
。write
方法在被调用时需要传递一个 Unicode 码元
还有 4 个附加的接口: Closeable
、Flushable
、Readable
、Appendable
InputStream
、OutputStream
、Reader
、Writer
都实现了Closeable
接口OutputStream
和Writer
实现了Flushable
接口Writer
实现了Appendable
Reader
实现了Readable
java.io.Closeable
接口扩展了 java.lang.AutoCloseale
接口。因此,对任何 Closeable
进行操作时,都可以使用 try-with-resource
语句。为什么要有两个接口呢?因为 Closeable
接口的 close
方法只抛出 IOException
,而 AutoCloseable.close
方法可以抛出任何异常
Readable
接口只有一个方法:
public int read(java.nio.CharBuffer cb) throws IOException;
CharBuffer
类拥有按顺序和随机地进行读写访问的方法,它表示一个内存中的缓冲区或者一个内存映像的文件
CharSequence
接口描述了一个 char
值序列的基本属性, String
、CharBuffer
、StringBuilder
、StringBuffer
都实现了它
java.io.Closeable 方法名称 |
方法声明 | 描述 |
---|---|---|
close |
public void close() throws IOException; |
关闭这个 Closeable ,这个方法可能会抛出 IOException |
java.io.Flushable 方法名称 |
方法声明 | 描述 |
---|---|---|
flush |
void flush() throws IOException; |
冲刷这个 Flushable |
java.lang.Readable 方法名称 |
方法声明 | 描述 |
---|---|---|
read |
public int read(java.nio.CharBuffer cb) throws IOException; |
尝试着向 cb 读入其可持有数量的 char 值。返回读入的 char 值的数量,或者当从这个 Readable 无法再获得更多的值时返回 -1 |
java.lang.Appendable 方法名称 |
方法声明 | 描述 |
---|---|---|
append |
Appendable append(char c) throws IOException; Appendable append(CharSequence csq) throws IOException; Appendable append(CharSequence csq, int start, int end) throws IOException; |
向这个 Appendable 中追加给定的码元或者给定的序列中的所有码元,返回 this |
java.lang.CharSequence 方法名称 |
方法声明 | 描述 |
---|---|---|
charAt |
char charAt(int index); |
返回给定索引处的码元 |
length |
int length(); |
返回在这个序列中的码元的数量 |
subSequence |
CharSequence subSequence(int start, int end); |
返回由存储在 startIndex 到 endIndex-1 处的所有码元构成的 CharSequence |
toString |
public String toString(); |
返回这个序列中所有码元构成的字符串 |
组合输入/输出流过滤器
FileInputStream
和 FileOutputStream
可以提供附着在一个磁盘文件上的输入流和输出流,而你只需向其构造器提供文件名或文件的完整路径名
FileInputStream fin = new FileInputStream("employee.dat");
所有在 java.io
中的类都将相对路径名解释为以用户工作目录开始,你可以通过调用 System.getProperty("user.dir")
来获得这个信息
由于反斜杠字符在 Java 字符串中是转义字符,因此要确保在 Windows 风格的路径名中使用 \\
(例如, C:\\Windows\\win.ini
) 。在 Windows 中,还可以使用单斜杠字符 ( C:/Windows/win.ini
),因为大部分 Windows 文件处理的系统调用都会将斜杠解择成文件分隔符。但是,并不推荐这样做,因为 Windows 系统函数的行为会因与时俱进而发生变化。因此,对于可移植的程序来说,应该使用程序所运行平台的文件分隔符,我们可以通过常量字符串 java.io.File.separator
获得它
与抽象类 InputStream
和 OutputStream
一样,FileInputStream
和 FileOutputStream
类只支持在字节级别上的读写:
byte b = (byte) fin.read();
如果我们只有 DataInputStream
,那么我们就只能读入数值类型:
DataInputStream din = ...;
double x = din.readDouble();
正如 FileInputStream
没有任何读入数值类型的方法一样, DataInputStream
也没有任何从文件中获取数据的方法
Java 使用了一种灵巧的机制来分离这两种职责。某些输入流(例如 FileInputStream
和由 URL 类的 openStream
方法返回的输入流)可以从文件和其他更外部的位置上获取字节,而其他的输入流(例如 DatalnputStream
)可以将字节组装到更有用的数据类型中。Java 程序员必须对二者进行组合。例如,为了从文件中读入数字,首先需要创建一个 FileInputStream
,然后将其传递给 DataInputStream
的构造器:
FileInputStream fin = new FileInputStream("employee.dat");
DataInputStream din = new DataInputStream(fin);
double x = din.readDouble();
FilterInputStream
和 FilterOutputStream
的子类用于向处理字节的输入/输出流添加额外的功能
你可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对 read
的调用都会请求操作系统再分发一个字节。相比之下,请求一个数据块并将其置于缓冲区中会显得更加高效。如果我们想使用缓冲机制,以及用于文件的数据输入方法,那么就需要使用下面这种相当恐怖的构造器序列:
DataInputStream din = new DataInputStream(
new BufferedInputStream(
new FileInputStream("employee. dat")));
注意,我们把 DataInputStream
置于构造器链的最后,这是因为我们希望使用 DataInputStream
的方法,并且希望它们能够使用带缓冲机制的 read
方法
有时当多个输入流链接在一起时,你需要跟踪各个 中介输入流( intermediate input stream ) 。例如,当读入输入时,你经常需要预览下一个字节,以了解它是否是你想要的值。Java 提供了用于此目的的 PushbackInputStream
:
PushbackInputStream pbin = new PushbackInputStream(
new BufferedInputStream(
new FileInputStream("target/classes/cities.txt")));
// 预读下一个字节
int b = pbin.read();
if (b != '<') {
// 将并非你所期望的值推回流中
pbin.unread(b);
}
在其他编程语言的输入 / 输出流类库中,诸如缓冲机制和预览等细节都是自动处理的。因此,相比较而言, Java 就有点麻烦,它必须将多个流过滤器组合起来。但是,这种混合并匹配过滤器类以构建真正有用的输入 / 输出流序列的能力,将带来极大的 灵活性 。
java.io.FileInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public FileInputStream(String name) throws FileNotFoundException public FileInputStream(File file) throws FileNotFoundException |
使用由 name 字符串或 file 对象指定路径名的文件创建一个新的文件输入流。 非绝对的路径名将按照相对于 VM 启动时所设置的工作目录来解析 |
java.io.FileOutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public FileOutputStream(String name) throws FileNotFoundException public FileOutputStream(String name, boolean append) throws FileNotFoundException public FileOutputStream(File file) throws FileNotFoundException public FileOutputStream(File file, boolean append) throws FileNotFoundException |
使用由 name 字符串或 file 对象指定路径名的文件 建一个新的文件输出流。如果 append 参数为 true ,那么数据将被添加到文件尾,而具有相同名字的已有文件不会被删除;否 ,这个方法会 除所有具有相同名字的已 有文件 |
java.io.BufferedInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public BufferedInputStream(InputStream in) |
创建一个带缓冲区的输入流。带缓冲区的输入流在从流中读入字符时,不会每次都对设备访问。当缓冲区为空时,会向缓冲区中读入一个新的数据块 |
java.io.BufferedOutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public BufferedOutputStream(OutputStream out) |
创建一个带缓冲区的输出流。带缓冲区的输出流在收集要写出的字符时,不会每次都对设备访问。当缓冲区填满或当流被冲刷时,数据就被写出 |
java.io.PushbackInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public PushbackInputStream(OutputStream out) public PushbackInputStream(InputStream in, int size) |
构建一个可以预览一个字节或者具有指定尺寸的回推缓冲区的输人流 |
unread |
public void unread(int b) throws IOException |
回推一个字节,它可以在下次调用 read 时被再次获取b :要再次读入的字节 |
文本输入与输出
在保存数据时,可以选择 二进制格式 或 文本格式
在存储文本字符串时,需要考虑 字符编码( character encoding )方式。 Java 内部使用 UTF-16 编码方式
OutputStreamWriter
类将使用选定的字符编码方式,把 Unicode 码元的输出流转换为字节流。InputStreamReader
类将包含字节(用某种字符编码方式表示的字符)的输入流转换为可以产生 Unicode 码元的读入器
OutputStreamWriter
和 InputStreamReader
默认使用主机系统所使用的默认字符编码方式,应该通过构造器指定编码方式
Reader reader = new InputStreamReader(System.in, StandardCharsets.UTF_8);
如何写出文本输出( PrintWriter
)
对于文本输出,可以使用 PrintWriter
。这个类拥有以文本格式打印字符串和数字的方法,它还有一个将 PrintWriter
链接到 FileWriter
的便捷方法
PrintWriter out = new PrintWriter("employee.txt", "UTF-8");
String name = "Harry Hacker";
double salary = 75000;
out.print(name);
out.print(' ');
out.println(salary);
out.close();
println
方法在行中添加了对目标系统来说恰当的行结束符( Windows 系统是 \r\n
,UNIX 系统是 \n
),也就是通过 System.getProperty("line.separator")
而获得的字符串。
如果写出器设置为自动冲刷模式,那么只要 println
被调用,缓冲区中的所有字符都会被发送到它们的目的地(打印写出器总是带缓冲区的) 。默认情况下,自动冲刷机制是禁用的,你可以通过使用 PrintWriter(Writer out, Boolean autoFlush)
来启用或禁用自动冲刷机制:
PrintWriter out =
new PrintWriter(new OutputStreamWriter(new FileOutputStream("employee.txt"), StandardCharsets.UTF_8),
true);
print
方法不抛出异常,你可以调用 checkError
方法来查看输出流是否出现了某些错误
System.out
是 PrintStream
对象
为了与已有的代码兼容, System.in
、System.out
、System.err
仍旧是输入/输出流而不是读入器和写出器。但是现在 PrintStream
类在内部采用与 PrintWriter
相同的方式将 Unicode 字符转换成了默认的主机编码方式,当你在使用 print
、println
方法时,PrintStream
类型的对象的行为看起来确实很像打印写出器,但是与打印写出器不同的是,它们允许我们用 write(int)
和 write(byte[])
方法输出原生字节
java.io.PrintWriter 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public PrintWriter (Writer out) public PrintWriter(OutputStream out) |
创建一个向给定的写出器写出的新的 PrintWriter |
构造器 | public PrintWriter(String fileName, String csn) throws FileNotFoundException, UnsupportedEncodingException public PrintWriter(File file, String csn) throws FileNotFoundException, UnsupportedEncodingException |
创建一个使用给定的编码方式向给定的文件写出的新的 PrintWriter |
print |
public void print(Object obj) |
通过打印从 toString 产生的字符串来打印一个对象 |
print |
public void print(String s) |
打印一个包含 Unicode 码元的字符串 |
println |
public void println(String x) |
打印一个字符串,后面紧跟一个行终止符。如果这个流处于自动冲刷模式,那么就会冲刷这个流 |
print |
public void print(char s[]) |
打印在给定的字符串中的所有 Unicode 码元 |
print |
public void print(char c) |
打印一个 Unicode 码元 |
print |
public void print(int i) public void print(long l) public void print(float f) public void print(double d) public void print(boolean b) |
以文本格式打印给定的值 |
printf |
public PrintWriter printf(String format, Object ... args) |
按照格式字符串指定的方式打印给定的值 |
checkError |
public boolean checkError() |
如果产生格式化或输出错误,则返回 true 。一旦这个流碰到了错误,它就受到了污染,并且所有对 checkError 的调用都将返回 true |
如何读入文本输入( PrintWriter
)
最简单的处理任意文本的方式就是使用 Scanner
类。我们可以从任何输入流中构建 Scanner
对象
可以将短小的文本文件像下面这样读入到一个字符串中:
Path path = Paths.get("employee.txt");
Charset charset = StandardCharsets.UTF_8;
System.out.println(path.toAbsolutePath());
String content = new String(Files.readAllBytes(path), charset);
如果想要将这个文件一行行地读入,那么可以调用:
List<String> lines = Files.readAllLines(path, charset);
如果文件太大,那么可以将行惰性处理为 Stream<String>
对象:
try (Stream<String> lineStream = Files.lines(path, charset)) {
// do something
}
在早期的 Java 版本中,处理文本输入的唯一方式就是通过 BufferedReader
它的 readLine
方法会产生一行文本,或者在无法获得更多的输入时返回 null
。 典型的输入循环看起来像下面这样:
InputStream inputStream = new FileInputStream("employee.txt");
try (BufferedReader in = new BufferedReader(new InputStreamReader(inputStream,
StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
// do something
}
}
如今, BufferedReader
类又有了一个 lines
方法,可以产生一个 Stream<String>
对象。但是,与 Scanner
不同, BufferedReader
没有用于任何读入数字的方法
以文本格式存储对象
@Getter
@ToString
public class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
public class TextFileTest {
public static void main(String[] args) throws IOException {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
// save all employee records to the file employee.dat
try (PrintWriter out = new PrintWriter("employee.dat", "UTF-8")) {
writeData(staff, out);
}
// retrieve all records into a new array
try (Scanner in = new Scanner(
new FileInputStream("employee.dat"), "UTF-8")) {
Employee[] newStaff = readData(in);
// print the newly read employee records
for (Employee e : newStaff) {
System.out.println(e);
}
}
}
private static void writeData(Employee[] employees, PrintWriter out) throws IOException {
// write number of employees
out.println(employees.length);
for (Employee e : employees) {
writeEmployee(out, e);
}
}
private static Employee[] readData(Scanner in) {
// retrieve the array size
int n = in.nextInt();
in.nextLine(); // consume newline
Employee[] employees = new Employee[n];
for (int i = 0; i < n; i++) {
employees[i] = readEmployee(in);
}
return employees;
}
public static void writeEmployee(PrintWriter out, Employee e) {
out.println(e.getName() + "|" + e.getSalary() + "|" + e.getHireDay());
}
public static Employee readEmployee(Scanner in) {
String line = in.nextLine();
String[] tokens = line.split("\\|");
String name = tokens[0];
double salary = Double.parseDouble(tokens[1]);
LocalDate hireDate = LocalDate.parse(tokens[2]);
int year = hireDate.getYear();
int month = hireDate.getMonthValue();
int day = hireDate.getDayOfMonth();
return new Employee(name, salary, year, month, day);
}
}
字符编码方式
输入和输出流都是用于字节序列的,但是在许多情况下,我们希望操作的是文本,即字符序列。于是,字符如何编码成字节 就成了问题
Java 针对字符使用的是 Unicode 标准。每个字符或“编码点”都具有一个 21 位的整数。有多种不同的字符编码方式,也就是说,将这些 21 位数字包装成字节的方法有多种。
最常见的编码方式是 UTF-8
,它会将每个 Unicode 编码点编码为 1 到 4 个字节的序列。UTF-8 的好处是传统的包含了英语中用到的所有字符的 ASCII 字符集中的每个字符都只会占用一个字节
另一种常见的编码方式是 UTF-16
。它会将每个 Unicode 编码点编码为 1 个或 2 个 16 位值。这是一种在 Java 字符串中使用的编码方式。实际上,有两种形式的 UTF-16 ,被称为“高位优先”和“低位优先”。考虑一下 16 位值 0x2122
,在高位优先格式中,高位字节会先出现: 0x21
后面跟着 0x22
。但是在低位优先格式中,是另外一种排列方式: 0x22
0x21
。为了表示使用的是哪一种格式,文件可以以“字节顺序标记”开头,这个标记为 16 位数值 0xFEFF
。读入器可以使用这个值来确定字节顺序,然后丢弃它。
警告:有些程序,包括 Microsoft Notepad (微软记事本)在内,都在 UTF-8 编码的文件开头处添加了一个字节顺序标记。很明显,这并不需要,因为在 UTF-8 中,并不存在字节顺序的问题。但是 Unicode 标准允许这样做,甚至认为这是一种好的做法,因为这样做可以使编码机制不留疑惑。遗憾的是, Java 并没有这么傲,有关这个问题的缺陷报告最终是以 “will not fix (不做修正)” 关闭的。对你来说,最好的做法是将输入中发现的所有先导的 \uFEFF
都剥离掉
除了 UTF 编码方式,还有一些编码方式,它们各自都覆盖了适用于特定用户人群的字符范围。例如, ISO 8859-1 是一种单字节编码,它包含了西欧各种语言中用到的带有重音符号的字符,而 Shift-JIS 是一种用于日文字符的可变长编码。的这些编码方式至今仍在被广泛使用
不存在任何可靠的方式可以自动地探测出字节流中所使用的字符编码方式。
某些 API 方法让我们使用“默认字符集”,即计算机的操作系统首选的字符编码方式。这种字符编码方式与我们的字节源中所使用的编码方式相同吗?字节源中的字节可能来自世界上的其他国家或地区,因此,你应该总是明确指定编码方式。例如,在编写网页时,应该检查 Content-Type
头信息
平台使用的编码方式可以由静态方法 Charset.defaultCharset
返回。静态方法 Charset.availableCharsets
会返回所有可用的 Charset
实例,返回结果是一个从字符集的规范名称到 Charset
对象的映射表
Charset charset = Charset.defaultCharset();
SortedMap<String, Charset> availableCharsets = Charset.availableCharsets();
System.out.println(charset);
System.out.println(availableCharsets);
警告: Oracle 的 Java 实现有一个用于覆盖平台默认值的系统属性 file.encoding
。但是它并非官方支持的属性,并且 Java 库的 Oracle 实现的所有部分并非都以一致的方式处理该属性,因此,你不应该设置它
StandardCharsets
类具有类型为 Charset
的静态变量 ,用于表示每种 Java 虚拟机都必须支持的字符编码方式
为了获得另一种编码方式的 Charset
,可以使用静态的 forName
方法,尽量使用 StandardCharsets
类中的常量,防止拼写错误
Charset charset = Charset.forName("UTF-8");
在读入或写出文本时,应该使用 Charset
对象 例如,我们可以像下面这样将数组转换为字符串
String string = new String(bytes, StandardCharsets.UTF_8);
警告:在不指定任何编码方式时,有些方法(例如 String(byte[])
构造器)会使用默认的平台编码方式,而其他方法(例如 Files.readAllLines
)会使用 UTF-8
读写二进制数据
文本格式对于测试和调试而言会显得很方便,因为它是人类可阅读的,但是它并不像以二进制格式传递数据那样高效
DataInput
和 DataOutput
接口
DataOutput
接口定义了用于以二进制格式写数组、字符、 boolean 值和字符串的方法
writeInt
总是将一个整数写出为 4 字节的二进制数量值,而不管它有多少位,writeDouble
是将一个 double
值写出为 8 字节的二进制数量值。这样产生的结果并非人可阅读的,但是对于给定类型的每个值,所需的空间都是相同的,而且将其读回也比解析文本要更快
在 Java 中,所有的值都按照高位在前的模式写出,不管使用何种处理器,这使得 Java 数据文件可以独立于平台
writeUTF
方法使用修订版的 Unicode 转换格式写出字符串。这种方式与直接使用标准的 UTF-8 编码方式不同 ,其中, Unicode 码元序列首先用 UTF-16 表示,其结果之后使用 UTF-8 进行编码。修订后的编码方式对于编码大于 OxFFFF
的字符的处理有所不同,这是为了向后兼容在 Unicode 还没有超过 16 位时构建的虚拟机
因为没有其他方法会使用 UTF-8 的这种修订,所以你应该只在写出用于 Java 虚拟机的字符串时才使用 writeUTF
方法,例如,当你需要编写一个生成字节码的程序时。对于其他场合,都应该使用 writeChars
方法
为了读回数据,可以使用在 DataInput
接口中定义的方法
DataInputStream
类实现了 DataInput
接口,为了从文件中读入二进制数据,可以将 DataInputStream
与某个字节源相组合,例如 FileInputStream
。与此类似,要想写出二进制数据,你可以使用实现了 DataOutput
接口的 DataOutputStream
DataOutputStream out = new DataOutputStream(new FileOutputStream("data1.dat"));
out.writeInt(1);
out.writeChars("data");
out.close();
DataInputStream in = new DataInputStream(new FileInputStream("data1.dat"));
System.out.println(in.readInt());
final char c1 = in.readChar();
final char c2 = in.readChar();
final char c3 = in.readChar();
final char c4 = in.readChar();
in.close();
System.out.println("" + c1 + c2 + c3 + c4);
java.io.DataInput 方法名称 |
方法声明 | 描述 |
---|---|---|
readBoolean readByte readChar readDouble readFloat readInt readLong readShort |
boolean readBoolean() throws IOException; byte readByte() throws IOException; char readChar() throws IOException; double readDouble() throws IOException; float readFloat() throws IOException; int readInt() throws IOException; long readLong() throws IOException; short readShort() throws IOException; |
读入一个给定类型的值 |
readFully |
void readFully(byte b[]) throws IOException; |
将字节读入到数组 b 中,其间阻塞直至所有字节都读入 |
readFully |
void readFully(byte b[], int off, int len) throws IOException; |
将字节读入到数组 b 中,其间阻塞直至所有字节都读入b :数据读入的缓冲区off :数据起始位置的偏移量len :读入字节的最大数量 |
readUTF |
String readUTF() throws IOException; |
读入由修订过的 UTF-8 格式的字符构成的字符串 |
skipBytes |
int skipBytes(int n) throws IOException; |
跳过 n 个字节,其间阻塞直至所有字节都被跳过 |
java.io.DataOutput 方法名称 |
方法声明 | 描述 |
---|---|---|
writeBoolean writeByte rwriteChar writeDouble writeFloat writeInt writeLong writeShort |
void writeBoolean(boolean v) throws IOException; void writeByte(int v) throws IOException; void writeChar(int v) throws IOException; void writeDouble(double v) throws IOException; void writeFloat(float v) throws IOException; void writeInt(int v) throws IOException; void writeLong(long v) throws IOException; void writeShort(int v) throws IOException; |
写出一个给定类型的值 |
writeChars |
void writeChars(String s) throws IOException; |
写出字符串中的所有字符 |
writeUTF |
void writeUTF(String s) throws IOException; |
写出由 “修订过的 UTF” 格式的字符构成的字符串 |
随机访问文件( RandomAccessFile
)
RandomAccessFile
类可以在文件中的任何位置查找或写入数据。磁盘文件都是随机访问的,但是与网络套接字通信的输入/输出流却不是。你可以打开一个随机访问文件,只用于读入或者同时用于读写,你可以通过使用字符串 r
(用于读入访问 )或 rw
(用于读入/
写出访问)作为构造器的第二个参数来指定这个选项
RandomAccessFile in = new RandomAccessFile("employee.txt", "r");
RandomAccessFile inOut = new RandomAccessFile("employee.txt", "rw");
当你将已有文件作为 RandomAccessFile
打开时, 这个文件并不会被删除
随机访问文件有一个表示下一个将被读入或写出的字节所处位置的文件指针, seek
方法可以用来将这个文件指针设置到文件中的任意字节位置, seek
的参数是一个 long
类型的整数,它的值位于 0 到文件按照字节来度量的长度之间
getFilePointer
方法将返回文件指针的当前位置
RandomAccessFile
类同时实现了 Datalnput
和 DataOutput
接口。为了读写随机访问文件,可以使用在 readInt
/ writeInt
和 readChar
/writeChar
之类的方法
java.io.RandomAccessFile 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public RandomAccessFile(File file, String mode) throws FileNotFoundException public RandomAccessFile(String name, String mode) throws FileNotFoundException |
file :要打开的文件mode :r 表示只读模式; rw 表示读/写模式;rws 表示每次更新时,都对数据和元数据的写磁盘操作进行同步的读/写模式;rwd 表示每次更新时,只对数据的写磁盘操作进行同步的读/写模式 |
getFilePointer |
public native long getFilePointer() throws IOException; |
返回文件指针的当前位置 |
seek |
public void seek(long pos) throws IOException |
将文件指针设置到距文件开头 pos 个字节处 |
length |
public native long length() throws IOException; |
返回文件按照字节来度量的长度 |
ZIP 文档
ZIP 文档(通常)以压缩格式存储了一个或多个文件,每个 ZIP 文档都有一个头,包含诸如每个文件名字和所使用的压缩方法等信息。在 Java 中,可以使用 ZipInputStream
来读入 ZIP 文档。你可能需要浏览文档中每个单独的项, getNextEntry
方法就可以返回一个描述这些项的 ZipEntry
类型的对象。
要写出到 ZIP 文件,可以使用 ZipOutputStream
,而对于你希望放入到 ZIP 文件中的每一项,都应该 建一个 ZipEntry
对象,并将文件名传递给 ZipEntry
构造器,它将设置其他诸如文件日期和解压缩方法等参数。
JAR 文件只是带有一个特殊项的 ZIP 文件,这个项称作清单。你可以使用 JarInputStream
和 JarOutputStream
类来读写清单项
ZIP 输入流是一个能够展示流的抽象化的强大之处的实例。当你读入以压缩格式存储的数据时,不必担心边请求边解压数据的问题,而且 ZIP 格式的字节源并非必须是文件,也可以是来自网络连接的 ZIP 数据 事实上,当 Applet 类加载器读入 JAR 文件时,它就是在读入和解压来自网络的数据
/**
* 读 ZIP
* @throws IOException
*/
@Test
public void test2() throws IOException {
String zipName = "test.zip";
ZipFile zipFile = new ZipFile(zipName);
ZipInputStream zin = new ZipInputStream(new FileInputStream(zipName));
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
final String name = entry.getName();
System.out.println(name+ " begin ========================");
InputStream in = zipFile.getInputStream(entry);
// read the contents of in
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
zin.closeEntry();
System.out.println(name+ " end ========================");
}
}
/**
* 写 ZIP
* @throws IOException
*/
@Test
public void test3() throws IOException {
Stream<Path> list =
Files.list(Paths.get("D:\\Develop\\workspace\\study\\study-corejava\\src\\main\\java\\others"));
FileOutputStream fout = new FileOutputStream("test.zip");
ZipOutputStream zout = new ZipOutputStream(fout);
// for all files
list.forEach((path) -> {
ZipEntry ze = new ZipEntry(path.getFileName().toString());
try {
zout.putNextEntry(ze);
// send data to zout
final FileInputStream fis = new FileInputStream(path.toString());
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
zout.write(bytes, 0, len);
}
zout.flush();
zout.closeEntry();
} catch (IOException e) {
e.printStackTrace();
}
});
zout.close();
}
java.util.zip.ZipInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ZipInputStream(InputStream in) |
创建一个 ZipInputStream 使得可以从给定的 InputStream 向其中填充数据 |
getNextEntry |
public ZipEntry getNextEntry() throws IOException |
为下一项返回 ZipEntry 对象,或者在没有更多的项时返回 null |
closeEntry |
public void closeEntry() throws IOException |
关闭这个 ZIP 文件中当前打开的项。之后可以通过使用 getNextEntry 读入下一项 |
java.util.zip.ZipOutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ZipOutputStream(OutputStream out) |
创建一个将压缩数据写出到指定的 OutputStream 的ZipOutputStream |
putNextEntry |
public void putNextEntry(ZipEntry e) throws IOException |
将给定的 ZipEntry 中的信息写出到输出流中,并定位用于写出数据的流,然后这些数据可以通过 write 写出 这个输 流中 |
closeEntry |
public void closeEntry() throws IOException |
关闭这个 ZIP 文件中当前打开项。使用 putNextEntry 方法可以开始下一项 |
setLevel |
public void setLevel(int level) |
设置后续的各个 DEFLATED 项的默认压缩级别 这里默认值是 Deflater.DEFAULT_COMPRESSION 。如果级别无效, 抛出 IllegalArgumentException level :压缩级别, 0(NO_COMPRESSION )到 9(BEST_COMPRESSION ) |
setMethod |
public void setMethod(int method) |
设置用于这个 ZipOutputStream 的默认压缩方法 这个压缩方法会作用于所有没有指定压缩方法的项上method :压缩方法, DEFLATED 或 STORED |
java.util.zip.ZipEntry 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ZipEntry(String name) |
用给定的名字构建一个 Zip 项 |
getCrc |
public long getCrc() |
返回用于这个 ZipEntry 的 CRC32 校验和的值 |
getName |
public String getName() |
返回这一项的名字 |
getSize |
public long getSize() |
返回这一项未压缩的尺寸,或者在未压缩的尺寸不可知的情况下返回 -1 |
isDirectory |
public boolean isDirectory() |
当这一项是目录时返回 true |
setMethod |
public void setMethod(int method) |
method :压缩方法, DEFLATED 或 STORED |
setSize |
public void setSize(long size) |
设置这一项的尺寸,只有在压缩方法是 STORED 时才是必需的size :这一项未压缩的尺寸 |
setCrc |
public void setCrc(long crc) |
给这一项设置 CRC32 校验和, 这个校验和是使用 CRC32 类计算的。只有在压缩方法是 STORED 时才是必需的 |
java.util.zip.ZipFile 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ZipFile(File file) throws ZipException, IOException public ZipFile(String name) throws IOException |
创建一个 ZipFile ,用于从给定的字符串或 File 对象中读入数据 |
entries |
public Enumeration<? extends ZipEntry> entries() |
返回 Enumeration 对象,它枚举了描述这个 ZipFile 中各个项的 ZipEntry 对象 |
getEntry |
public ZipEntry getEntry(String name) |
返回给定名字所对应的项或者在没有对应项的时候返回 null name :项名 |
getInputStream |
public InputStream getInputStream(ZipEntry entry) throws IOException |
返回用于给定项的 InputStream |
getName |
public String getName() |
返回这个 ZIP 文件的路径 |
对象输入/输出流与序列化
Java 语言支持一种称为 对象序列化(object serialization )的非常通用的机制 ,它可以将任何对象写出到输出流中,并在之后将其读回
保存和加载序列化对象
为了保存对象,可以直接使用 ObjectOutputStream
的 writeObject
方法
为了将这些对象读回,可以使用 ObjectInputStream
对象的 readObject
方法以这些对象被写出时的顺序获得它们
对希望在对象输出流中存储或从对象输入流中恢复的所有类必须实现 Serializable
接口
你只有在写出对象时才能用 writeObject
/ readObject
方法,对于基本类型值,你需要使用诸如 writeInt
/ readInt
或writeDouble
/ readDouble
这样的方法 (对象流类都实现了 DataInput
/ DataOutput
接口
在幕后,是 ObjectOutputStream
在浏览对象的所有域,并存储它们的内容
但是,有一种重要的情况需要考虑:当一个对象被多个对共享,作为它们各自状态的一部分时,会发生什么呢?
每个对象都是用一个 序列号 ( serial number )保存的,这就是这种机制之所以称为对象序列化的原因。下面是其算法:
- 每一个对象引用都关联一个序列号
- 对于每个对象,当第一次遇到时,保存其对象数据到输出流中
- 如果某个对象之前已经被保存过,那么只写出“与之前保存过的序列号为 x 的对象相同”
在读回对象时,整个过程是反过来的:
- 对于对象输入流中的对象,在第一次遇到其序列号时,构建它,并使用流中数据来初始化它,然后记录这个顺序号和新对象之间的关联
- 当遇到“与之前保存过的序列号为 x 的对象相同”标记时,获取与这个顺序号相关联的对象引用
序列化的另一种非常重要的应用是通过网络将对象集合传送到另一台计算机上
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);
carl.setSecretary(harry);
Manager tony = new Manager("Tony Tester", 40000, 1990, 3, 15);
tony.setSecretary(harry);
Employee[] staff = new Employee[3];
staff[0] = carl;
staff[1] = harry;
staff[2] = tony;
// save all employee records to the file employee.dat
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.dat"))) {
out.writeObject(staff);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("employee.dat"))) {
// retrieve all records into a new array
Employee[] newStaff = (Employee[]) in.readObject();
// raise secretary's salary
newStaff[1].raiseSalary(10);
// print the newly read employee records
for (Employee e : newStaff) {
System.out.println(e);
}
}
java.io.ObjectOutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ObjectOutputStream(OutputStream out) throws IOException |
创建一个 ObjectOutputStream 使得你可以将对象写出到指定的 OutputStream |
writeObject |
public final void writeObject(Object obj) throws IOException |
写出指定的对象到 ObjectOutputStream ,这个方法将存储指定对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时的域的值 |
java.io.ObjectInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public ObjectInputStream(InputStream in) throws IOException |
创建一个 ObjectInputStream 用于从指定的 InputStream 中读回对象信息 |
readObject |
public final Object readObject() throws IOException, ClassNotFoundException |
从 ObjectInputStream 中读入一个对象。特别是,这个方法会读回对象的类、类的签名以及这个类及其超类中所有非静态和非瞬时的域的值。它执行的反序列化允许恢复多个对象引用 |
理解对象序列化的文件格式
对象序列化是以特殊的文件格式存储对象数据的
- 对象流输出中包含所有对象的类型和数据域
- 每个对象都被赋予一个序列号
- 相同对象的重复出现将被存储为对这个对象的序列号的引用
修改默认的序列化机制
某些数据域是不可以序列化的,例如,只对本地方法有意义的存储文件句柄或窗口句柄的整数值。Java 拥有一种很简单的机制来防止这
种域被序列化,那就是将它们标记成是 transient
。如果这些域属于不可序列化的类,你也需要将它们标记成 transient
。瞬时的域在对象被序列化时总是被跳过的
序列化机制为单个的类提供了一种方式,去向默认的读写行为添加验证或任何其他想要的行为。可序列化的类可以定义具有下列签名的方法:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {}
private void writeObject(ObjectOutputStream in) throws IOException {}
之后,数据域就再也不会被自动序列化,取而代之的是调用这些方法。
如果父类同样定义了这两个方法,先调用父类的,再调用子类的
java.util.Date
类,它提供了自己的 readObject
和 writeObject
方法,这些方法将日期写出为从纪元( UTC 时间 1970年1月1日0点 )开始的毫秒数
readObject
和 writeObject
方法只需要保存和加载它们的数据域,而不需要关心超类数据和任何其他类的信息
除了让序列化机制来保存和恢复对象数据,类还可以定义它自己的机制。这个类必须实现 Externalizable
接口,需要实现它定义两个方法
与 readObject
和 writeObject
不同,这些方法对包括超类数据在内的整个对象的存储和恢复负全责。在写出对象时, 序列化机制在输出流中仅仅只是记录该对象所属的类。在读入可外部化的类时,对象输入流将用无参构造器创建一个对象,然后调用 readExternal
方法
警告: readObject
和 writeObject
是私有的, 且只能被序列化机制调用。与此不同的是, readExternal
和 writeExternal
方法是公共的。特别是, readExternal
还潜在地允许修改现有对象的状态
序列化单例和类型安全的枚举
在序列化和反序列化时 ,如果目标对象是唯一的,那么必须加倍小心,这通常会在实现单例和类型安全的枚举时发生
如果你使用 Java 的 enum
结构,那么你就不必担心序列化,它能够正常工作。但是,假设你在维护遗留代码,其中包含下面这样的枚举类型
public class Orientation {
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
public Orientation(int value) {
this.value = value;
}
}
这种风格在枚举被添加到 Java 语言中之前是很普遍的。注意,其构造器是私有的,因此,不可能创建出超出 Orientation.HORIZONTAL
和 Orientation.VERTICAL
之外的对象。特别是,你可以使用 ==
操作符来测试对象的等同性:
if (orientation == Orientation.HORIZONTAL)
当类型安全的枚举实现 Serializable
接口时,你必须牢记存在着一种重要的变化,此时,默认的序列化机制是不适用的
即使构造器是私有的,序列化机制也可以创建新的对象!
为了解决这个问题,你需要定义另外一种称为 readResolve
的特殊序列化方法。如果定义了 readResolve
方法,在对象被序列化之后就会调用它。它必须返回一个对象,而该对象之后会成为 readObject
的返回值。在上面的情况中, readResolve
方法将检查 value
域并返回恰当的枚举常量:
protected Object readResolve() throws ObjectStreamException {
System.out.println("Orientation.readResolve...");
if (value == 1) {
return HORIZONTAL;
}
if (value == 2) {
return VERTICAL;
}
throw new ObjectStreamException() {
};
}
完整示例代码:
public class Orientation implements Serializable {
public static final Orientation HORIZONTAL = new Orientation(1);
public static final Orientation VERTICAL = new Orientation(2);
private int value;
public Orientation(int value) {
this.value = value;
}
protected Object readResolve() throws ObjectStreamException {
System.out.println("Orientation.readResolve...");
if (value == 1) {
return HORIZONTAL;
}
if (value == 2) {
return VERTICAL;
}
throw new ObjectStreamException() {
};
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Orientation original = HORIZONTAL;
File file = new File("Orientation.data");
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(original);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
Orientation saved = (Orientation) in.readObject();
System.out.println(saved);
System.out.println(saved == HORIZONTAL); // true
}
}
}
版本管理
如果使用序列化来保存对象,就需要考虑在程序演化时会有什么问题
无论类的定义产生了什么样的变化,它的 SHA 指纹也会跟着变化,而我们都知道对象输入流将拒绝读入具有不同指纹的对象。但是,类可以表明它对其早期版本保持兼容,要想这样做,就必须首先获得这个类的早期版本的指纹:
serialver v2ch02.Orientation
v2ch02.Orientation: private static final long serialVersionUID = 8180666149608210912L;
这个类的所有较新的版本都必须把 serialVersionUID
常量定义为与最初版本的指纹相同
public static final long serialVersionUID = 8180666149608210912L;
如果一个类具有名为 serialVersionUID
的静态数据成员,它就不再需要人工地计算其指纹,而只需直接使用这个值
一旦这个静态数据成员被置于某个类的内部,那么序列化系统就可以读人这个类的对象的不同版本
如果这个类只有方法产生了变化,那么在读入新对象数据时是不会有任何问题的。但是,如果数据域产生了变化,那么就可能会有问题
对象输入流会将这个类当前版本的数据域与被序列化的版本中的数据域进行比较,当然,对象流只会考虑非瞬时和非静态的数据域。如果这两部分数据域之间名字匹配而类型不匹配,那么对象输入流不会尝试将一种类型转换成另一种类型,因为这两个对象不兼容;如果被序列化的对象具有在当前版本中所没有的数据域,那么对象输入流会忽略这些额外的数据;如果当前版本具有在被序列化的对象中所没有的数据域,那么这些新添加的域将被设置成它们的默认值
为克隆使用序列化
序列化机制有一种很有趣的用法:即提供了一种克隆对象的简便途径,只要对应的类是可序列化的即可。其做法很简单:直接将对象序列化到输出流中,然后将其读回。这样产生的新对象是对现有对象的一个深拷贝( deep copy )。在此过程中,我们不必将对象写出到文件中,因为可以用 ByteArrayOutputStream
将数据保存到字节数组中
public Object clone() throws CloneNotSupportedException {
try {
// save the object to a byte array
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(this);
}
// read a clone of the object from the byte array
try (InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return in.readObject();
}
} catch (IOException | ClassNotFoundException e) {
CloneNotSupportedException e2 = new CloneNotSupportedException();
e2.initCause(e);
throw e2;
}
}
当心这个方法,尽管它很灵巧,但是它通常会比显式地构建新对象并复制或克隆数据域的克隆方法慢得多
操作文件
Path
和 Files
类封装了在用户机器上处理文件系统所需的所有功能。
Path
接口和 Files
类是在 Java SE 7 中新添加进来的,它们用起来比自 JDK 1.0 以来就一直使用的 File
类要方便得多
Path
Path
表示的是一个目录名序列,其后还可以跟着一个文件名。路径中的第一个部件可以是根部件,例如 /
或 C:\
,而允许访问的根部件取决于文件系统。以根部件开始的路径是绝对路径;否则,就是相对路径
Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "user.properties");
静态的 Paths.get
方法接受一个或多个字符串,并将它们用默认文件系统的路径分隔符(类 Unix 文件系统是 /
, Windows 是 \
)连接起来。然后它解析连接起来的结果,如果其表示的不是给定文件系统中的合法路径,那么就抛出 InvalidPathException
异常。这个连接起来的结果就是一个 Path
对象
get
方法可以获取包含多个部件构成的单个字符串
String baseDir = props.getProperty("base.dir");
Path basePath = Paths.get(baseDir);
路径不必对应着某个实际存在的文件,它仅仅只是一个抽象的名字序列。当你想要创建文件时,首先要创建一个路径,然后才调用方法去创建对应的文件
组合或解析路径是司空见惯的操作,调用 p.resolve(q)
将按照下列规则返回一个路径:
- 如果
q
是绝对路径, 则结果就是q
- 否则,根据文件系统的规则,将 “
p
后面跟q
” 作为结果
resolve
方法有一种快捷方式,它接受一个字符串而不是一个路径
resolveSibling
通过解析指定路径的父路径产生其兄弟路径
resolve
的对立面是 relativize
,即调用 p.relativize(r)
将产生路径 ,而对 q
进行解析的结果正是 r
normalize
方法将移除所有冗余的 .
和 ..
部件(或者文件系统认为冗余的所有部件)
toAbsolutePath
方法将产生给定路径的绝对路径,该绝对路径从根部件开始
Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "myuser.properties");
System.out.println(absolute); // \home\harry
System.out.println(relative); // myprog\conf\myuser.properties
Path workPath = absolute.resolve("work");
System.out.println(workPath); // \home\harry\work
Path tempPath = workPath.resolveSibling("temp");
System.out.println(tempPath); // \home\harry\temp
Path relativize = workPath.relativize(tempPath);
System.out.println(relativize); // ..\temp
Path myprog = Paths.get("/home/cay/../fred/./myprog");
Path normalize = myprog.normalize();
System.out.println(normalize); // \home\fred\myprog
Path
类有许多有用的方法用来将路径断开
Path p = Paths.get("C:", "home", "fred", "myprog.properties");
Path parent = p.getParent(); // C:\home\fred
Path file = p.getFileName(); // myprog.properties
Path root = p.getRoot(); // C:\
还可以从 Path
对象中构建 Scanner
对象
Scanner in = new Scanner(Paths.get("employee.txt"));
偶尔,你可能需要与遗留系统的 API 交互,它们使用的是 File
类而不是 Path
接口。Path
接口有一个 toFile
方法,而 File
类有一个 toPath
方法
java.nio.file.Paths 方法名称 |
方法声明 | 描述 |
---|---|---|
get |
public static Path get(String first, String... more) |
通过连接给定的字符串创建一个路径 |
java.nio.file.Path 方法名称 |
方法声明 | 描述 |
---|---|---|
resolve |
Path resolve(Path other); Path resolve(String other); |
如果 other 是绝对路径,那么就返回 other ;否则,返回通过连接 this 和 other 得的路径 |
resolveSibling |
Path resolveSibling(Path other); Path resolveSibling(String other); |
如果 other 是绝对路径,那么就返回 other ,返回通过连接 this 的父路径和 other 获得的路径 |
relativize |
Path relativize(Path other); |
返回用 this 进行解析,相对于 other 的相对路径 |
normalize |
Path normalize(); |
移除诸如 . 和 .. 等冗余的路径元素 |
toAbsolutePath |
Path toAbsolutePath(); |
返回与该路径等价的绝对路径 |
getParent |
Path getParent(); |
返回父路径,或者在该路径没有父路径时 ,返回 null |
getFileName |
Path getFileName(); |
返回该路径的最后一个部件,或者在该路径没有任何部件时,返回 null |
getRoot |
Path getRoot(); |
返回该路径的根部件,或者在该路径没有任何根部件时,返回 null |
toFile |
File toFile(); |
从该路径中创建一个 File 对象 |
java.io.File 方法名称 |
方法声明 | 描述 |
---|---|---|
toPath |
public Path toPath() |
从该文件中创建一个 Path 对象 |
读写文件
Files
以字节方式读入文件:
Path path = Paths.get("employee.txt");
byte[] bytes = Files.readAllBytes(path);
String str = new String(bytes, StandardCharsets.UTF_8);
将文件当作行序列读入:
List<String> lines = Files.readAllLines(path, charset);
写出一个字符串到文件中,这将清除文件内容:
Files.write(path, content.getBytes(charset));
向指定文件追加内容,可以调用:
Files.write(path, content.getBytes(charset), StandardOpenOption.APPEND);
将一个行的集合写出到文件中:
Files.write(path, lines);
这些简便方法适用于处理中等长度的文本文件,如果要处理的文件长度比较大,或者是二进制文件,那么还是应该使用所熟知的输入/输出流或者读入器 /写出器:
InputStream in = Files.newInputStream(path);
OutputStream out = Files.newOutputStream(path);
BufferedReader br = Files.newBufferedReader(path, charset);
BufferedWriter bw = Files.newBufferedWriter(path, charset);
java.nio.file.Files 方法名称 |
方法声明 | 描述 |
---|---|---|
readAllBytes |
public static byte[] readAllBytes(Path path) throws IOException |
读入文件的内容 |
write |
public static Path write(Path path, byte[] bytes, OpenOption... options) throws IOException public static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) throws IOException public static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) throws IOException |
将给定内容写出到文件中,并返回 path |
newInputStream newOutputStream newBufferedReader newBufferedWriter |
public static InputStream newInputStream(Path path, OpenOption... options) throws IOException public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException public static BufferedReader newBufferedReader(Path path, Charset cs) throws IOException public static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) throws IOException |
打开一个文件,用于读入或写出 |
创建文件和目录
创建新目录可以调用
Files.createDirectory(path);
其中,路径中除最后一个部件外,其他部分都必须是己存在的 要创建路径中的中间目录,应该使用
Files.createDirectories(path);
可以使用下面的语句创建一个空文件:
Files.createFile(path);
如果文件已经存在了,那么这个调用就会抛出异常。检查文件是否存在和创建文件是原子性的,如果文件不存在,该文件就会被创建,并且其他程序在此过程中是无法执行文件创建操作的
有些便捷方法可以用来在给定位置或者系统指定位置创建临时文件或临时目录:
Path tempFile = Files.createTempFile(dir, prefix, suffix);
Path tempFile = Files.createTempFile(prefix, suffix);
Path tempDirectory = Files.createTempDirectory(dir, prefix);
Path tempDirectory = Files.createTempDirectory(prefix);
其中, dir
是一个 Path
对象, prefix
和 suffix
是可以为 null
的字符串。例如,调用 Files.createTempFile(null, ".txt ")
可能会返回一个像 tmp/1234405522364837194.txt
这样的路径
在创建文件或目录时,可以指定属性,例如文件的拥有者和权限。但是,指定属性的细节取决于文件系统
java.nio.file.Files 方法名称 |
方法声明 | 描述 |
---|---|---|
createFile createDirectory createDirectories |
public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException public static Path createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException public static Path createDirectories(Path dir, FileAttribute<?>... attrs) throws IOException |
创建一个文件或目录, createDirectories 方法还会创建路径中所有的中间目录 |
createTempFile createTempDirectory |
public static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs) throws IOException public static Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs) throws IOException |
在适合临时文件的位置,或者在给定的父目录中,创建一个临时文件或目录。返回所创建的文件或目录的路径 |
复制、移动和删除文件
将文件从一个位置复制到另一个位置:
Files.copy(fromPath, toPath);
移动文件(复制并删除原文件)
Files.move(fromPath, toPath);
如果目标路径已经存在,那么复制或移动将失败。如果想要覆盖已有的目标路径,可以使用 REPLACE_EXISTING
选项。如果想要复制所有的文件属性,可以使用 COPY_ATTRIBUTES
选项。也可以同时选择这两个选项:
Files.copy(fromPath, toPath, StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
可以使用 ATOMIC_MOVE
选项将移动操作定义为原子性的,这样可以保证要么移动操作成功完成,要么源文件继续保持在原来位置
Files.move(fromPath, toPath, StandardCopyOption.ATOMIC_MOVE);
可以将一个输入流复制到 Path
中,这表示要将该输入流存储到硬盘上。类似地,你可以将 Path
复制到输出流中:
Files.copy(inputStream, toPath);
Files.copy(fromPath, outputStream);
至于其他对 copy
的调用, 可以根据需要提供相应的复制选项
删除文件:
Files.delete(fromPath); // 文件不存在时抛出异常
boolean deleted = Files.deleteIfExists(fromPath); // 文件不存在时返回 false
用于文件操作的标准选项:
StandardOpenOption
:与 newBufferedWriter
、newInputStream
、newOutputStream
、write
一起使用
java.nio.file.StandardOpenOption |
描述 |
---|---|
READ |
用于读取而打开 |
WRITE |
用于写入而打开 |
APPEND |
如果用于写入而打开,那么在文件末尾追加 |
TRUNCATE_EXISTING |
如果用于写入而打开,那么移除已有内容 |
CREATE_NEW |
创建新文件并且在文件已存在的情况下会创建失败 |
CREATE |
自动在文件不存在的情况下创建新文件 |
DELETE_ON_CLOSE |
当文件被关闭时, 尽“可能”地删除该文件 |
SPARSE |
给文件系统一个提示,表示该文件是稀疏的 |
DSYNC 、SYNC |
要求对文件数据、数据和元数据的每次更新都必须同步地写入到存储设备中 |
StandardCopyOption
:与 copy
、move
一起使用
java.nio.file.StandardCopyOption |
描述 |
---|---|
ATOMIC_MOVE |
原子性地移动文件 |
COPY_ATTRIBUTES |
复制文件的属性 |
REPLACE_EXISTING |
如果目标已存在,则替换它 |
LinkOption
:与上面所有方法以及 exists
、isDirectory
、isRegularFile
等一起使用
java.nio.file.LinkOption |
描述 |
---|---|
NOFOLLOW_LINKS |
不要跟踪符号链接 |
FileVisitOption
:与 find
、walk
、walkFileTree
一起使用
java.nio.file.FileVisitOption |
描述 |
---|---|
FOLLOW_LINKS |
跟踪符号链接 |
java.nio.file.Files 方法名称 |
方法声明 | 描述 |
---|---|---|
copy move |
public static Path copy(Path source, Path target, CopyOption... options) throws IOException public static Path move(Path source, Path target, CopyOption... options) throws IOException |
将 from 复制或移动到给定位置,并返回 to |
copy |
public static long copy(InputStream in, Path target, CopyOption... options) throws IOException public static long copy(Path source, OutputStream out) throws IOException |
从输入流复制到文件中,或者从文件复制到输出流中,返回复制的字节数 |
delete deleteIfExists |
public static void delete(Path path) throws IOException public static boolean deleteIfExists(Path path) throws IOException |
删除给定文件或空目录。第一个方法在文件或目录不存在情况下抛出异常, 第二个方法在这种情况下会返回 false |
获取文件信息
所有 文件系统都会报告一个基本属性集,被封装在 BasicFileAttributes
接口中:
BasicFileAttributes basicFileAttributes = Files.readAttributes(path, BasicFileAttributes.class);
基本文件属性包括:
- 创建文件、最后一次访问以及最后一次修改文件 ,这些时间都表示成
java.nio.file.attribute.FileTime
- 文件是常规文件 、目录还是符号链接 ,或者三者都不是
- 文件尺寸
- 文件主键, 是某种类的对象,具体所属类与文件系统相关,有 能是文件的唯一标识符,也可能不是
如果用户文件系统兼容 POSIX (Windows 系统不兼容),那么你可以获取 PosixFileAttributes
实例:
PosixFileAttributes posixFileAttributes = Files.readAttributes(path, PosixFileAttributes.class);
java.nio.file.Files 方法名称 |
方法声明 | 描述 |
---|---|---|
exists isHidden isReadable isWritable isExecutable isRegularFile isDirectory isSymboliclink |
public static boolean exists(Path path, LinkOption... options) public static boolean isHidden(Path path) throws IOException public static boolean isReadable(Path path) public static boolean isWritable(Path path) public static boolean isExecutable(Path path) public static boolean isRegularFile(Path path, LinkOption... options) public static boolean isDirectory(Path path, LinkOption... options) public static boolean isSymbolicLink(Path path) |
检查由路径指定的文件的给定属性 |
size |
public static long size(Path path) throws IOException |
获取文件按字节数度量的尺寸 |
readAttributes |
public static <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) throws IOException |
读取类型为 A 的文件属性 |
java.nio.file.attribute.BasicFileAttributes 方法名称 |
方法声明 | 描述 |
---|---|---|
creationTime lastAccessTime lastModifiedTime isRegularFile isDirectory isSymbolicLink size fileKey |
FileTime creationTime(); FileTime lastAccessTime(); FileTime lastModifiedTime(); boolean isRegularFile(); boolean isDirectory(); boolean isSymbolicLink(); long size(); Object fileKey(); |
获取所请求的属性 |
访问目录中的项
静态的 Files.list
方法会返回一个可以读取目录中各个项的 Stream<Path>
对象。目录是被惰性读取的
list
方法不会进入子目录。为了处理目录中的所有子目录,需要使用 Files.walk
方法
Path path = Paths.get("target");
System.out.println("========================list begin");
try (final Stream<Path> entries = Files.list(path)) {
// do something
entries.forEach(System.out::println);
}
System.out.println("========================walk begin");
try (final Stream<Path> entries = Files.walk(path)) {
// do something
entries.forEach(System.out::println);
}
可以通过调用 File.walk(pathToRoot, depth)
来限制想要访问的树的深度。两种 walk
方法都具有 FileVisitOption...
的可变长参数,但是你只能提供一种选项: FOLLOW_LINKS
,即跟踪符号链接
注意:如果要过滤 walk
返回的路径,并且你的过滤标准涉及与目录存储相关的文件属性,例如尺寸、创建时间和类型(文件、目录、符号链接),那么应该使用 find
方法来替代 walk
方法 可以用某个谓词函数来调用这个方法,该函数接受一个路径和一个 BasicFileAttributes
对象。这样做唯一的优势就是效率高,因为路径总是会被读入,所以这些属性很容易获取
使用 Files.walk
方法来将一个目录复制到另一个目录:
Path path = Paths.get("target");
Path target = Paths.get("target_copy");
try (Stream<Path> entries = Files.walk(path)) {
entries.forEach(p -> {
try {
Path q = target.resolve(path.relativize(p));
if (Files.isDirectory(p)) {
Files.createDirectory(q);
} else {
Files.copy(p, q);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
遗憾的是,你无法很容易地使用 Files.walk
方法来删除目录树,因为你需要在删除父目录之前必须先删除子目录
使用目录流
Files.walk
方法会产生一个可以遍历目录中所有子孙的 Stream<Path>
对象。有时,你需要对遍历过程进行更加细粒度的控制。在这种情况下,应该使用 File.newDirectoryStream
对象,它会产生一个 DirectoryStream
。注意,它不是 java.util.stream.Stream
的子接口,而是专门用于目录遍历的接口 Iterable
的子接口,因此你可以在增强的 for 循环中使用目录流
Path path = Paths.get("target");
try (DirectoryStream<Path> entries = Files.newDirectoryStream(path)) {
for (Path entry : entries) {
// do something
}
}
java.nio.file.Files 方法名称 |
方法声明 | 描述 |
---|---|---|
newDirectoryStream |
public static DirectoryStream<Path> newDirectoryStream(Path dir) throws IOException public static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) throws IOException |
获取给定目录中可以遍历所有文件和目录的迭代器。第二个方法只接受那些与给定的 glob 模式匹配的项 |
walkFileTree |
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException public static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) throws IOException |
遍历给定路径的所有子孙,并将访问器应用于这些子孙之上 |
java.nio.file.SimpleFileVisitor<T> 方法名称 |
方法声明 | 描述 |
---|---|---|
visitFile |
public FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException |
在访问文件或目录时被调用,默认实现是不做任何操作而继续访问 |
preVisitDirectory postvisitDirectory |
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException public FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException |
在访问目录之前和之后被调用,默认实现是不做任何操作而继续访问 |
visitFileFailed |
public FileVisitResult visitFileFailed(T file, IOException exc) throws IOException |
如果在试图获取给定文件的信息时抛出异常,则该方法被调用。默认实现是重新抛出异常,这会导致访问操作以这个异常而终止。如果你想自己访问,可以覆盖这个方法 |
ZIP 文件系统
Paths
会在默认文件系统中查找路径,即在用户本地磁盘中的文件。你也可以有别的文件系统,其中最有用的之一是 ZIP 文件系统
从 ZIP 文档中复制出单个文件
Path path = Paths.get("test.zip");
FileSystem fs = FileSystems.newFileSystem(path, null);
String sourceName = "MyInterface.java";
Path targetPath = Paths.get("MyInterface.java");
Files.copy(fs.getPath(sourceName), targetPath);
fs.getPath
于任意文件系统来说,都与 Paths.get
类似
要列出 ZIP 文档中的所有文件 ,可以遍历文件树:
Path path = Paths.get("test.zip");
FileSystem fs = FileSystems.newFileSystem(path, null);
Files.walkFileTree(fs.getPath("/"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
});
java.nio.file.FileSystems 方法名称 |
方法声明 | 描述 |
---|---|---|
newFileSystem |
public static FileSystem newFileSystem(Path path, ClassLoader loader) throws IOException |
对所安装的文件系统提供者进行迭代,并且如果 loader 不为 null ,那么就还迭代给定的类加载器能够加载的文件系统,返回由第一个可以接受给定路径的文件系统提供者创建的文件系统。默认情况下,对于 ZIP 文件系统是有一个提供者的,它接受名字以 .zip 或 .jar 结尾的文件 |
java.nio.file.FileSystem 方法名称 |
方法声明 | 描述 |
---|---|---|
getPath |
public abstract Path getPath(String first, String... more); |
将给定的字符串连接起来创建一个路径 |
内存映射文件
大多数操作系统都可以利用虚拟内存实现来一个文件或者文件的一部分“映射”到内存中。然后,这个文件就可以当作是内存数组一样地访问,这比传统的文件操作要快得多
内存映射文件的性能
在同一台机器上,我们对 JDK 的 jre/lib 目录中的 37MB 的 rt.jar 文件用不同的方式来计算校验和
方法 | 时间 |
---|---|
普通输入流 | 110s |
带缓冲的输入流 | 9.9s |
随机访问文件 | 62s |
内存映射文件 | 7.2s |
内存映射比使用带缓冲的顺序输入要稍微快一点,比使用 RandomAccessFile
快很多
对于中等尺寸文件的顺序读入没有必要使用内存映射
java.nio 包使内存映射变得十分简单。首先,从文件中获得一个 通道( channel ),通道是用于磁盘文件的对象,它使我们可以访问诸如内存映射、文件加锁机制以及文件间快速数据传递等操作系统特性
FileChannel fileChannel = FileChannel.open(path, options);
然后,通过调用 FileChannel
类的 map
方法从这个通道中获得 ByteBuffer
。可以指定想要映射的文件区域与映射模式,支持的模式有三种:
FileChannel.MapMode.READ_ONLY
所产生的缓冲区是只读的,任何对该缓冲区写入的尝试都会导致 ReadOnlyBufferException 异常FileChannel.MapMode.READ_WRITE
:所产生的缓冲区是可写的,任何修改都会在某个时刻写回到文件中 注意,其他映射同 个文件的程序可能不能立即看到这些修改,多个程序同时进行文件映射的确切行为是依赖于操作系统的FileChannel.MapMode.PRIVATE
:所产生的缓冲区是可写的,但是任何修改对这个缓冲区来说都是私有的,不会传播到文件中
一旦有了缓冲区,就可以使用 ByteBuffer
类和 Buffer
超类的方法读写数据了
缓冲区支持顺序和随机数据访问,它有一个可以通过 get
和 put
操作来移动的位置
顺序遍历缓冲区中的所有字节:
while (buffer.hasRemaining()) {
byte b = buffer.get();
// ...
}
随机访问:
for (int i = 0; i < buffer.limit(); i++) {
byte b = buffer.get();
// ...
}
Java 对二进制数据使用高位在前的排序机制,但是,如果需要以低位在前的排序方式处理包含二进制数字的文件,那么只需调用
buffer.order(ByteOrder.LITTLE_ENDIAN);
要查询缓冲区内当前的字节顺序,可以调用:
ByteOrder order = buffer.order();
在恰当的时机,以及当通道关闭时,会将修改写回到文件中
java.util.zip
包中包含一个 CRC32
类,可以用来计算一个字节序列的校验和
java.io.FileInputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
getChannel |
public FileChannel getChannel() |
返回用于访问这个输入流的通道 |
java.io.FileOutputStream 方法名称 |
方法声明 | 描述 |
---|---|---|
getChannel |
public FileChannel getChannel() |
返回用于访问这个输出流的通道 |
java.io.RandomAccessFile 方法名称 |
方法声明 | 描述 |
---|---|---|
getChannel |
public final FileChannel getChannel() |
返回用于访问这个文件的通道 |
java.nio.channels.FileChannel 方法名称 |
方法声明 | 描述 |
---|---|---|
open |
public static FileChannel open(Path path, OpenOption... options) throws IOException |
打开指定路径的文件通道,默认情况下,通道打开时用于读入 |
map |
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException; |
将文件的一个区域映射到内存中 |
java.nio.Buffer 方法名称 |
方法声明 | 描述 |
---|---|---|
hasRemaining |
public final boolean hasRemaining() |
如果 前的缓冲区位置没有到达这个缓冲区的界限位置, 返回 true |
limit |
public final int limit() |
返回这个缓冲区的界限位 没有任何值可用的第一个位置 |
java.nio.ByteBuffer 方法名称 |
方法声明 | 描述 |
---|---|---|
get |
public abstract byte get(); |
从当前位置获得一个字节,并将当前位置移动到下一个字节 |
get |
public abstract byte get(int index); |
从指定索引处获得一个字节 |
put |
public abstract ByteBuffer put(byte b); |
向当前位置推入一个字节,并将当前位置移动到下一个字节。返回对这个缓冲区的引用 |
put |
public abstract ByteBuffer put(int index, byte b); |
向指定索引处推入一个字节 返回对这个缓冲区的引用 |
get |
public ByteBuffer get(byte[] dst) public ByteBuffer get(byte[] dst, int offset, int length) |
用缓冲区中的字节来填充字节数组,或者字节数组的某个区域,并将当前位置向前移动读入的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出 BufferOverflowException 。 返回对这个缓冲区的引用dst :要填充的字节数组offset :要填充区域的偏移量length :要填充区域的长度 |
put |
public final ByteBuffer put(byte[] src) public ByteBuffer put(byte[] src, int offset, int length) |
将字节数组中的所有字节或者给定区域的字节都推入缓冲区中,并将当前位置向前移动写出的字节数个位置。如果缓冲区不够大,那么就不会读入任何字节,并抛出 BufferOverflowException 。返回对这个缓冲区的引用src :要写出的数组offset :要写出区域的偏移量length :要写出区域的长度 |
getXxx putXxx |
Xxx getXxx() Xxx getXxx(int index) ByteBuffer putXxx(Xxx value) ByteBuffer putXxx(int index, Xxx value) |
获得或放置一个二进制数。Xxx 是 Int 、Long 、Short 、Char 、Float 、Double 中的一个 |
order |
public final ByteOrder order() public final ByteBuffer order(ByteOrder bo) |
设置或获得字节顺序 |
allocate |
public static ByteBuffer allocate(int capacity) |
构建具有给定容量的缓冲区 |
wrap |
public static ByteBuffer wrap(byte[] array) |
构建具有指定容量的缓冲区,该缓冲区是对给定数组的包装 |
asCharBuffer |
public abstract CharBuffer asCharBuffer(); |
构建字符缓冲区,它是对这个缓冲区的包装。对该字符缓冲区的变更将在这个缓冲区中反映出来,但是该字符缓冲区有自己的位置、界限和标记 |
java.nio.CharBuffer 方法名称 |
方法声明 | 描述 |
---|---|---|
get |
public abstract char get(); public CharBuffer get(char[] dst) public CharBuffer get(char[] dst, int offset, int length) |
从这个缓冲区的当前位置开始,获取一个 char 值,或者一个范围内的所有 char 值,然后将位置向前移动越过所有读入的字符。最后两个方法将返回 this |
put |
public abstract CharBuffer put(char c); public final CharBuffer put(char[] src) public CharBuffer put(String src, int start, int end) public final CharBuffer put(String src) public CharBuffer put(CharBuffer src) |
从这个缓冲区的当前位置开始,放置一个 char 值,或者一个范围内的所有 char 值,然后将位置向前移动越过所有被写出的字符。当放置的值是从 CharBuffer 读入时,将读入所有剩余字符 所有方法将返回 this |
缓冲区数据结构
缓冲区是由具有相同类型的数值构成的数组, Buffer
类是一个抽象类,它有众多的具体子类,包括 ByteBuffer
、CharBuffer
、DoubleBuffer
、IntBuffer
、LongBuffer
、ShortBuffer
注意: StringBuffer
类与这些缓冲区没有关系
在实践中,最常用的是 ByteBuffer
、CharBuffer
,,每个缓冲区都具有
capacity
,一个容量,它永远不能改变position
,一个读写位置,下一个值将在此进行读写limit
,一个界限,超过它进行读写是没有意义的mark
,一个可选的标记,用于重复一个读入或写出操作
这些值满足下面的条件:
0 <= mark <= position <= limit <= capacity
使用缓冲区的主要目的是执行 “写,然后读入” 循环。假设我们有一个缓冲区,在一开始,它的位置为 0 ,界限等于容量。我们不断地调用 put
将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切换到读入操作了
这时调用 flip
方法将界限设置到当前位置,并把位置复位到 0 。现在在 remaining
方法返回正数时(它返回的值是 “界限- 位置
” ),不断地调用 get
在我们将缓冲区中所有的值都读入之后,调用 clear
使缓冲区为下一次写循环做好准备。clear
方法将位置复位到 0 ,并将界限复位到容量
如果你想重读缓冲区,可以使用 rewind
或 mark
/ reset
方法
要获取缓冲区,可以调用诸如 ByteBuffer.allocate
或 ByteBuffer.wrap
这样的静态方法
然后,可以用来自某个通道的数据填充缓冲区,或者将缓冲区的内容写出通道中
ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
channel.read(buffer);
channel.position(newpos);
buffer.flip();
channel.write(buffer);
java.nio.Buffer 方法名称 |
方法声明 | 描述 |
---|---|---|
clear |
public final Buffer clear() |
通过将位置复位到 0 ,并将界限设置到容量,使这个缓冲区为写出做好准备。返回 this |
flip |
public final Buffer flip() |
通过将界限设置到位置,并将位置复位到 0 ,使这个缓冲区为读人做好准备。返回 this |
rewind |
public final Buffer rewind() |
通过将读写位置复位到 0 ,并保持界限不变,使这个缓冲区为重新读入相同的值做好准备。返回 this |
mark |
public final Buffer mark() |
将这个缓冲区的标记设置到读写位置,返回 this |
reset |
public final Buffer reset() |
将这个缓冲区的位置设置到标记,从而允许被标记的部分可以再次被读入或写出,返回 this |
remaining |
public final int remaining() |
返回剩余可读入或可写出的值的数量,即界限与位置之间的差异 |
position |
public final int position() public final Buffer position(int newPosition) |
返回这个缓冲区的位置 |
capacity |
public final int capacity() |
返回这个缓冲区的容量 |
文件加锁机制
考虑一下多个同时执行的程序需要修改同一个文件的情形,很明显,这些程序需要以某种方式进行通信,不然这个文件很容易被损坏。文件锁可以解决这个问题,它可以控制对文件或文件中某个范围的字节的访问
要锁定一个文件,可以调用 FileChannel
类的 lock
或 trylock
方法:
FileChannel channel = FileChannel.open(path);
FileLock lock = channel.lock();
或者
FileLock lock = channel.tryLock();
第一个调用会阻塞直至可获得锁,而第二个调用将立即返回,要么返回锁,要么在锁不可获得的情况下返回 null
。这个文件将保持锁定状态,直至这个通道关闭,或者在锁上调用了 release
方法
锁定文件的一部分:
public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
如果 shared
标志为 false
,则锁定文件的目的是读写,而如果为 true
,则这是一个共享锁,它允许多个进程从文件读入,并阻止任何进程获得独占的锁。并非所有的操作系统都支持共事锁,因此你可能会在请求共享锁的时候得到的是独占的锁。调用 FileLock
类的 isShared
方法可以查询你所持有的锁的类型
注意:如果你锁定了文件的尾部,而这个文件的长度随后增长超过了锁定的部分,那么增长出来的额外区域是未锁定的,要想锁定所有的字节,可以使用 Long.MAX_VALUE
表示尺寸
要确保在操作完成时释放锁,与往常一样,最好在一个 try 语句中执行释放锁的操作:
try (FileLock lock = channel.lock()) {
// do something
}
文件加锁机制是依赖于操作系统的,下面是需要注意的几点:
- 在某些系统中,文件加锁仅仅是建议性的,如果一个应用未能得到锁,它仍旧可以向被另一个应用并发锁定的文件执行写操作
- 在某些系统中,不能在锁定一个文件的同时将其映射到内存中
- 文件锁是由整个 Java 虚拟机持有的。如果有两个程序是由同一个虚拟机启动的,那么它们不可能每一个都获得一个在同一个文件上的锁。当调用
lock
和trylock
方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出OverlappingFileLockException
- 在一些系统中,关闭一个通道会释放由 Java 虚拟机持有的底层文件上的所有锁。因此,在同一个锁定文件上应避免使用多个通道
- 在网络文件系统上锁定文件是高度依赖于系统的,因此应该尽量避免
java.nio.channels.FileChannel 方法名称 |
方法声明 | 描述 |
---|---|---|
lock |
public final FileLock lock() throws IOException |
在整个文件上获得一个独占的锁,这个方法将阻塞直至获得锁 |
tryLock |
public final FileLock tryLock() throws IOException |
在整个文件上获得一个独占的锁,或者在无法获得锁的情况下返回 null |
lock tryLock |
public abstract FileLock lock(long position, long size, boolean shared) throws IOException; public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException; |
在文件的一个区域上获得锁。第一个方法将阻塞直至获得锁,而第二个方法将在无法获得锁时返回 null position :要锁定区域的起始位置size :要锁定区域的尺寸shared :true 为共享锁, false 为独占锁 |
java.nio.channels.FileLock 方法名称 |
方法声明 | 描述 |
---|---|---|
close |
public final void close() throws IOException |
释放这个锁 |
正则表达式
正则表达式( regular expression )用于指定字符串的模式,你可以在任何需要定位匹配某种特定模式的字符串的情况下使用正则表达式
[Jj]ava.+
匹配下列形式的所有字符串:
- 第一个字母是
J
或j
- 接下来三个字母是
ava
- 字符串的其余部分由一个或多个任意的字符构成
对于大多数情况,一小部分很直观的语法结构就足够用了
- 字符类( character class )是一个括在括号中的可选择的字符集,例如,
[Jj]
、[0-9]
、[A-Za-z]
、[^0-9]
。这里-
表示一个范围(所有 Unicode 值落在两个边界范围之内的字符),而^
表示补集(除了指定字符之外的所有字符) - 如果字符类中包含
-
,那么它必须是第一项或最后一项;如果要包含[
,那么它必须第一项;如果要包含^
,那么它可以是除开始位置之外的任何位置。其中,你只需要转义[
和\
- 有许多预定的字符类,例如
\d
(数字)和\p{Sc}
(Unicode 货币符号)
正则表达式语法分为:
- 字符
- 字符类
- 序列和选择
- 群组
- 量词
- 边界匹配
字符表达式 | 描述 | 示例 |
---|---|---|
c ,除 `.*+?{ |
()[^ | 字符表达式 |
----------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------- |
之外 | 字符 c |
] |
. |
任何除行终止符之外的字符,或者在 DOTALL 标志被设置时表示任何字符 | |
\x{p} |
十六进制码为 p 的 Unicode 码点 | \x{1D546} |
\uhhhh 、 \xhh 、 \0o 、 \0oo 、 \0ooo |
具有给定十六进制或八进制值的码元 | \uFEFF |
\a 、\e 、\f 、\n 、\r 、\t |
响铃符( \x{7} )、转义符( \x{18} )、换页符( \x{8} ) 、换行符( \x{A} ) 、回车符( \x{D} )、指标符( \x{9} ) |
\n |
\cc ,其中 c 在 [a-z] 的范围内,或者是 @[\]^_? 之一 |
对应于字符 c 的控制字符 |
\cH 是退格符( \x{8} ) |
\c ,其中 c 不在 [A-Za-z0-9] 的范围内 |
字符 c |
\\ |
\Q...\E |
在左引号和右引号之间的所有字符 | \Q(...)\E 匹配字符串 (...) |
- 大部分字符都可以与它们自身匹配
.
符号可以匹配任何字符(有可能不包括行终止符 ,这取决于标志的设置)- 使用
\
作为转义字符,例如,\.
匹配句号而\\
匹配反斜线 ^
和$
分别匹配一行的开头和结尾- 如果
X
和Y
是正则表达式,那么XY
表示 “任何 X 的匹配后面跟随 Y 的匹配”,X | Y
表示“任何 X 或 Y 的匹配” - 可以将量词运用到表达式 X :
X+
(一个或多个)、X*
( 0 个或多个)与X?
( 0 个或 1 个) - 默认情况下,量词要匹配能够使整个匹配成功的最大可能的重复次数。你可以修改这种行为,方法是使用后缀
?
(使用勉强或吝啬匹配,也就是匹配最小的重复次数)或使用后缀+
(使用占有或贪婪匹配, 也就是即使让整个匹配失败,也要匹配最大的重复次数) - 使用群组来定义子表达式,其中群组用括号
()
括起来。例如,([+-]?)([0-9]+)
。然后可以询问模式匹配器,让其返回每个组的匹配,或者用\
来引用某个群组,其中n
是群组号(从\1
开始)
在使用正则表达式的各种程序和类库之间,表达式语法并未完全标准化
正则表达式的最简单用法就是测试某个特定的字符串是否与它匹配:
Pattern pattern = Pattern.compile(patternString);
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
// do something
}
input
可以是任何实现了 CharSequence
接口的类的对象,例如 String
、StringBuilder
、CharBuffer
在编译这个模式时,可以设置一个或多个标志:
Pattern pattern = Pattern.compile(expression,
Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);
或者可以在模式中指定它们:
String regex = "(?iU:expression)";
Pattern.CASE_INSENSITIVE
或r
:匹配字符时忽略字母的大小写,默认情况下,这个标志只考虑 US ASCII 字符Pattern.UNICODE_CASE
或u
:当与CASE_INSENSITIVE
组合使用时,用 Unicode 字母的大小写来匹配Pattern.UNICODE_CHARACTER_CLASS
或U
:选择 Unicode 字符类代替 POSIX ,其中蕴含了UNICODE_CASE
Pattern.MULTILINE
或m
:^
和$
匹配行的开头和结尾,而不是整个输入的开头和结尾Pattern.UNIX_LINES
或d
:在多行模式中匹配^
和$
时,只有\n
被识别成行终止符Pattern.DOTALL
或s
:当使用这个标志时,.
符号匹配所有字符,包括行终止符Pattern.COMMENTS
或x
:空白字符和注释(从#
到行末尾)将被忽略Pattern.LITERAL
:该模式将被逐字地采纳,必须精确匹配,因字母大小写而造成的差异除外Pattern.CANON_EQ
:考虑 Unicode 字符规范的等价性,例如,u
后面跟随¨
(分音符号)匹配ü
如果想要在集合或流中匹配元素,那么可以将模式转换为谓词:
Stream<String> strings = Stream.of("a", "b", "c");
Stream<String> result = strings.filter(pattern.asPredicate());
如果正则表达式包含群组,那么 Matcher
对象可以揭示群组的边界
public int start(int group)
public int end(int group)
可以直接通过调用下面的方法抽取匹配的字符串:
public String group(int group)
群组 0 是整个输入,而用于第一个实际群组的群组索引是 1 。 调用 groupCount
方法可以获得全部群组的数量。对于具名的组,使用下面的方法
public int start(String name)
public int end(String name)
public String group(String name)
嵌套群组是按照前括号排序的
假设有模式 (([1-9]|1[0-2]):([0-5][0-9]))[ap]m
和输出 11:59am
String expression = "(([1-9]|1[0-2]):([0-5][0-9]))[ap]m";
String input = "11:59am";
Pattern pattern = Pattern.compile(expression);
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
int count = matcher.groupCount();
System.out.println(count);
for (int i = 0; i <= count; i++) {
System.out.println("=================" + i);
System.out.println("start :: " + matcher.start(i));
System.out.println("end :: " + matcher.end(i));
System.out.println("group :: " + matcher.group(i));
}
}
输出:
3
=================0
start :: 0
end :: 7
group :: 11:59am
=================1
start :: 0
end :: 5
group :: 11:59
=================2
start :: 0
end :: 2
group :: 11
=================3
start :: 3
end :: 5
group :: 59
通常,你不希望用正则表达式来匹配全部输人,而只是想找出输入中一个或多个匹配的子字符串。这时可以使用 Matcher
类的 find
方法来查找匹配内容,如果返回 true
,再使用 start
和 end
方法来查找匹配的内容,或使用不带引元的 group
方法来获取匹配的字符串
String expression = "[ap]m";
String input = "11:59am";
Pattern pattern = Pattern.compile(expression);
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
String match = matcher.group();
System.out.println(match); // am
}
Matcher
类的 replaceAll
方法将正则表达式出现的所有地方都用替换字符串来替换,将所有的数字序列都替换成#
字符
Pattern pattern = Pattern.compile("[0-9]+");
Matcher matcher = pattern.matcher(input);
String output = matcher.replaceAll("#");
替换字符串可以包含对模式中群组的引用: $n
表示替换成第 n 个群组, ${name}
被替换为具有给定名字的组,因此我们需要用 \$
来表示在替换文本中包含一个 $
字符
如果字符串中包含 $
和 \
,但是又不希望它们被解释成群组的替换符,那么就可以调用
matcher.replaceAll(matcher.quoteReplacement(str));
replaceFirst
方法将只替换模式的第一次出现
Pattern
类有一个 split
方法,它可以用正则表达式来匹配边界,从而将输入分割成字符串数组
如果有多个标记,那么可以惰性地获取它们:
Stream<String> token = pattern.splitAsStream(input);
如果不关心预编译模式和惰性获取,那么可以使用 String.split
方法
String[] tokens = input.split("\\s*,\\s*");
java.util.regex.Pattern 方法名称 |
方法声明 | 描述 |
---|---|---|
compile |
public static Pattern compile(String regex) public static Pattern compile(String regex, int flags) |
把正则表达式字符串编译到一个用于快速处理匹配的模式对象中 |
matcher |
public Matcher matcher(CharSequence input) |
返回 matcher 对象,你可以用它在输入中定位模式的匹配 |
split splitAsStream |
public String[] split(CharSequence input) public String[] split(CharSequence input, int limit) public Stream<String> splitAsStream(final CharSequence input) |
将输入分割成标记,其中模式指定了分隔符的形式。 返回标记数组,分隔符并非标记的一部分 |
java.util.regex.Matcher 方法名称 |
方法声明 | 描述 |
---|---|---|
matches |
public boolean matches() |
如果输入匹配模式, 返回 true |
lookingAt |
public boolean lookingAt() |
如果输入的开头匹配模式, 返回 true |
find |
public boolean find() public boolean find(int start) |
尝试查找下一个匹配,如果找到了另一个匹配,则返回 true start :开始查找的索引位置 |
start end |
public int start() public int end() |
返回当前匹配的开始索引和结尾之后的索引位置 |
group |
public String group() |
返回当前的匹配 |
groupCount |
public int groupCount() |
返回输入模式中的群组数量 |
start end |
public int start(int group) public int end(int group) |
返回当前匹配中给定群组的开始和结尾之后的位置group :群组索引(从 0 开始),或者表示整个匹配的 0 |
group |
public String group(int group) |
返回匹配给定群组的字符串 |
replaceAll replaceFirst |
public String replaceAll(String replacement) public String replaceFirst(String replacement) |
返回从匹配器输入获得的通过将所有匹配或第一个匹配用替换字符串替换之后的字符串replacement :替换字符串,它可以包含用 $n 表示的对群组的引用,这时需要用 \$ 来表示字符串中包含一个 ` |
---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
matches |
public boolean matches() |
如果输入匹配模式, 返回 true |
lookingAt |
public boolean lookingAt() |
如果输入的开头匹配模式, 返回 true |
find |
public boolean find() public boolean find(int start) |
尝试查找下一个匹配,如果找到了另一个匹配,则返回 true start :开始查找的索引位置 |
start end |
public int start() public int end() |
返回当前匹配的开始索引和结尾之后的索引位置 |
group |
public String group() |
返回当前的匹配 |
groupCount |
public int groupCount() |
返回输入模式中的群组数量 |
start end |
public int start(int group) public int end(int group) |
返回当前匹配中给定群组的开始和结尾之后的位置group :群组索引(从 0 开始),或者表示整个匹配的 0 |
group |
public String group(int group) |
返回匹配给定群组的字符串 |
符号 | ||
quoteReplacement |
public static String quoteReplacement(String s) |
引用 str 中的所有 \ 和 ` |
---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
matches |
public boolean matches() |
如果输入匹配模式, 返回 true |
lookingAt |
public boolean lookingAt() |
如果输入的开头匹配模式, 返回 true |
find |
public boolean find() public boolean find(int start) |
尝试查找下一个匹配,如果找到了另一个匹配,则返回 true start :开始查找的索引位置 |
start end |
public int start() public int end() |
返回当前匹配的开始索引和结尾之后的索引位置 |
group |
public String group() |
返回当前的匹配 |
groupCount |
public int groupCount() |
返回输入模式中的群组数量 |
start end |
public int start(int group) public int end(int group) |
返回当前匹配中给定群组的开始和结尾之后的位置group :群组索引(从 0 开始),或者表示整个匹配的 0 |
group |
public String group(int group) |
返回匹配给定群组的字符串 |
|
| reset
| public Matcher reset()
public Matcher reset(CharSequence input)
| 复位匹配器的状态。第二个方法将使匹配器作用于另一个不同的输入。这两个方法都返回 this
|