20220424 Java核心技术 卷2 高级特性 2

输入与输出

输入/输出流

在 Java API 中,可以从其中读入一个字节序列的对象称做 输入流 ,而可以向其中写入一个字节序列的对象称做 输出流 。 这些字节序列的来源地和目的地可以是文件,而且通常都是文件,但是也可以是网络连接,甚至是内存块。抽象类 InputStreamOutputStream 构成了输入/输出( I/O )类层次结构的基础

这些输入/输出流与在 Java SE 8 的流没有任何关系

因为面向字节的流不便于处理以 Unicode 形式存储的信息(回忆一下, Unicode 每个字符都使用了多个字节来表示),所以从抽象类 ReaderWriter 中继承出来了一个专门用于处理 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;

readwrite 方法在执行时都将阻塞,直至字节确实被读入或写出。这就意味着如果流不能被立即访问(通常是因为网络连接忙),那么当前的线程将被阻塞

available 方法可以去检查当前可读入的字节数 ,这意味着像下面这样的代码片段就不可能被阻塞:

InputStream in = null;
int bytesAvailable = in.available();
if (bytesAvailable > 0) {
    byte[] data = new byte[bytesAvailable];
    in.read(data);
}

当你完成对输入/输出流的读写时,应该通过调用 close 方法来关闭它,这个调用会释放掉十分有限的操作系统资源。关闭一个输出流的同时还会冲刷用于该输出流的缓冲区:所有被临时置于缓冲区中,以便用更大的 的形式传递的字节在关闭输出流时都将被送出。特别是,如果不关闭文件,那么写出字节的最后一个包可能将永远也得不到传递。当然,我们还可以使用 flush 方法来人为地冲刷这些输出

即使某个输入/输出流类提供了使用原生的 readwrite 功能的某些具体方法,应用系统的程序员还是很少使用它 ,因为大家感兴趣的数据可能包含数字、字符串和对象,而不是原生字节。我们可以使用众多从基本 InputStreamOutputStream 类导出的某个输入/输出类,而不只是直接使用字节

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 拥有一个流家族,包含各种输入 / 输出流类型

InputStreamOutputStream 可以读写单个字节或字节数组:

img

DataInputStreamDataOutputStream 可以以二进制格式读写所有的基本 Java 类型。最后,还包含了多个很有用的输入/输出流,例如,ZipInputStreamZipOutputStream 可以以常见的 ZIP 压缩格式读写文件

对于 Unicode 文本 可以使用抽象类 ReaderWriter 的子类。 ReaderWriter 类的基本方法与 InputStreamOutputStream 中的方法类似

public int read() throws IOException
public void write(int c) throws IOException

read 方法将返回一个 Unicode 码元( 1 - 65535 之间的整数),或者在碰到文件结尾时返回 -1write 方法在被调用时需要传递一个 Unicode 码元

img

还有 4 个附加的接口: CloseableFlushableReadableAppendable

  • InputStreamOutputStreamReaderWriter 都实现了 Closeable 接口
  • OutputStreamWriter 实现了 Flushable 接口
  • Writer 实现了 Appendable
  • Reader 实现了 Readable

img

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 值序列的基本属性, StringCharBufferStringBuilderStringBuffer 都实现了它

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); 返回由存储在 startIndexendIndex-1 处的所有码元构成的 CharSequence
toString public String toString(); 返回这个序列中所有码元构成的字符串

组合输入/输出流过滤器

FileInputStreamFileOutputStream 可以提供附着在一个磁盘文件上的输入流和输出流,而你只需向其构造器提供文件名或文件的完整路径名

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 获得它

与抽象类 InputStreamOutputStream 一样,FileInputStreamFileOutputStream 类只支持在字节级别上的读写:

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();

FilterInputStreamFilterOutputStream 的子类用于向处理字节的输入/输出流添加额外的功能

你可以通过嵌套过滤器来添加多重功能。例如,输入流在默认情况下是不被缓冲区缓存的,也就是说,每个对 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 码元的读入器

OutputStreamWriterInputStreamReader 默认使用主机系统所使用的默认字符编码方式,应该通过构造器指定编码方式

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.outPrintStream 对象

