Java-9_1-输入输出处理
本篇将介绍如何通过输入输出流以二进制流的形式操作文件、网络数据。
A. 字节流
在 Java API 中,我们通过 输入流读取字节,输出流处理字节输出。此外,使用 readers,writers 处理字符序列的读取写入。
需要注意的是,这里的流与处理集合类的 Stream 没有关系。
a. 流的获取
介绍三种获取流的方式:
1 | // 从文件获取流,使用 final 类 Files 的静态方法 |
b. 从输入流读取字节&向输出流写入字节
从输入流读取字节如下:
1 | InputStream in = ...; |
向输出流写入字节,如下:
1 | OutputStream out = ...; |
B. 字符流
a. 编码问题
InputStream 和 OutputStream 都是处理的字节的问题,但是实际中,我们基本都是处理的文本内容。因此,编码问题很重要,涉及到文件能否正确读取。
Java 使用 Unicode标准(Unicode standard) 来处理字符。每个字符或者说“code point” 为21-bit 的整数。针对这一标准,有不同的字符编码方式来处理这 21个位(character encodings)。
最常用的为 UTF-8 编码方式(encoding),它将每个 Unicode 定义的 code point 视为 1到4个 字节(bytes)。
另一种常用的为 UTF-16 编码方式,它将 Unicode 定义的 code point 分成 1到2 个16位的值。Java 的 String 就默认使用这种编码方式。此外,UTF-16 还分为 “big-endian” 和 “little-endian” 两种,区别是表示字符的2个 bytes 放置的前后顺序不同。
Note: 一些软件在 UTF-8 编码的文档头部添加一个字节顺序标志,(例如微软的 notepad)对于 UTF-8 来说,这通常是不必要的,(但Unicode 标准推荐这么做)。对于 Java 程序来说,读取这类文档需要注意。
构建字符串时,我们可以用如下形式规定编码方式:
1 | // bytes 为字符数组 |
StandardCharsets
包含所有java 虚拟机支持的8中编码格式。
b. 读取、写入字符
不同于 InputStream 和 OutputStream 处理 字节 对象。我们可以利用 Reader, Writer, 以及Files, String 等的一些方法进行直接的 字符 的处理,需要注意的是,处理字符时,通常要传入使用的编码格式。
Reader
读取内容,我们可以利用 Reader 对象来实现,通过向 Reder 对象中传入 输入流对象(InputStream),并指明编码方式,实现内容的正确读取。如下:
1 | InputStream inStream = ...; |
更方便的,我们可以直接从 文件中读取内容到 程序中,可以使用如下几种方式:
1 | // 输入到字符串 |
需要注意的是,由于读取文件可能出现 IOException, 因此需要使用 try … catch 语句包裹读取文件的代码。
如果我们想将 String 切割为 单个元素,可以利用 Scanner 读取数字和单词,例子如下:
1 | Scanner in = new Scanner(path, "UTF-8"); |
如果不是从文件中读取,我们可以利用 BufferedReader
来包裹 InputStream
及 Reader
, 来实现字符的成块处理以提升效率。例子如下:
1 | try(BufferedReader reader |
Writer
实现文本的输出,使用 Writer。我们可以通过如下手段 构造 Writer 对象。
1 | // 利用已有的输出流 构造 writer |
我们也可以利用 PrinterWriter 来包裹 Writer 对象,从而可以利用 PrinterWriter 的 print()
等方法。
1 | PrintWriter out = new PrintWriter(Files.newBufferedWriter(path, charset)); |
Note: 由于历史原因, System.out
是 PrintStream
的一个实例,而不是 PrintWriter
的实例。
此外,类似 Reader,我们也可以直接向文件输出内容,使用 Files 的静态方法。
1 | String content = ...; |
C. 其他形式的读写&注意事项
a.读取二进制数据
推荐使用 DataInput
和 DataOutput
接口来读取和输出纯数值类型的数据(基本数据类型的)。因为这些数据具有固定的字节长度,使用 DataInput/DataOutput
进行读写效率更高。
他们的主要方法有:byte readByte()
, int readUnsignedByte()
, int readInt()
…
可以使用 DataInputStream/DataOutputStream
来包裹 stream
类型的数据
b. 随机读写
RandomAccessFile
类让我们在文件的任意位置处 读取或者写入 数据。通过 new 方法生成一个 该类的对象,例子如下:
1 | RandomAccessFile file = new RandomAccessFile(path.toString(), "rw"); |
传入构造函数的第二个参数表明是只允许读(使用 “r”),还是同时允许读写。
random access 的文件拥有一个 文件指针(file pointer),来标记 读写的位置。 可以通过向 seek()
方法传入一个在 0 和文档长度之间的 long 型整数来设定 文件指针的位置。getFilePointer
可以返回当前的文件指针的位置。
此外,RandomAccessFile
类实现了 DataInput
和 DataOutput
两个接口,因此可以使用这两个接口中的方法。
另一种随机读写的思路是通过将文件数据载入到内存中进行读写,以实现更加高效的操作。它的实现利用FileChannel
类来打开文件,然后通过 FileChannel
的实例方法 map()
将数据映射到 ByteBuffer
(继承了 Buffer
类) 等对象的实例中(应该也就是内存中),再利用 buffer 对象进行读写(get
, put
方法)。
c. 文件锁
当我们有并行的程序处理同一个文件时,可能发生修改冲突而导致文件损坏。这是,我们需要使用文件锁(File locks)来保证文件的安全处理。
为文件加锁的流程如下:
1 | FileChannel channel = FileChannel.open(path); |
类似打开文件,写入文件,推荐使用 try 方法来处理加锁过程。
D. 路径、文件与目录
a. Path 对象
我们可以使用 Paths 类的静态方法 类构建一个 Path 对象,如下:
1 | // 自动使用系统的路径分隔符来构建路径,UNIX - '/', Windows- '\' |
针对路径 Path,有很多的实例方法,例如:
resolve()
, 向路径尾部追加路径,resolveSibling()
替换最尾部的一级路径;relativize()
类似一个求差操作;toAbsolutePath()
, 得到path 的绝对路径;getParent()
得到目路径…
此外, Path
接口 继承了 Iterable<Path>
接口,因此可以用增强型的 for 循环来 遍历 Path
中的成员元素(每一级路径)。
b. 创建文件/文件夹
利用 Path 对象,我们可以 创建文件或者 文件夹, 以及检查文件是否存在。
1 | // 只能创建最尾部的文件夹 |
此外,还可以通过 Files.createTempFile()
, Files.createTempDirectory()
方法来创建临时文件。
c. 文件的移动、复制与删除
我们可以配合使用 Path 和 Files 的静态方法来复制,移动,和删除文件。如下:
1 | // 复制 |
移动文件时要注意所移动的文件是否存在,以及移动到的地方是否有同名文件。
我们可以使用标准的文件操作 options 来规定文件移动时的行为,例如:
1 | // atomic 模式移动,如果移动失败,保留文件在原地 |
这些标准行为参数 在 枚举类 StandardOpenOption
及 StandardCopyOption
中。
d. 遍历文件夹条目
我们可以使用下述方法对文件夹中的内容进行遍历:
1 | // 只列path 指定路径的同一级文件和文件夹 |
需要注意的是,文件夹的读取涉及到系统资源的使用,因此,通常使用 try 方法进行包裹,来保证资源的顺利关闭。
此外,如果需要利用 文件夹的遍历方法进行文件的删除,上述 walk()
方法无法使用,需要使用 FileVisitor
接口的 walkFileTree()
方法。(具体例子略。需要重写 FileVisitor
接口中的一些方法)
此外,Java 还提供了方法来直接遍历压缩文件 - zip file。例子如下:
1 | // zipname 为压缩文件的文件名 |
E. HTTP 连接
在之前的例子中,我们看到可以使用 URL 来获取输入流,但是,如果需要关于网络传输的更多信息,URL 类通常难以胜任。实际上,Java 中的 URLConnection 类在 Http 协议广泛应用之前就已经出现,因此,它对 网络协议的很多支持显得非常笨重。
在 Java 9 中,java 中新加入了 HttpClient
类(在 java 9中, 该类有点类似公测状态,到 Java 11中,已经基本成熟,该类位于 Java.net.http 包中)来提共对于 HTTP/2 协议更好的支持。
a. 使用 URLConnection 完成网络交互
我们逐步介绍使用 URLConnection 进行 网络 request 与 response 读取的步骤。
1- 获取一个 URL 对象。
1 | URLConnection connection = url.openConnection(); |
对于一个 HTTP 的 URL,返回的对象实际上是 HttpURLConnection
的实例。
2- 如果需要,设置 request 的参数。
1 | connection.setRequestProperty("Accept-charset", "UTF-8, ISO-8859-1"); |
3- 将数据发送至 服务器。
1 | connection.setDoOutput(true); |
4- 如果没有调用 getOutputStream 而想要读取 response 的头。
1 | connection.connect(); |
5- 读取 response。
1 | try(InputStream in = connection.getInputStream()){ |
b. HTTP Client API
从 Java 9 开始,java提供 了 HTTP client 来更加方便的处理网络相关的操作。下面介绍 HttpClient 类的使用方法。
- 新建 HttpClient 的对象。
1 | HttpClient client = HttpClient.newHttpClient(); |
对于 builder 方法,首先调用方法对 待构建的对象的 特定属性 进行定制化处理,最后调用 build()
方法完成构造。
- 新建 Request。
1 | // Get request |
uri 是 uniform resource identifier 的简称,对于 Http,它与 URL 相同。 在 java 中,URL可以用于生成连接,而 uri 只规定了语法格式。
在 POST 中,我们传入了一个 post 的内容,该内容可以是 很多格式(一般的为 JSON 格式),因此,在参数中,上面长串的方法 用于指明具体的格式。(例子中为 string 格式)
- 处理 response。
发送请求时,我们需要告知 client 如何处理 reponse。尤其地,要指明 response 的 body 以格式进行处理。这里,我们将 reponse body 以 String 格式进行处理,代码如下:
1 | HTTPResponse<String> response |