美文网首页
[Java Tutorials] 05 | Java Basic

[Java Tutorials] 05 | Java Basic

作者: 夏海峰 | 来源:发表于2018-12-25 11:48 被阅读5次

Basic I/O 基本输入输出

本节包含了Java平台所提供的用于基础I/O的类。首先介绍的是I/O流,一个非常强大的思想用于简化I/O操作。其次将研究序列化,序列化用于把整个对象写入到流中,并还能从流中将其再读取回来。最后将研究文件I/O 和 文件系统操作,包括随机读写文件。这些类主要分布在 java.io包、java.nio.file包中。

Basic I/O 学习路径

I/O Stream 流

I/O流代表着一个输入源或输出目的地。“流”代表着多种类型的“源”和“目的地”,包括磁盘文件、设备、其它程序或者内存等。“流”支持多种不同类型的数据,包括简单的字符数据、原始类型数据、本地化的字符集数据、对象。有些“流”只是简单地传递数据,另一些“流”除了传递数据外还可以实现数据的操作和转换。

不管“流”在内部是如何工作的,所有的“流”都代表着相同的简单的编程模型。“流”就是一连串的数据。程序使用input stream 读取“源”中的数据;程序使用 output stream 向“目的地”写入数据。如下图示:

我们可以使用“流”来处理数据,从最基本的原始数据类型到高级的对象数据。上图中的“源”和“目的地”,可以是硬盘文件、另一个程序、外部设备、网络socket 或 array阵列。

Byte Stream 字节流

程序使用字节流执行8位字节的输入与输出。所有的字节流class 都继承自 InputStream 和 OutputStream。有很多的字节流的类,下面我们演示一下字节流的工作方式,把注意力放在基于字节流的文件I/O(FileInputStream / FileOutputStream),其它的字节流的使用方式几乎致,举一反三即可。下面示例,我们将使用字节流来复制 demo.txt 文件:

