缓冲流
原文:
docs.oracle.com/javase/tutorial/essential/io/buffers.html
到目前为止,我们看到的大多数示例都使用非缓冲的 I/O。这意味着每个读取或写入请求都直接由底层操作系统处理。这可能会使程序效率大大降低,因为每个这样的请求通常会触发磁盘访问、网络活动或其他相对昂贵的操作。
为了减少这种开销,Java 平台实现了缓冲 I/O 流。缓冲输入流从称为缓冲区的内存区域读取数据;只有当缓冲区为空时才调用本机输入 API。类似地,缓冲输出流将数据写入缓冲区,只有当缓冲区满时才调用本机输出 API。
一个程序可以使用我们已经多次使用的包装习惯将非缓冲流转换为缓冲流,其中非缓冲流对象传递给缓冲流类的构造函数。以下是您可能如何修改CopyCharacters示例中的构造函数调用以使用缓冲 I/O 的方式:
inputStream = new BufferedReader(new FileReader("xanadu.txt")); outputStream = new BufferedWriter(new FileWriter("characteroutput.txt"));
有四个缓冲流类用于包装非缓冲流:BufferedInputStream和BufferedOutputStream创建缓冲字节流,而BufferedReader和BufferedWriter创建缓冲字符流。
刷新缓冲流
在关键点写出缓冲区而不等待其填满通常是有意义的。这被称为刷新缓冲区。
一些缓冲输出类支持自动刷新,可以通过可选的构造函数参数指定。启用自动刷新时,某些关键事件会导致缓冲区被刷新。例如,一个自动刷新的PrintWriter对象会在每次调用println或format时刷新缓冲区。查看格式化以获取更多关于这些方法的信息。
要手动刷新流,请调用其flush方法。flush方法对任何输出流都有效,但除非流是缓冲的,否则不会产生任何效果。
扫描和格式化
原文:
docs.oracle.com/javase/tutorial/essential/io/scanfor.html
编程 I/O 通常涉及将数据翻译成人类喜欢处理的整洁格式。为了帮助您完成这些任务,Java 平台提供了两个 API。扫描器 API 将输入分解为与数据位相关联的单个标记。格式化 API 将数据组装成格式整齐、易读的形式。
扫描
原文:
docs.oracle.com/javase/tutorial/essential/io/scanning.html
类型为Scanner的对象对于将格式化输入拆分为标记并根据其数据类型翻译单个标记非常有用。
将输入分解为标记
默认情况下,扫描器使用空白字符来分隔标记。(空白字符包括空格、制表符和行终止符。有关完整列表,请参考Character.isWhitespace的文档。)为了了解扫描的工作原理,让我们看看ScanXan,一个程序,它读取xanadu.txt中的单词并将它们逐行打印出来。
import java.io.*; import java.util.Scanner; public class ScanXan { public static void main(String[] args) throws IOException { Scanner s = null; try { s = new Scanner(new BufferedReader(new FileReader("xanadu.txt"))); while (s.hasNext()) { System.out.println(s.next()); } } finally { if (s != null) { s.close(); } } } }
请注意,当ScanXan完成对扫描器对象的操作时,会调用Scanner的close方法。即使扫描器不是一个流,你也需要关闭它以表示你已经完成了对其底层流的操作。
ScanXan的输出如下所示:
In Xanadu did Kubla Khan A stately pleasure-dome ...
要使用不同的标记分隔符,调用useDelimiter(),指定一个正则表达式。例如,假设您希望标记分隔符是逗号,后面可以跟随空白。您可以调用,
s.useDelimiter(",\\s*");
翻译单个标记
ScanXan示例将所有输入标记视为简单的String值。Scanner还支持所有 Java 语言的基本类型(除了char),以及BigInteger和BigDecimal。此外,数值可以使用千位分隔符。因此,在US区域设置中,Scanner可以正确地将字符串"32,767"读取为整数值。
我们必须提及区域设置,因为千位分隔符和小数符是与区域设置相关的。因此,如果我们没有指定扫描器应该使用US区域设置,下面的示例在所有区域设置中都不会正确工作。这通常不是您需要担心的事情,因为您的输入数据通常来自与您相同区域设置的源。但是,这个示例是 Java 教程的一部分,会分发到世界各地。
ScanSum示例读取一组double值并将它们相加。以下是源代码:
import java.io.FileReader; import java.io.BufferedReader; import java.io.IOException; import java.util.Scanner; import java.util.Locale; public class ScanSum { public static void main(String[] args) throws IOException { Scanner s = null; double sum = 0; try { s = new Scanner(new BufferedReader(new FileReader("usnumbers.txt"))); s.useLocale(Locale.US); while (s.hasNext()) { if (s.hasNextDouble()) { sum += s.nextDouble(); } else { s.next(); } } } finally { s.close(); } System.out.println(sum); } }
这是示例输入文件,usnumbers.txt
8.5 32,767 3.14159 1,000,000.1
输出字符串为"1032778.74159"。在某些区域设置中,句号可能是不同的字符,因为System.out是一个PrintStream对象,该类不提供覆盖默认区域设置的方法。我们可以为整个程序覆盖区域设置,或者我们可以使用格式化,如下一主题中所述,格式化。
格式化
原文:
docs.oracle.com/javase/tutorial/essential/io/formatting.html
实现格式化的流对象是PrintWriter(字符流类)或PrintStream(字节流类)的实例。
**注意:**你可能需要的唯一PrintStream对象是System.out和System.err。(有关这些对象的更多信息,请参阅从命令行进行 I/O。)当您需要创建格式化输出流时,请实例化PrintWriter,而不是PrintStream。
像所有字节和字符流对象一样,PrintStream和PrintWriter的实例实现了一组用于简单字节和字符输出的标准write方法。此外,PrintStream和PrintWriter都实现了相同的一组方法,用于将内部数据转换为格式化输出。提供了两个级别的格式化:
print和println以标准方式格式化单个值。format根据格式字符串几乎可以格式化任意数量的值,具有许多精确格式化选项。
print和println方法
调用print或println在使用适当的toString方法转换值后输出单个值。我们可以在Root示例中看到这一点:
public class Root { public static void main(String[] args) { int i = 2; double r = Math.sqrt(i); System.out.print("The square root of "); System.out.print(i); System.out.print(" is "); System.out.print(r); System.out.println("."); i = 5; r = Math.sqrt(i); System.out.println("The square root of " + i + " is " + r + "."); } }
这是Root的输出:
The square root of 2 is 1.4142135623730951. The square root of 5 is 2.23606797749979.
i和r变量被格式化两次:第一次使用print重载中的代码,第二次是由 Java 编译器自动生成的转换代码,也利用了toString。您可以以这种方式格式化任何值,但对结果的控制不多。
format方法
format方法根据格式字符串格式化多个参数。格式字符串由静态文本与格式说明符嵌入在一起组成;除了格式说明符外,格式字符串不会改变输出。
格式字符串支持许多功能。在本教程中,我们只涵盖了一些基础知识。有关完整描述,请参阅 API 规范中的格式字符串语法。
Root2示例使用单个format调用格式化两个值:
public class Root2 { 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); } }
这里是输出:
The square root of 2 is 1.414214.
像这个示例中使用的三个一样,所有格式说明符都以%开头,并以指定正在生成的格式化输出类型的 1 个或 2 个字符转换结尾。这里使用的三个转换是:
d将整数值格式化为十进制值。f将浮点值格式化为十进制值。n输出特定于平台的换行符。
这里有一些其他转换:
x将整数格式化为十六进制值。s将任何值格式化为字符串。tB格式化一个整数为本地特定的月份名称。
还有许多其他转换。
注意:
除了 %% 和 %n 之外,所有格式说明符都必须匹配一个参数。如果不匹配,就会抛出异常。
在 Java 编程语言中,\n 转义始终生成换行符(\u000A)。除非特别需要换行符,否则不要使用 \n。要获取本地平台的正确换行符,请使用 %n。
除了转换之外,格式说明符还可以包含几个额外元素,进一步定制格式化输出。这里是一个示例,Format,使用了每种可能的元素类型。
public class Format { public static void main(String[] args) { System.out.format("%f, %1$+020.10f %n", Math.PI); } }
这是输出结果:
3.141593, +00000003.1415926536
所有附加元素都是可选的。下图显示了更长格式说明符如何分解为元素。
格式说明符的元素。
元素必须按照所示顺序出现。从右边开始,可选元素包括:
- 精度。对于浮点值,这是格式化值的数学精度。对于
s和其他一般转换,这是格式化值的最大宽度;如果需要,值将被右截断。 - 宽度。格式化值的最小宽度;如果需要,将填充值。默认情况下,值左侧用空格填充。
- 标志 指定额外的格式选项。在
Format示例中,+标志指定数字应始终带有符号格式,0标志指定0为填充字符。其他标志包括-(右侧填充)和,(使用本地特定的千位分隔符格式化数字)。请注意,某些标志不能与其他标志或某些转换一起使用。 - 参数索引 允许您显式匹配指定的参数。您还可以指定
<来匹配与上一个格式说明符相同的参数。因此,示例可以这样说:System.out.format("%f, %<+020.10f %n", Math.PI);
命令行 I/O
程序通常从命令行运行并在命令行环境中与用户交互。Java 平台通过标准流和控制台两种方式支持这种交互。
标准流
标准流是许多操作系统的特性。默认情况下,它们从键盘读取输入并将输出写入显示器。它们还支持文件和程序之间的 I/O,但该功能由命令行解释器控制,而不是程序。
Java 平台支持三个标准流:标准输入,通过 System.in 访问;标准输出,通过 System.out 访问;以及标准错误,通过 System.err 访问。这些对象会自动定义,无需打开。标准输出和标准错误都用于输出;将错误输出单独处理允许用户将常规输出重定向到文件并仍能读取错误消息。有关更多信息,请参考您的命令行解释器文档。
你可能期望标准流是字符流,但出于历史原因,它们是字节流。System.out 和 System.err 被定义为 PrintStream 对象。虽然技术上是字节流,但 PrintStream 利用内部字符流对象来模拟许多字符流的特性。
相比之下,System.in 是一个没有字符流特性的字节流。要将标准输入作为字符流使用,需要将 System.in 包装在 InputStreamReader 中。
InputStreamReader cin = new InputStreamReader(System.in);
控制台
比标准流更高级的替代方案是控制台。这是一种类型为 Console 的单一预定义对象,具有标准流提供的大部分功能,以及其他功能。控制台特别适用于安全密码输入。控制台对象还通过其 reader 和 writer 方法提供真正的字符流输入和输出流。
在程序可以使用控制台之前,必须通过调用 System.console() 尝试检索控制台对象。如果控制台对象可用,则此方法将返回它。如果 System.console 返回 NULL,则不允许控制台操作,可能是因为操作系统不支持它们或者因为程序在非交互环境中启动。
控制台对象通过其readPassword方法支持安全密码输入。该方法通过两种方式帮助安全密码输入。首先,它抑制回显,因此密码不会在用户屏幕上可见。其次,readPassword返回一个字符数组,而不是一个String,因此密码可以被覆盖,一旦不再需要,即从内存中删除。
Password示例是一个用于更改用户密码的原型程序。它演示了几种Console方法。
import java.io.Console; import java.util.Arrays; import java.io.IOException; public class Password { public static void main (String args[]) throws IOException { Console c = System.console(); if (c == null) { System.err.println("No console."); System.exit(1); } String login = c.readLine("Enter your login: "); char [] oldPassword = c.readPassword("Enter your old password: "); if (verify(login, oldPassword)) { boolean noMatch; do { char [] newPassword1 = c.readPassword("Enter your new password: "); char [] newPassword2 = c.readPassword("Enter new password again: "); noMatch = ! Arrays.equals(newPassword1, newPassword2); if (noMatch) { c.format("Passwords don't match. Try again.%n"); } else { change(login, newPassword1); c.format("Password for %s changed.%n", login); } Arrays.fill(newPassword1, ' '); Arrays.fill(newPassword2, ' '); } while (noMatch); } Arrays.fill(oldPassword, ' '); } // Dummy change method. static boolean verify(String login, char[] password) { // This method always returns // true in this example. // Modify this method to verify // password according to your rules. return true; } // Dummy change method. static void change(String login, char[] password) { // Modify this method to change // password according to your rules. } }
Password类遵循以下步骤:
- 尝试检索控制台对象。如果对象不可用,则中止。
- 调用
Console.readLine提示并读取用户的登录名。 - 调用
Console.readPassword提示并读取用户的现有密码。 - 调用
verify确认用户有权限更改密码。(在这个例子中,verify是一个始终返回true的虚拟方法。) - 重复以下步骤,直到用户两次输入相同的密码:
- 两次调用
Console.readPassword提示并读取新密码。 - 如果用户两次输入相同的密码,调用
change进行更改。(同样,change是一个虚拟方法。) - 用空格覆盖两个密码。
- 用空格覆盖旧密码。
数据流
原文:
docs.oracle.com/javase/tutorial/essential/io/datastreams.html
数据流支持原始数据类型值(boolean、char、byte、short、int、long、float和double)以及String值的二进制 I/O。所有数据流都实现了DataInput接口或DataOutput接口。本节重点介绍了这些接口的最常用实现,DataInputStream和DataOutputStream。
DataStreams示例演示了通过写出一组数据记录,然后再次读取它们来演示数据流。每个记录包含与发票上的项目相关的三个值,如下表所示:
| 记录中的顺序 | 数据类型 | 数据描述 | 输出方法 | 输入方法 | 示例值 |
| 1 | double |
项目价格 | DataOutputStream.writeDouble |
DataInputStream.readDouble |
19.99 |
| 2 | int |
单位数量 | DataOutputStream.writeInt |
DataInputStream.readInt |
12 |
| 3 | String |
项目描述 | DataOutputStream.writeUTF |
DataInputStream.readUTF |
"Java T-Shirt" |
让我们来看看DataStreams中关键的代码。首先,程序定义了一些包含数据文件名称和将写入其中的数据的常量:
static final String dataFile = "invoicedata"; static final double[] prices = { 19.99, 9.99, 15.99, 3.99, 4.99 }; static final int[] units = { 12, 8, 13, 29, 50 }; static final String[] descs = { "Java T-shirt", "Java Mug", "Duke Juggling Dolls", "Java Pin", "Java Key Chain" };
然后DataStreams打开一个输出流。由于DataOutputStream只能作为现有字节流对象的包装器创建,DataStreams提供了一个带缓冲的文件输出字节流。
out = new DataOutputStream(new BufferedOutputStream( new FileOutputStream(dataFile)));
DataStreams写出记录并关闭输出流。
for (int i = 0; i < prices.length; i ++) { out.writeDouble(prices[i]); out.writeInt(units[i]); out.writeUTF(descs[i]); }
writeUTF方法以修改后的 UTF-8 形式写出String值。这是一种只需要一个字节来表示常见西方字符的可变宽度字符编码。
现在DataStreams再次读取数据。首先,它必须提供一个输入流和变量来保存输入数据。与DataOutputStream一样,DataInputStream必须作为字节流的包装器构建。
in = new DataInputStream(new BufferedInputStream(new FileInputStream(dataFile))); double price; int unit; String desc; double total = 0.0;
现在DataStreams可以读取流中的每个记录,并报告遇到的数据。
try { while (true) { price = in.readDouble(); unit = in.readInt(); desc = in.readUTF(); System.out.format("You ordered %d" + " units of %s at $%.2f%n", unit, desc, price); total += unit * price; } } catch (EOFException e) { }
请注意,DataStreams通过捕获EOFException来检测文件结束条件,而不是测试无效的返回值。所有DataInput方法的实现都使用EOFException而不是返回值。
还要注意,DataStreams 中的每个专门的 write 都与相应的专门的 read 完全匹配。程序员需要确保输出类型和输入类型以这种方式匹配:输入流由简单的二进制数据组成,没有任何内容指示个别值的类型,或者它们在流中的位置。
DataStreams 使用了一种非常糟糕的编程技术:它使用浮点数来表示货币值。一般来说,浮点数对于精确值是不好的。对于十进制小数来说尤其糟糕,因为常见的值(比如0.1)没有二进制表示。
用于货币值的正确类型是java.math.BigDecimal。不幸的是,BigDecimal是一个对象类型,所以它不能与数据流一起使用。然而,BigDecimal 可以 与对象流一起使用,这将在下一节中介绍。
对象流
原文:
docs.oracle.com/javase/tutorial/essential/io/objectstreams.html
就像数据流支持原始数据类型的 I/O 一样,对象流支持对象的 I/O。大多数标准类支持其对象的序列化,但并非所有类都支持。那些实现了标记接口Serializable的类支持序列化。
对象流类是ObjectInputStream和ObjectOutputStream。这些类实现了ObjectInput和ObjectOutput,它们是DataInput和DataOutput的子接口。这意味着在对象流中也实现了数据流中涵盖的所有原始数据 I/O 方法。因此,对象流可以包含原始值和对象值的混合。ObjectStreams示例说明了这一点。ObjectStreams创建了与DataStreams相同的应用程序,但有一些变化。首先,价格现在是BigDecimal对象,以更好地表示分数值。其次,一个Calendar对象被写入数据文件,表示发票日期。
如果readObject()没有返回预期的对象类型,尝试将其强制转换为正确类型可能会抛出ClassNotFoundException。在这个简单的例子中,这种情况不会发生,所以我们不尝试捕获异常。相反,我们通过将ClassNotFoundException添加到main方法的throws子句中来通知编译器,我们已经意识到了这个问题。
复杂对象的输出和输入
writeObject和readObject方法使用起来很简单,但它们包含一些非常复杂的对象管理逻辑。对于像日历这样只封装原始值的类来说,这并不重要。但是许多对象包含对其他对象的引用。如果readObject要从流中重建一个对象,它必须能够重建原始对象引用的所有对象。这些额外的对象可能有它们自己的引用,依此类推。在这种情况下,writeObject遍历整个对象引用网络,并将该网络中的所有对象写入流中。因此,一次writeObject调用可能导致大量对象被写入流中。
这在下图中有所展示,其中调用writeObject来写入一个名为a的单一对象。这个对象包含对对象b和c的引用,而b包含对d和e的引用。调用writeobject(a)不仅写入a,还写入了重建a所需的所有对象,因此这个网络中的其他四个对象也被写入了。当a被readObject读回时,其他四个对象也被读回,并且所有原始对象引用都被保留。
多个被引用对象的 I/O
你可能会想知道,如果同一流上的两个对象都包含对同一对象的引用会发生什么。当它们被读回时,它们会都指向同一个对象吗?答案是"是"。一个流只能包含一个对象的副本,尽管它可以包含任意数量的引用。因此,如果你明确地将一个对象两次写入流中,实际上只是写入了引用两次。例如,如果以下代码将对象ob两次写入流中:
Object ob = new Object(); out.writeObject(ob); out.writeObject(ob);
每个writeObject都必须与一个readObject匹配,因此读取流的代码看起来会像这样:
Object ob1 = in.readObject(); Object ob2 = in.readObject();
这导致了两个变量,ob1和ob2,它们都是指向同一个对象的引用。
然而,如果一个单一对象被写入两个不同的流,它实际上会被复制 — 一个程序读取这两个流将看到两个不同的对象。
文件 I/O(具有 NIO.2 功能)
注意: 本教程反映了 JDK 7 版本中引入的文件 I/O 机制。Java SE 6 版本的文件 I/O 教程很简短,但您可以下载包含早期文件 I/O 内容的Java SE Tutorial 2008-03-14版本的教程。
java.nio.file包及其相关包java.nio.file.attribute为文件 I/O 和访问默认文件系统提供了全面支持。尽管 API 有许多类,但您只需关注其中的一些入口点。您会发现这个 API 非常直观和易于使用。
本教程首先询问什么是路径?然后介绍了该包的主要入口点 Path 类。解释了与语法操作相关的Path类中的方法。然后教程转向包中的另一个主要类Files类,其中包含处理文件操作的方法。首先介绍了许多 file operations 共有的一些概念。然后介绍了用于检查、删除、复制和移动文件的方法。
教程展示了在继续学习 file I/O 和 directory I/O 之前如何管理元数据。解释了随机访问文件并检查了与符号链接和硬链接相关的问题。
接下来,涵盖了一些非常强大但更高级的主题。首先演示了递归遍历文件树的能力,然后介绍了如何使用通配符搜索文件的信息。接下来,解释并演示了如何监视目录以进行更改。然后,给出了一些其他地方不适用的方法的关注。
最后,如果您在 Java SE 7 发布之前编写了文件 I/O 代码,有一个从旧 API 到新 API 的映射,以及关于File.toPath方法的重要信息,供希望利用新 API 而无需重写现有代码的开发人员参考。
Java 中文官方教程 2022 版(八)(2)https://developer.aliyun.com/article/1486339