为了与已有的代码兼容, System.inSystem.outSystem.err 仍旧是输入/输出流而不是读入器和写出器。但是现在 PrintStream 类在内部采用与 PrintWriter 相同的方式将 Unicode 字符转换成了默认的主机编码方式,当你在使用 printprintln 方法时,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

读写二进制数据

文本格式对于测试和调试而言会显得很方便,因为它是人类可阅读的,但是它并不像以二进制格式传递数据那样高效

DataInputDataOutput 接口

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 类同时实现了 DatalnputDataOutput 接口。为了读写随机访问文件,可以使用在 readInt / writeIntreadCharwriteChar 之类的方法

java.io.RandomAccessFile 方法名称 方法声明 描述
构造器 public RandomAccessFile(File file, String mode) throws FileNotFoundException
public RandomAccessFile(String name, String mode) throws FileNotFoundException
file :要打开的文件
moder 表示只读模式; 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 文件,这个项称作清单。你可以使用 JarInputStreamJarOutputStream 类来读写清单项

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) 创建一个将压缩数据写出到指定的 OutputStreamZipOutputStream
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 :压缩方法, DEFLATEDSTORED
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 :压缩方法, DEFLATEDSTORED
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 )的非常通用的机制 ,它可以将任何对象写出到输出流中,并在之后将其读回

保存和加载序列化对象

为了保存对象,可以直接使用 ObjectOutputStreamwriteObject 方法

为了将这些对象读回,可以使用 ObjectInputStream 对象的 readObject 方法以这些对象被写出时的顺序获得它们

对希望在对象输出流中存储或从对象输入流中恢复的所有类必须实现 Serializable 接口

你只有在写出对象时才能用 writeObject / readObject 方法,对于基本类型值,你需要使用诸如 writeInt / readIntwriteDouble / 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 类,它提供了自己的 readObjectwriteObject 方法,这些方法将日期写出为从纪元( UTC 时间 1970年1月1日0点 )开始的毫秒数

readObjectwriteObject 方法只需要保存和加载它们的数据域,而不需要关心超类数据和任何其他类的信息

除了让序列化机制来保存和恢复对象数据,类还可以定义它自己的机制。这个类必须实现 Externalizable 接口,需要实现它定义两个方法

readObjectwriteObject 不同,这些方法对包括超类数据在内的整个对象的存储和恢复负全责。在写出对象时, 序列化机制在输出流中仅仅只是记录该对象所属的类。在读入可外部化的类时,对象输入流将用无参构造器创建一个对象,然后调用 readExternal 方法

警告readObjectwriteObject 是私有的, 且只能被序列化机制调用。与此不同的是, readExternalwriteExternal 方法是公共的。特别是, 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.HORIZONTALOrientation.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;
    }
}

当心这个方法,尽管它很灵巧,但是它通常会比显式地构建新对象并复制或克隆数据域的克隆方法慢得多

操作文件

PathFiles 类封装了在用户机器上处理文件系统所需的所有功能。

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 ;否则,返回通过连接 thisother 得的路径
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 对象, prefixsuffix 是可以为 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 :与 newBufferedWriternewInputStreamnewOutputStreamwrite 一起使用

java.nio.file.StandardOpenOption 描述
READ 用于读取而打开
WRITE 用于写入而打开
APPEND 如果用于写入而打开,那么在文件末尾追加
TRUNCATE_EXISTING 如果用于写入而打开,那么移除已有内容
CREATE_NEW 创建新文件并且在文件已存在的情况下会创建失败
CREATE 自动在文件不存在的情况下创建新文件
DELETE_ON_CLOSE 当文件被关闭时, 尽“可能”地删除该文件
SPARSE 给文件系统一个提示,表示该文件是稀疏的
DSYNCSYNC 要求对文件数据、数据和元数据的每次更新都必须同步地写入到存储设备中

StandardCopyOption :与 copymove 一起使用

java.nio.file.StandardCopyOption 描述
ATOMIC_MOVE 原子性地移动文件
COPY_ATTRIBUTES 复制文件的属性
REPLACE_EXISTING 如果目标已存在,则替换它

LinkOption :与上面所有方法以及 existsisDirectoryisRegularFile 等一起使用

java.nio.file.LinkOption 描述
NOFOLLOW_LINKS 不要跟踪符号链接

FileVisitOption :与 findwalkwalkFileTree 一起使用

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 超类的方法读写数据了