// ByteCopy.java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteCopy {
    public static void main(String[] args) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;
        try {
            // 创建“源”对象
            in = new FileInputStream("demo.txt");
            // 创建“目的地”对象
            out = new FileOutputStream("demo_copy.txt");
            // 读文件
            int c;
            while ((c = in.read()) != -1) {
                // 写文件
                out.write(c);
            }
        } finally {
            // 关闭“流”
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

源码解读:CopyFile.java 将花费一定的时间来完成文件的读取与写入。在 while 循环中,一次一个字节地读写文件。其工作方式如下图示:

流的工作方式

必须注意:当“流”不再被使用时,一定要关闭掉它们,即使是发生了意外的异常。把“流”的关闭放在finally中,以保证“流”总是能够被关闭,以减少资源浪费。上述代码中,当无法打开文件时就会导致异常的发生,“源”流和“目的地”流就是null,不能调用.close()方法。

上述代码看上去是一个正常的程序,但事实上我们应该尽量避免这么写,因为demo.txt文件中可以包含字符数据。最佳的方式是使用“字符流”来实现文件的读写。“字节流”大多数时候适用于最原始的I/O操作。所有的流类型,都是基于“字节流”而构建的。

Character Stream 字符流

Java平台使用Unicode字符集规则。字符流,使用内部格式或者本地字符集来转换数据。本地字符集通常是8位的ASCII超集。所有的字符流的类,都派生自 Reader / Writer。也有专门用于文件的字符流,如 FileReader / FileWriter。示例如下:

// CharacterCopy.java

import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class CharacterCopy {
    public static void main(String[] args) throws IOException {
        FileReader in = null;
        FileWriter out = null;
        try {
            in = new FileReader("demo.txt");
            out = new FileWriter("demo_copy_2.txt");
            int c;
            while((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if(in != null) {
                in.close();
            }
            if(out != null) {
                out.close();
            }
        }
    }
}

FileReader/FileWriter 与 FileInputStream/FileOutputStream 看似都可以实现文件的读写,它们的区别在于,前者每次读写16位,后者每次仅读取8位。

基于行的 I/O
对于文本文件来讲,所谓的“行”就是使用了换行符(\n)或回车符(\r)的字符串。文件的读写操作,我们可以基于“行”来读写,示例如下:

// LinesCopy.java

import java.io.FileReader;
import java.io.FileWriter;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.IOException;

public class LinesCopy {
    public static void main(String[] args) throws IOException {
        BufferedReader in = null;
        PrintWriter out = null;
        try {
            // 创建“源”对象
            in = new BufferedReader(new FileReader("demo.txt"));
            // 创建“目的地”对象
            out = new PrintWriter(new FileWriter("demo_copy_3.txt"));
            // 读一行数据
            String line;
            while((line = in.readLine()) != null) {
                // 写一行数据
                out.println(line);
            }
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }
    }
}

.readLine() 每次读取文件中的一行数据,.println() 每次向目的地处写入一行数据。事实上,对文本文件的读写操作,有很多种解决方案,更多了解参见 Scanning & Formatting。

Buffered Stream 缓冲流

没有使用缓冲流的文件读写,是直接地依靠操作系统来处理的,这会导致一些麻烦,进而影响流操作的效率。因为这种没有使用缓冲流的操作,会频繁地触发硬盘访问、网络活动或者一些其它开销较大的操作。

为了减少这种开销,Java平台提供了缓冲I/O流。缓冲输入流可以从内存缓冲区中读取数据,缓冲输出流可以向内存缓冲区中写入数据。对一个程序来讲,我们可以把非缓冲流转化成缓冲流,做法是用缓冲流构造器包裹非缓冲流对象,示例如下:

BufferedReader in = new BufferedReader(new FileReader("demo.txt"));
BufferedWriter out = new BufferedWriter(new FileWriter("demo_copy.txt"));

用四个缓冲流的Java类,分别是 BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter。

缓冲流的 flush()
当我们在使用缓冲流来操作I/O时,应该及时地把缓冲区中数据flush出来,而不是等待缓冲区被装满,这是非常重要的。这就是所谓的“flush the buffer”。有些缓冲流的java类,支持自动flush。如果想手动flush,只需调用缓冲流对象的.flush() 方法即可。这个 .flush()对所有的缓冲输出流都有用,但对非缓冲流是没有用的。

Scanning and Formatting

编程I/O通常要在普通数据和接近于人类习惯的数据之间进行相互转化。Java平台分别提供了 Scanner 和 Formatting 来处理这个任务。

Scanning 有两个作用,其一是使用指定的分割符把被输入数据分解开来;其二是根据指定的格式对数据进行输出。

// ScanText.java

import java.io.*;
import java.util.Scanner;

public class ScanText {
    public static void main(String[] args) throws IOException {
        Scanner scan = null;
        try {
            scan = new Scanner(new BufferedReader(new FileReader("demo.txt")));
            while (scan.hasNext()) {
                System.out.println(scan.next());
            }
        } finally {
            if (scan != null) {
                scan.close();
            }
        }
    }
}

Formatting 使用格式化字符串对参数进行格式化。格式化字符有很多,常用的有:

%d 用于把整型数据格式化为小数形式
%f 用于把浮点数格式化成小数形式
%n 输出一个换行符
%x 把整型数据格式化成十六进制数
%s 把数值格式化为一个字符串。等等。

public class FormatTest {
    public static void main(String[] args) {
        int i = 2;
        double r = Math.sqrt(i);
        
        // 格式化输出
        System.out.format("The square root of %d is %f.%n", i, r);
    }
}

命令行环境下的 I/O

有时候程序需要在命令行环境下运行,因此Java平台提供了 Standard Stream 和 Console 支持Java程序在命令行环境下运行。如下示例,在命令行环境下修改密码:

import java.io.Console;
import java.util.Arrays;
import java.io.IOException;

public class Password {
    public static void main(String[] args) {
        Console c = System.console();
        if (c == null) {
            System.err.println("No console");
            System.exit(1);
        }
        
        String login = c.readLine("Enter your login:");
        char[] oldPasswd = c.readPassword("Enter your old password");
        
        if (verify(login, oldPasswd)) {
            boolean noMatch;
            do {
                char[] newPasswd1 = c.readPassword("Enter your new password:");
                char[] newPasswd2 = c.readPassword("Enter new password again:");
                noMatch = !Arrays.equals(newPasswd1, newPasswd2);
                if (noMatch) {
                    c.format("password don't match. Try again. %n");
                } else {
                    change(login, newPasswd1);
                    c.format("password for %s changed.%n", login);
                }
                Arrays.fill(newPasswd1, ' ');
                Arrays.fill(newPasswd2, ' ');
            } while(noMatch);
        }
        Arrays.fill(oldPasswd, ' ');
    }
    static boolean verify(String login, char[] passwd) {
        // verify
        return true;
    }
    static void change(String login, char[] passwd) {
        // change
    }
}

Data Streams 原始类型数据流

数据流支持原始数据类型的二进制I/O,比如布尔、字符、字节、整型、浮点类型等。所有的数据流都实现了 DataInput接口 / DataOutput接口 二者之一。

原始类型数据的I/O操作

Object Streams 对象流

对象流支持对象的I/O读写操作。大多数情况下,标准的Java类都支持对象序列化。对象流所使用到的类有 ObjectInputStream / ObjectOutputStream ,它们实现了 ObjectInput / ObjectOutput 接口。

对象的I/O操作

文件 I/O 操作

java.nio.file包 / java.nio.file.attribute 提供了大量的API 以支持文件的I/O操作,访问默认的文件系统。尽管这里有很多类,但是我们只需要学习其中几个即可,这些API是非常直观的简单的。

什么是文件系统?什么是 Path 路径?

文件系统基于硬件驱动,用于存储和组织文件。大多数的文件系统都是使用树结构(层级结构)的方式来存储文件的,最顶端的是根节点。路径有相对路径和绝对路径之分。如下图示:

文件系统

Path类 和 路径操作

Path类是文件系统路径的一种可编程式表示。一个Path对象包含了文件名、目录列表,用于对文件进行检测、定位和操作。

创建一个Path对象:

Path p1 = Paths.get("/tmp/foo");

从Path对象中读取系列信息:

System.out.format("toString: %s%n", p1.toString());
System.out.format("fileName: %s%n", p1.getFileName());
System.out.format("root: %s%n", p1.getRoot());

对路径进行转化:

System.out.format("%s%n", p1.toUri());

合并两个路径:

Paths.get("foo").resolve("/home/joe");

对两个路径进行比较:

if (p1.equals(p2)) {
    // equal
} else if (p1.startWith(p3)) {
    // startWith
} else if (p1.endsWith(p4)) {
    // endsWith
}

文件操作

Files类是 java.nio.file包中的另一个主要的入口。这个类提供了一系列的静态方法,用于对文件和目录进行读、写和操作。Files类中的方法,是基于Path对象来工作的。在开始学习本节之前,你应该先熟悉以下几个通用的概念:

什么是释放系统资源?什么是捕获异常?什么是可变参数?什么是原子操作?什么是方法的链式调用?

What is a Glob ? 在Files 类中有两个方法支持 glob 参数,你可以使用 glob 语法去指定模式匹配行为。glob模式是以字符串的形式被指定的,用于匹配其它字符串,比如目录、文件名等。glob语法遵守以下规则:

glob 语法规则

对目录和文件进行若干检测

一个Path实例就代表着一个文件或目录。那么该如何判断指定文件是否存在呢?该文件是可读的?可写的?还是可执行的?

验证文件或目录的存在性

Files.exists(path);
Files.notExists(path);

检测文件的可访问性

Files.isRegularFile(path)
Files.isReadable(path)
Files.isExecutable(path);

检测两个Path实例是否指定同一个文件

Files.isSameFile(path1, path2);

删除文件或目录

你可以删除文件、目录或链接。对于符号链接来讲,你删除的只是链接本身,而不会删除源文件。对于目录来讲,被删除的目录必须是空目标,否则删除失败。Files类提供了两个用于删除任务的方法。

Files.delete(path);     // 如果路径不存在,会抛出异常
Files.deleteIfExists(path);   // 如果路径不存在,不会抛出异常

复制文件或目录

使用 .copy() 方法可以实现文件或目录的复制。如果目标源已经存在,则复制失败,除非指定了 REPLACE_EXISTING 参数选项。

import static java.nio.file.StandardCopyOption.*;

File.copy(source, target, REPLACE_EXISTING);

移动文件或目录

使用 .move() 方法可以移动文件或目录。如果目标已经存在,则移动失败,除非你指定了 REPLACE_EXISTING 参数选项。

import static java.nio.file.StandardCopyOption.*;

Files.move(source, target, REPLACE_EXISTING);

对文件和目录的存储属性(元数据)进行管理

元数据metadata的定义:即数据的数据属性。在文件系统中,数据是存储在文件和目录中的,所谓“元数据”指的就是每个文件对象或目录对象的相关属性,比如是普通文件还是目录?文件大小?创建时间?文件所有者?文件所有组?访问权限等?

在文件系统中,元数据通常指向文件属性。Files类提供了系列方法,可用于获取文件的属性,或者设置文件的属性。(详情参见 Files类 api 文档)

文件的读、写、创建

本小节详细讨论文件的创建、打开与读写。有大量的api方法可供你选择使用。如下图表可以帮助你搞懂这些api的复杂度,由简至繁。

File I/O API

上图中最左边的实用的方法,如 readAllBytes() / readAllLines() / write(),常用于简单的通用的场景。右侧的方法用于循环遍历文本流或文本行,如 newBufferedReader / newBufferedWriter / newInputStream / newOutputStream。 newByteChannel() 用于处理 ByteChannels / ByteBuffers / SeekableByteChannels。 最右侧的方法使用 FileChannel 满足高级应用程序需求,比如文件加锁、内存映射I/O等。

值得注意的是,在使用这些方法创建文件时,允许你为文件指定一系列可选的初始化参数。比如说,在一个支持 POSIX 的文件系统上(如 UNIX),你可以在创建文件时为其指定文件所有者、所属组、文件权限等。

本章节你应该学会以下内容:

  • OpenOptions 参数:许多方法都支持可选的 OpenOptions 参数,当不指定这个参数时,这些方法将会采用默认行为。
  • 针对小型文件的通用方法:如 readAllBytes() / readAllLines() / write() 等。
  • 针对文本文件的缓冲流I/O 方法:java.nio.file 包支持 I/O 通道,可以从缓冲区中转移数据,从而绕开那些可能阻塞I/O流的 layers 。
  • 用于非缓冲流的方法。
  • 用于通道和 ByteBuffers 的方法。
  • 用于创建普通文件、临时文件的方法。

随机存取文件

随机存取文件允许我们无序地、随机地访问文件内容。也就是说你可以打开文件,寻找一个特定的位置,从中读取文件内容,或者向文件中写入内容。

这个功能可能来自 SeekableByteChannel 接口,该接口继承自通道I/O。这些方法使用能够设置或查询一个具体的位置,进而读写数据。这个 API 包含以下简单易用的方法:

  • position 返回通道的当前位置。
  • position() 设置通道的位置。
  • read() 从通道中读取字节数据至缓冲区。
  • truncate() 截取文件或者另一个实体,并连接到通道

创建目标、读取目录

前面章节中所讨论的方法,比如 delete() ,都是基于文件、链接和目录来工作的。但是我们该如何列举出文件系统中最顶级目录下所有的目录呢?该如何列表出目录中所有的内容呢?该如何创建一个目录呢?

本小节将讨论以下几个功能性的话题:

  • 列举出文件系统根目录下的所有目录。
  • 创建一个目录。
  • 创建一个临时目录。
  • 列举出目录下的内容。
  • 使用 Glob 过滤一个目录中的内容。
  • 自定义目录过滤器。

链接——符号化链接,或者其它类型的链接

前面已经提到过,java.nio.file包中,Path类尤其重要。每一个Path方法,无论用于检测你想做什么的path方法,还是为你提供参数选项的path方法,都能确保你在遇到符号化链接时能够配置其行为。在本章节之前,我们已经讨论过了符号化链接、软链接。然而还有一些文件系统还支持硬链接,硬链接有着更多的限制,限制如下:

  • 硬链接的源文件必须存在。
  • 硬链接一般不能用于目录。
  • 硬链接不允许跨分区,因此硬链接不可以跨文件系统而存在。
  • 硬链接看上去、行为上表现像是普通链接,因此同样难以查找。
  • 硬链接的作用和源文件的作用一致,它们有着相同的权限、时间戳等,所有的文件属性都完全一样。

鉴于硬链接的限制条件更多,所以硬链接的使用场合比符号化链接更少。但是,Path类同样可以无缝地基于硬链接而工作。主要学习内容有以下几个点:

  • 如何创建一个符号化链接?
  • 如何创建一个硬链接?
  • 如何检测一个链接是符号化链接?
  • 如何查找链接所对应的源文件?

遍历文件树(文件系统)

你的应用是否需要递归地访问文件树中的所有文件?假如你需要删除所有的 .class 文件,假如你想知道过去一年哪些文件被访问过,等等。这些需要,可以使用 FileVisitor 接口来实现。本小节要学习的内容有:

  • 学习 FileVisitor 接口。
  • 使用 walkFileTree() 方法遍历文件树。
  • 创建一个 FileVisitor 时需要注意哪些事项?
  • 工作流控制

文件查找

如果你使用过 shell 脚本,你或许用过模式匹配来定位文件。事实上,模式匹配的应用十分广泛。模式匹配使用特殊的字符来创建一个模式,然后用文件名和这个模式进行比较。举个例子,在大多数的shell脚本中,星号 * 可以匹配任意一个字符。

java.nio.file包提供了编程级别的特性支持,每个文件系统都实现了模式匹配,并提供了 PathMatcher 类。你可以使用 FileSystem类的 getPathMatcher() 方法(递归)检索一个文件系统。

监听目录的变化

你是否遇到过这样的场景:当你修改了目录中某个文件时,系统会弹出一个确认框来提示你是否需要重新加载?这个功能,就是文件变化通知。程序必须能够检测到文件系统中相关目录的变化情况。一种实现文案是,轮询整个文件系统以查找变化,但是这种实现文案的效率不高;并且当打开的文件成千上百时,就不适合这么做了。

java.nio.file包提供了“文件变化通知功能”相关的类,它使得我们能够对一个或多个目录注册监听服务。在注册目录监听服务时,你可以选择使用哪种类型的目录变化,是文件新建?是文件删除?还是文件修改?等等。当监听服务检测到你指定的目录变化类型发生时,它会被转发到注册进程,注册进程会调用线程(池)来响应这个目录变化的事件。在本小节要学习的内容如下:

  • 监听服务概述。
  • 试用监听服务。
  • 创建一个监听服务,并注册相关事件。
  • 进程事件。
  • 检索文件名。
  • 监听服务最适宜的一些使用场景?

另一些有用的方法

有一些方法并不适合上述各章节的使用场景,但这些方法同样非常有用。本小节介绍几个这样的方法:

  • 判断文件的 MIME 类型: Files.probeContentType()
  • 获取默认的文件系统: FileSystems.getDefault()
  • 路径字符串分割符: FileSystems.getDefault().getSeparator()
  • 文件系统的文件存储: FileSystems.getDefault().getFileStores()

Legacy File I/O Code

在JavaSE 7 发布之前,java.io.File 用于机器系统的文件 I/O 操作,但是确实存在着一些缺陷。

自JavaSE 7 发布之后,关于文件I/O的实现完全重构了。如果你想使用 java.nio.file 包所提供了富功能来替换 JavaSE 7 之前的功能,那么你必须重写你的文件I/O代码。下图是 java.io.file 和 java.nio.file 之间的简单对比,但需要注意的是,它们之间的变化并不是一一对应的,并不是把旧API 替换成新API 就能完成代码的迁移。

java.io 对比 java.nio

本章小结

java.io包中有着很多有用的方法,可以帮助你在程序中实现数据的读与写。它们中大多数的类都实现了流的顺序访问。流的顺序访问,可以被划分成两大模块,一者是字节流的读写,二者是Unicode字符流的读写。每一个顺序访问的流,都有其特定的功能,比如读文件、写文件、数据过滤 或者 对象序列化等。

java.nio.file包提供了更多关于文件I/O、文件系统I/O的扩展功能。java.nio.file包是一个综合性的API集,其核心关键点有如下几个:

  • Path类,用于操作和管理文件系统中的路径。
  • Files类,用于文件操作,比如移动、复制、删除等;还可以用于获取和设置文件的属性。
  • FileSystem类,用于管理文件系统的信息。

本章END 2018年12月27日

相关文章

网友评论

      本文标题:[Java Tutorials] 05 | Java Basic

      本文链接:https://www.haomeiwen.com/subject/zizxlqtx.html