缓冲区支持顺序和随机数据访问,它有一个可以通过 getput 操作来移动的位置

顺序遍历缓冲区中的所有字节:

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)
获得或放置一个二进制数。XxxIntLongShortCharFloatDouble 中的一个
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 类是一个抽象类,它有众多的具体子类,包括 ByteBufferCharBufferDoubleBufferIntBufferLongBufferShortBuffer

注意: StringBuffer 类与这些缓冲区没有关系

在实践中,最常用的是 ByteBufferCharBuffer ,,每个缓冲区都具有

  • capacity ,一个容量,它永远不能改变
  • position ,一个读写位置,下一个值将在此进行读写
  • limit ,一个界限,超过它进行读写是没有意义的
  • mark ,一个可选的标记,用于重复一个读入或写出操作

img

这些值满足下面的条件:

0 <= mark <= position <= limit <= capacity

使用缓冲区的主要目的是执行 “写,然后读入” 循环。假设我们有一个缓冲区,在一开始,它的位置为 0 ,界限等于容量。我们不断地调用 put 将值添加到这个缓冲区中,当我们耗尽所有的数据或者写出的数据量达到容量大小时,就该切换到读入操作了

这时调用 flip 方法将界限设置到当前位置,并把位置复位到 0 。现在在 remaining 方法返回正数时(它返回的值是 “界限- 位置” ),不断地调用 get 在我们将缓冲区中所有的值都读入之后,调用 clear 使缓冲区为下一次写循环做好准备。clear 方法将位置复位到 0 ,并将界限复位到容量

如果你想重读缓冲区,可以使用 rewindmark / reset 方法

要获取缓冲区,可以调用诸如 ByteBuffer.allocateByteBuffer.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 类的 locktrylock 方法:

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 虚拟机持有的。如果有两个程序是由同一个虚拟机启动的,那么它们不可能每一个都获得一个在同一个文件上的锁。当调用 locktrylock 方法时,如果虚拟机已经在同一个文件上持有了另一个重叠的锁,那么这两个方法将抛出 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 :要锁定区域的尺寸
sharedtrue 为共享锁, false 为独占锁
java.nio.channels.FileLock 方法名称 方法声明 描述
close public final void close() throws IOException 释放这个锁

正则表达式

正则表达式( regular expression )用于指定字符串的模式,你可以在任何需要定位匹配某种特定模式的字符串的情况下使用正则表达式

[Jj]ava.+ 匹配下列形式的所有字符串:

  • 第一个字母是 Jj
  • 接下来三个字母是 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 匹配字符串 (...)
  • 大部分字符都可以与它们自身匹配
  • . 符号可以匹配任何字符(有可能不包括行终止符 ,这取决于标志的设置)
  • 使用 \ 作为转义字符,例如,\. 匹配句号而 \\ 匹配反斜线
  • ^$分别匹配一行的开头和结尾
  • 如果 XY 是正则表达式,那么 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 接口的类的对象,例如 StringStringBuilderCharBuffer

在编译这个模式时,可以设置一个或多个标志:

Pattern pattern = Pattern.compile(expression,
        Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE);

或者可以在模式中指定它们:

String regex = "(?iU:expression)";
  • Pattern.CASE_INSENSITIVEr :匹配字符时忽略字母的大小写,默认情况下,这个标志只考虑 US ASCII 字符
  • Pattern.UNICODE_CASEu :当与 CASE_INSENSITIVE 组合使用时,用 Unicode 字母的大小写来匹配
  • Pattern.UNICODE_CHARACTER_CLASSU :选择 Unicode 字符类代替 POSIX ,其中蕴含了 UNICODE_CASE
  • Pattern.MULTILINEm^$ 匹配行的开头和结尾,而不是整个输入的开头和结尾
  • Pattern.UNIX_LINESd :在多行模式中匹配 ^$ 时,只有 \n 被识别成行终止符
  • Pattern.DOTALLs :当使用这个标志时,. 符号匹配所有字符,包括行终止符
  • Pattern.COMMENTSx :空白字符和注释(从 # 到行末尾)将被忽略
  • 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 ,再使用 startend 方法来查找匹配的内容,或使用不带引元的 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 |

posted @ 2022-04-24 21:18  流星<。)#)))≦  阅读(66)  评论(0编辑  收藏  举报